|
|
@ -1,6 +1,5 @@
|
|||
# Basic configuration
|
||||
# ------------------------------------------------------------
|
||||
MONGODB_CONNECTION_STRING=mongodb://host.docker.internal:27017/rowboat
|
||||
OPENAI_API_KEY=<OPENAI_API_KEY>
|
||||
|
||||
|
||||
|
|
|
|||
1
.gitignore
vendored
|
|
@ -1,3 +1,4 @@
|
|||
.DS_Store
|
||||
.env
|
||||
.vscode/
|
||||
data/
|
||||
|
|
|
|||
439
README.md
|
|
@ -1,339 +1,62 @@
|
|||

|
||||
|
||||
<h2 align="center">The AI-assisted agent builder</h2>
|
||||
<h2 align="center">Let AI build multi-agent workflows for you in minutes</h2>
|
||||
<h5 align="center">
|
||||
|
||||
[Quickstart](#quick-start) | [Docs](https://docs.rowboatlabs.com/) | [Website](https://www.rowboatlabs.com/) | [Discord](https://discord.gg/jHhUKkKHn8)
|
||||
|
||||
</h5>
|
||||
|
||||
A Cursor-like, AI-assisted, no-code IDE for building production-ready multi-agents.
|
||||
- ✨ **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**
|
||||
- Grab the project ID and generated API key from settings and use the API.
|
||||
|
||||
- ✨ Start from a simple prompt to create fully functional agents with the Copilot
|
||||
- 🧪 Test them in AI-simulated scenarios
|
||||
- 🌐 Connect MCP servers and tools
|
||||
- 📞 Interact through the Python SDK, a web widget, or a Twilio phone number
|
||||
- ♻️ Continuously refine your agents by providing feedback to the Copilot
|
||||
Powered by OpenAI's Agents SDK, Rowboat is the fastest way to build multi-agents!
|
||||
|
||||
## Quick start
|
||||
1. Set your OpenAI key
|
||||
```bash
|
||||
export OPENAI_API_KEY=your-openai-api-key
|
||||
```
|
||||
|
||||
|
||||
Built on OpenAI's Agents SDK, **Rowboat is the fastest way to build multi-agents!**
|
||||
|
||||
|
||||

|
||||
|
||||
# Quick start
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before running Rowboat, ensure you have:
|
||||
|
||||
1. **Docker Desktop**
|
||||
- [Download Docker Desktop](https://www.docker.com/products/docker-desktop)
|
||||
|
||||
2. **OpenAI API Key**
|
||||
- Obtain from your OpenAI account.
|
||||
|
||||
3. **MongoDB**
|
||||
- macOS (Homebrew)
|
||||
```bash
|
||||
brew tap mongodb/brew
|
||||
brew install mongodb-community@8.0
|
||||
brew services start mongodb-community@8.0
|
||||
```
|
||||
- Other platforms: Refer to the [MongoDB documentation](https://www.mongodb.com/docs/manual/installation/) for details.
|
||||
|
||||
## Setup Rowboat
|
||||
|
||||
1. **Clone the Repository**
|
||||
2. Clone the repository and start Rowboat docker
|
||||
```bash
|
||||
git clone git@github.com:rowboatlabs/rowboat.git
|
||||
cd rowboat
|
||||
```
|
||||
|
||||
2. **Environment Configuration**
|
||||
- Copy the `.env.example` file and rename it to `.env`:
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
- Open the new .env file and update the OPENAI_API_KEY:
|
||||
|
||||
```ini
|
||||
# OpenAI Configuration
|
||||
OPENAI_API_KEY=your-openai-api-key
|
||||
```
|
||||
|
||||
3. **Start the App**
|
||||
```bash
|
||||
docker-compose up --build
|
||||
```
|
||||
|
||||
4. **Access the App**
|
||||
- Visit [http://localhost:3000](http://localhost:3000).
|
||||
3. Access the app at [http://localhost:3000](http://localhost:3000).
|
||||
|
||||
## Demos
|
||||
|
||||
#### Create a multi-agent assistant with tools from a single prompt
|
||||
|
||||
[](https://www.youtube.com/watch?v=3t2Fpn6Vyds)
|
||||
|
||||
#### Add MCP servers
|
||||
|
||||
[](https://www.youtube.com/watch?v=EbkIPCTyD58)
|
||||
|
||||
#### Use Firecrawl's MCP server and build a quick url scraping agent
|
||||
|
||||
[](https://www.youtube.com/watch?v=_KZWla3Khco)
|
||||
|
||||
#### Improve agents with feedback
|
||||
|
||||
[](https://www.youtube.com/watch?v=uoCEQtOe7eE)
|
||||
|
||||
|
||||
Refer to [Docs](https://docs.rowboatlabs.com/) to learn how to start building agents with Rowboat.
|
||||
## Integrate with Rowboat agents
|
||||
|
||||
# Advanced
|
||||
|
||||
## 1. Tool Use
|
||||
|
||||
You can add your tools / APIs to Rowboat through (a) connecting MCP servers, or (b) connecting a webhook.
|
||||
|
||||
### 1.1 MCP Servers
|
||||
|
||||
You can intergrate any MCP server in Settings -> Tools -> MCP Servers. The Tools on the servers will show up inside Rowboats Tools section.
|
||||
|
||||
<img src="/assets/mcp-import.png" alt="ui" width="400"/>
|
||||
|
||||
Tip: You might want to set the MCP url as http://host.docker.internal/... to allow services to access the MCP servers on your localhost.
|
||||
|
||||
### 1.2 Webhook
|
||||
|
||||
You can point Rowboat to any webhook in Settings -> Tools -> Webhook.
|
||||
|
||||
Rowboat also includes a built-in webhook service that allows you to implement custom tool functions easily. To use this feature:
|
||||
|
||||
1. **Generate Signing Secret**
|
||||
Generate a secret for securing webhook requests:
|
||||
```bash
|
||||
openssl rand -hex 32
|
||||
```
|
||||
|
||||
2. **Update Environment Variables**
|
||||
```ini
|
||||
SIGNING_SECRET=<your-generated-secret>
|
||||
```
|
||||
|
||||
3. **Implement Your Functions**
|
||||
Add your custom functions to `apps/tools_webhook/function_map.py`:
|
||||
```python
|
||||
def get_weather(location: str, units: str = "metric"):
|
||||
"""Return weather data for the given location."""
|
||||
# Your implementation here
|
||||
return {"temperature": 20, "conditions": "sunny"}
|
||||
|
||||
def check_inventory(product_id: str):
|
||||
"""Check inventory levels for a product."""
|
||||
# Your implementation here
|
||||
return {"in_stock": 42, "warehouse": "NYC"}
|
||||
|
||||
# Add your functions to the map
|
||||
FUNCTIONS_MAP = {
|
||||
"get_weather": get_weather,
|
||||
"check_inventory": check_inventory
|
||||
}
|
||||
```
|
||||
|
||||
4. **Start the Tools Webhook Service**
|
||||
```bash
|
||||
docker compose --profile tools_webhook up -d
|
||||
```
|
||||
|
||||
5. **Register Tools in Rowboat**
|
||||
- Navigate to your project config at `/projects/<PROJECT_ID>/config`
|
||||
- Ensure that the webhook URL is set to: `http://tools_webhook:3005/tool_call`
|
||||
- Tools will automatically be forwarded to your webhook implementation
|
||||
|
||||
The webhook service handles all the security and parameter validation, allowing you to focus on implementing your tool logic.
|
||||
|
||||
## 2. Retrieve Augmented Generation (RAG)
|
||||
|
||||
Rowboat supports adding text directly, document uploads or scraping URLs to enhance the responses with your custom knowledge base. Rowboat uses Qdrant as the vector DB.
|
||||
|
||||
### 2.1 Setup Qdrant
|
||||
To enable RAG you need to first setup Qdrant.
|
||||
|
||||
1. Option1: Run Qdrant locally
|
||||
- Run Qdrant docker
|
||||
```bash
|
||||
docker run -p 6333:6333 qdrant/qdrant
|
||||
```
|
||||
- Update environment variables
|
||||
```ini
|
||||
USE_RAG=true
|
||||
QDRANT_URL=http://localhost:6333
|
||||
QDRANT_API_KEY=<your-api-key> # Only needed for Qdrant Cloud
|
||||
```
|
||||
2. Option2: Use [Qdrant Cloud](https://cloud.qdrant.io/)
|
||||
- Note your cluster URL and API key
|
||||
- Update environment variables
|
||||
```ini
|
||||
USE_RAG=true
|
||||
QDRANT_URL=<your-qdrant-cloud-url>
|
||||
QDRANT_API_KEY=<your-api-key>
|
||||
```
|
||||
3. Initialize Qdrant Collections
|
||||
```bash
|
||||
docker compose --profile setup_qdrant up setup_qdrant
|
||||
```
|
||||
|
||||
If you need to delete the collections and start fresh, you can run:
|
||||
```bash
|
||||
docker compose --profile delete_qdrant up delete_qdrant
|
||||
```
|
||||
### 2.2 Adding Knowledge Base for RAG
|
||||
|
||||
You can add a knowledge corpus to Rowboat by directly adding text information, uploading supported files or by pointing Rowboat to URLs for scraping.
|
||||
|
||||
#### (a) Create Text for Knowledge
|
||||
Rowboat support directly creating a corpus of knowledge inside the platform.
|
||||
|
||||
- Start the Text Worker
|
||||
```bash
|
||||
docker compose --profile rag_text_worker up -d
|
||||
```
|
||||
#### (b) Scrape URLs for Knowledge
|
||||
|
||||
Rowboat supports scraping urls using Firecrawl. To setup scraping:
|
||||
|
||||
1. Get Firecrawl API Key
|
||||
- Sign up at [Firecrawl](https://firecrawl.co)
|
||||
- Generate an API key
|
||||
|
||||
2. Update Environment Variables
|
||||
```ini
|
||||
USE_RAG_SCRAPING=true
|
||||
FIRECRAWL_API_KEY=<your-firecrawl-api-key>
|
||||
```
|
||||
|
||||
3. Start the URLs Worker
|
||||
```bash
|
||||
docker compose --profile rag_urls_worker up -d
|
||||
```
|
||||
|
||||
#### (c) Upload Files for Knowledge
|
||||
|
||||
Rowboat supports file uploads (PDF, DOCX, TXT) for your knowledge base. It uses Google's Gemini LLM to convert the documents to Markdown before indexing:
|
||||
|
||||
1. Prerequisites
|
||||
- An AWS S3 bucket for file storage
|
||||
- Google Cloud API key with Generative Language (Gemini) API enabled (for enhanced document parsing)
|
||||
|
||||
2. Configure AWS S3
|
||||
- Create an S3 bucket
|
||||
- Add the following CORS configuration to your bucket:
|
||||
```json
|
||||
[
|
||||
{
|
||||
"AllowedHeaders": [
|
||||
"*"
|
||||
],
|
||||
"AllowedMethods": [
|
||||
"PUT",
|
||||
"POST",
|
||||
"DELETE",
|
||||
"GET"
|
||||
],
|
||||
"AllowedOrigins": [
|
||||
"http://localhost:3000",
|
||||
],
|
||||
"ExposeHeaders": [
|
||||
"ETag"
|
||||
]
|
||||
}
|
||||
]
|
||||
```
|
||||
- Ensure your AWS credentials have the following IAM policy:
|
||||
```json
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Sid": "VisualEditor0",
|
||||
"Effect": "Allow",
|
||||
"Action": [
|
||||
"s3:PutObject",
|
||||
"s3:GetObject",
|
||||
"s3:DeleteObject",
|
||||
"s3:ListBucket"
|
||||
],
|
||||
"Resource": [
|
||||
"arn:aws:s3:::<your-bucket-name>/*",
|
||||
"arn:aws:s3:::<your-bucket-name>"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
3. Update Environment Variables
|
||||
```ini
|
||||
USE_RAG_UPLOADS=true
|
||||
AWS_ACCESS_KEY_ID=<your-aws-access-key>
|
||||
AWS_SECRET_ACCESS_KEY=<your-aws-secret-key>
|
||||
RAG_UPLOADS_S3_BUCKET=<your-s3-bucket-name>
|
||||
RAG_UPLOADS_S3_REGION=<your-s3-region>
|
||||
GOOGLE_API_KEY=<your-google-api-key>
|
||||
```
|
||||
|
||||
4. Start the Files Worker
|
||||
```bash
|
||||
docker compose --profile rag_files_worker up -d
|
||||
```
|
||||
|
||||
After enabling RAG and starting the required workers, you can manage your knowledge base through the Rowboat UI at `/projects/<PROJECT_ID>/sources`.
|
||||
|
||||
## 3. Chat Widget
|
||||
|
||||
Rowboat provides an embeddable chat widget that you can add to any website. To enable and use the chat widget:
|
||||
|
||||
1. **Generate JWT Secret**
|
||||
Generate a secret for securing chat widget sessions:
|
||||
```bash
|
||||
openssl rand -hex 32
|
||||
```
|
||||
|
||||
2. **Update Environment Variables**
|
||||
```ini
|
||||
USE_CHAT_WIDGET=true
|
||||
CHAT_WIDGET_SESSION_JWT_SECRET=<your-generated-secret>
|
||||
```
|
||||
|
||||
3. **Start the Chat Widget Service**
|
||||
```bash
|
||||
docker compose --profile chat_widget up -d
|
||||
```
|
||||
|
||||
4. **Add Widget to Your Website**
|
||||
You can find the chat-widget embed code under `/projects/<PROJECT_ID>/config`
|
||||
|
||||
After setup, the chat widget will appear on your website and connect to your Rowboat project.
|
||||
|
||||
|
||||
## 4. Interact with Rowboat API
|
||||
|
||||
There are two ways to interact with Rowboat's API:
|
||||
|
||||
1. **Option 1: Python SDK**
|
||||
|
||||
|
||||
For Python applications, we provide an official SDK for easier integration:
|
||||
```bash
|
||||
pip install rowboat
|
||||
```
|
||||
|
||||
```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": "Tell me the weather in London"}]
|
||||
response_messages, state = client.chat(messages=messages)
|
||||
```
|
||||
|
||||
For more details, see the [Python SDK documentation](./apps/python-sdk/README.md).
|
||||
|
||||
1. **Option 2: HTTP 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`
|
||||
There are 2 ways to integrate with the agents you create in Rowboat
|
||||
|
||||
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' \
|
||||
|
|
@ -347,61 +70,37 @@ There are two ways to interact with Rowboat's API:
|
|||
]
|
||||
}'
|
||||
```
|
||||
which gives:
|
||||
```json
|
||||
{
|
||||
"messages": [
|
||||
{
|
||||
"role": "assistant",
|
||||
"tool_calls": [
|
||||
{
|
||||
"function": {
|
||||
"arguments": "{\"location\":\"London\",\"units\":\"metric\"}",
|
||||
"name": "weather_lookup_tool"
|
||||
},
|
||||
"id": "call_r6XKuVxmGRogofkyFZIacdL0",
|
||||
"type": "function"
|
||||
}
|
||||
],
|
||||
"agenticSender": "Example Agent",
|
||||
"agenticResponseType": "internal"
|
||||
}
|
||||
],
|
||||
"state": {
|
||||
// .. state data
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
2. Python SDK
|
||||
- You can use the included Python SDK to interact with the Agents
|
||||
- See [SDK Docs](https://docs.rowboatlabs.com/using_the_sdk/) for details
|
||||
```python
|
||||
from rowboat import Client
|
||||
from rowboat.schema import UserMessage, SystemMessage
|
||||
|
||||
# Initialize the client
|
||||
client = Client(
|
||||
host="<HOST>",
|
||||
project_id="<PROJECT_ID>",
|
||||
api_key="<API_KEY>"
|
||||
)
|
||||
|
||||
# Create messages
|
||||
messages = [
|
||||
SystemMessage(role='system', content="You are a helpful assistant"),
|
||||
UserMessage(role='user', content="Hello, how are you?")
|
||||
]
|
||||
|
||||
# Get response
|
||||
response_messages, state = client.chat(messages=messages)
|
||||
print(response_messages[-1].content)
|
||||
|
||||
# For subsequent messages, include previous messages and state
|
||||
messages.extend(response_messages)
|
||||
messages.append(UserMessage(role='user', content="What's your name?"))
|
||||
response_messages, state = client.chat(messages=messages, state=state)
|
||||
```
|
||||
|
||||
### 5. Authentication
|
||||
|
||||
By default, Rowboat runs without authentication. To enable user authentication using Auth0:
|
||||
|
||||
1. **Auth0 Setup**
|
||||
- **Create an Auth0 Account**: Sign up at [Auth0](https://auth0.com).
|
||||
- **Create a New Application**: Choose "Regular Web Application", select "Next.js" as the application type, and name it "Rowboat".
|
||||
- **Configure Application**:
|
||||
- **Allowed Callback URLs**: In the Auth0 Dashboard, go to your "Rowboat" application settings and set `http://localhost:3000/api/auth/callback` as an Allowed Callback URL.
|
||||
- **Get Credentials**: Collect the following from your Auth0 application settings:
|
||||
- **Domain**: Copy your Auth0 domain (ensure you append `https://` to the Domain that the Auth0 dashboard shows you)
|
||||
- **Client ID**: Your application's unique identifier
|
||||
- **Client Secret**: Your application's secret key
|
||||
- **Generate secret**: Generate a session encryption secret in your terminal and note the output for later:
|
||||
```bash
|
||||
openssl rand -hex 32
|
||||
```
|
||||
|
||||
2. **Update Environment Variables**
|
||||
Add the following to your `.env` file:
|
||||
```ini
|
||||
USE_AUTH=true
|
||||
AUTH0_SECRET=your-generated-secret # Generated using openssl command
|
||||
AUTH0_BASE_URL=http://localhost:3000 # Your application's base URL
|
||||
AUTH0_ISSUER_BASE_URL=https://example.auth0.com # Your Auth0 domain (ensure it is prefixed with https://)
|
||||
AUTH0_CLIENT_ID=your-client-id
|
||||
AUTH0_CLIENT_SECRET=your-client-secret
|
||||
```
|
||||
|
||||
After enabling authentication, users will need to sign in to access the application.
|
||||
|
||||
|
||||
Refer to [Docs](https://docs.rowboatlabs.com/) to learn how to start building agents with Rowboat.
|
||||
|
|
|
|||
|
|
@ -1,19 +1,19 @@
|
|||
## Add tools to agents
|
||||
Copilot can help you 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.
|
||||
|
||||
### Instruct copilot to add tools
|
||||

|
||||
|
||||

|
||||
### Adding mock tools
|
||||
You can mock any tool you have created by checking the 'Mock tool responses' option.
|
||||
|
||||
### Inspect tools and agents
|
||||
Note how copilot not only creates the tool definitions for you, but also updates the relevant agent instructions to use the tool and connects the tool to the agent.
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
### Adding MCP tools
|
||||
|
||||
[](https://www.youtube.com/watch?v=EbkIPCTyD58)
|
||||
|
||||
|
||||
### 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.
|
||||
|
||||

|
||||

|
||||

|
||||
|
|
@ -19,21 +19,11 @@ The agent uses examples as a reference for behavior in different scenarios. Whil
|
|||
### Prompts
|
||||
Prompts attached to an agent will be used by the agent in addition to instructions.
|
||||
|
||||
### RAG
|
||||
Data sources added to an agent will be used as knowledge, retrieved using embedding match in a typical RAG fashion. Advanced configurations allow for setting number of matches, etc. RAG is currently implemented as a predefined tool call which the agent will use when it determines that it needs to retrieve knowledge. This behavior can be further fine-tuned by specifying corresponding instructions or prompts.
|
||||
|
||||
### 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 graph, connected agents refer to children of an agent. An agent can choose to transfer control of the conversation to one of its children, by using internal tool calls (need not be configured separately). Similar to tools, the behavior of when to transfer the chat to a child agent can be fine-tuned by specifying corresponding instructions, examples and prompts.
|
||||
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 any of the OpenAI LLMs.
|
||||
|
||||
### Conversation control after turn
|
||||
This setting specifies different options for control of conversation after the current agent has put out a user-facing response (i.e., completed the turn). Currently available options are:
|
||||
|
||||
1. Retain control for the next turn of conversation (most common and default setting)
|
||||
2. Give up control to the parent agent (used when the agent has narrow scope such as answering a FAQ)
|
||||
3. Give up control to the agent designated as Start agent
|
||||
RowBoat currently supports OpenAI LLMs. Agents can be configured to use GPT-4o or GPT-4o-mini.
|
||||
|
|
@ -4,20 +4,17 @@ 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.
|
||||
|
||||

|
||||
[](https://www.youtube.com/watch?v=3t2Fpn6Vyds)
|
||||
|
||||
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, Examples and Connected Agents in each agent.
|
||||
Once you apply changes, inspect the agents to see how copilot has built them. Specifically, note the Instructions, and Examples in each agent.
|
||||
|
||||

|
||||

|
||||
|
||||
Also notice that copilot would likely have created a "Hub" agent that is "connected" to other agents.
|
||||
|
||||

|
||||
|
||||
### Make changes if needed
|
||||
Tweak the instructions and examples manually if needed.
|
||||
Tweak the instructions and examples through the copilot, or generate instructions button, or by manually editing it.
|
||||
|
||||

|
||||
[](https://www.youtube.com/watch?v=uoCEQtOe7eE)
|
||||
BIN
apps/docs/docs/img/agent-instruction.png
Normal file
|
After Width: | Height: | Size: 152 KiB |
BIN
apps/docs/docs/img/chat-delivery.png
Normal file
|
After Width: | Height: | Size: 114 KiB |
|
Before Width: | Height: | Size: 87 KiB After Width: | Height: | Size: 88 KiB |
BIN
apps/docs/docs/img/mock-response.png
Normal file
|
After Width: | Height: | Size: 135 KiB |
BIN
apps/docs/docs/img/mock-tool.png
Normal file
|
After Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 41 KiB |
|
|
@ -1,8 +1,8 @@
|
|||
# Welcome to Rowboat
|
||||
|
||||
Rowboat is an open-source Cursor like IDE that helps you build, test, and deploy multi-agent AI systems.
|
||||
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 both developers who would like to self-host our [open-source code](https://github.com/rowboatlabs/rowboat/) as well as users of our [hosted (managed) app](https://app.rowboatlabs.com/).
|
||||
**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/jHhUKkKHn8)
|
||||
|
|
@ -12,7 +12,7 @@ Rowboat is an open-source Cursor like IDE that helps you build, test, and deploy
|
|||
## 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.
|
||||
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.
|
||||
|
||||
|
|
@ -26,29 +26,22 @@ RowBoat Studio lets you create AI agents in minutes, using a visual interface an
|
|||
| 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|
|
||||
| Simulator | Simulates real-world user interactions<br>with your assistant |• Maintain and run a test-bench of different scenarios<br>• Mock tool responses for quick testing<br>• Reproduce your end-user's experience comprehensively|
|
||||
|
||||
### RowBoat Chat API & SDK
|
||||
- RowBoat Chat 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 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.
|
||||
- [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 **tools** by connecting them to your APIs
|
||||
3. Configure **MCP servers** and **tools** and connect them to agents
|
||||
4. Chat with your assistant in the **playground**
|
||||
5. Create and run a test-bench of scenarios in the **simulator**
|
||||
6. Deploy the current version to production, with **version control**
|
||||
|
||||
**RowBoat SDK:**
|
||||
|
||||
1. **Integrate** the SDK into your end-user facing chat application. Use the latest deployed version of your assistant from RowBoat Studio, by specifying your RowBoat API key.
|
||||
2. Alternatively, **export** your assistant as a JSON artifact from RowBoat Studio and use it to power your custom implementations.
|
||||
6. Deploy and use the HTTP API or Python SDK to integrate the agents into your system
|
||||
|
||||
## Why RowBoat?
|
||||
Accelerate your path to production-ready multi-agent systems.
|
||||
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
|
||||
|
|
@ -56,5 +49,5 @@ Accelerate your path to production-ready multi-agent systems.
|
|||
|
||||
## Getting started
|
||||
|
||||
- To set up our open-source installation, see [this guide](/installation)
|
||||
- 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)
|
||||
|
|
@ -4,12 +4,4 @@
|
|||
|
||||
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.
|
||||
|
||||

|
||||
|
||||
In the playground, you can also set initial context at start of chat, that will be passed to all agents. This is typically used for providing user identity information such as user ID, login email, etc.
|
||||

|
||||
|
||||
### Ask copilot questions
|
||||
You can ask copilot clarifications about the chat, such as why the agents responded a certain way or why an agent was invoked.
|
||||
|
||||

|
||||

|
||||
|
|
@ -3,4 +3,4 @@
|
|||
- 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 a Webhook URL in Studio, to which all tool calls will be routed.
|
||||
- Developers can easily connect tools to APIs by configuring MCP servers or Webhook URL in Settings.
|
||||
|
|
@ -10,27 +10,27 @@ This is a guide on using the HTTP API to power conversations with the assistant
|
|||
Generate API keys via the developer configs in your project. Copy the Project ID from the same page.
|
||||

|
||||
|
||||
## Call the API
|
||||
## API Endpoint
|
||||
|
||||
When you provide your Project ID in the API call, RowBoat uses the version of your assistant deployed to production.
|
||||
```
|
||||
POST <HOST>/api/v1/<PROJECT_ID>/chat
|
||||
```
|
||||
|
||||
**Request parameters:**
|
||||
Where:
|
||||
|
||||
- `messages`: history of all messages in the conversation till now (system, user, tool and assistant messages)
|
||||
- `state`: generated from the previous turn (this is needed because the API does not maintain state on its own)
|
||||
- For self-hosted: `<HOST>` is `http://localhost:3000`
|
||||
|
||||
**Response parameters:**
|
||||
## Authentication
|
||||
|
||||
- `messages`: assistant responses for the current turn (the last message in `messages` is either the user-facing response or a tool call by the assistant)
|
||||
- `state`: to be passed to the next turn
|
||||
Include your API key in the Authorization header:
|
||||
|
||||
### 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)
|
||||
```
|
||||
Authorization: Bearer <API_KEY>
|
||||
```
|
||||
|
||||
### Example first turn of a chat
|
||||
## Examples
|
||||
|
||||
#### Request
|
||||
### First Turn
|
||||
|
||||
```bash
|
||||
curl --location '<HOST>/api/v1/<PROJECT_ID>/chat' \
|
||||
|
|
@ -38,206 +38,129 @@ curl --location '<HOST>/api/v1/<PROJECT_ID>/chat' \
|
|||
--header 'Authorization: Bearer <API_KEY>' \
|
||||
--data '{
|
||||
"messages": [
|
||||
{
|
||||
"role": "system",
|
||||
"content": "UserID: 345227"
|
||||
// Provide context to be passed to all agents in the assistant
|
||||
// E.g. user identity info (user ID) for logged in users
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "What is my outstanding balance and how do I make the payment?"
|
||||
"content": "Hello, can you help me?"
|
||||
}
|
||||
],
|
||||
"state": {
|
||||
"last_agent_name": "Credit Card Hub"
|
||||
// Last agent used in the previous turn
|
||||
// Set to the "start agent" for first turn of chats
|
||||
}
|
||||
"state": null
|
||||
}'
|
||||
```
|
||||
#### Response
|
||||
|
||||
Response:
|
||||
```json
|
||||
{
|
||||
"messages": [
|
||||
{
|
||||
"sender": "Credit Card Hub",
|
||||
"role": "assistant",
|
||||
"response_type": "internal",
|
||||
"content": null,
|
||||
"current_turn": true,
|
||||
"tool_calls": [
|
||||
{
|
||||
"function": {
|
||||
// Internal tool calls are used to transfer between agents
|
||||
"name": "transfer_to_outstanding_payments",
|
||||
"arguments": "{\"args\":\"\",\"kwargs\":\"\"}"
|
||||
},
|
||||
"id": "call_SLyQKXt9ZMqnxSqJjo9j1fU5",
|
||||
"type": "function"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"role": "tool",
|
||||
"tool_name": "transfer_to_outstanding_payments",
|
||||
"content": "{\"assistant\": \"Outstanding Payments\"}",
|
||||
"tool_call_id": "call_SLyQKXt9ZMqnxSqJjo9j1fU5"
|
||||
},
|
||||
{
|
||||
// Last message in response messages is a tool call
|
||||
"sender": "Outstanding Payments",
|
||||
"role": "assistant",
|
||||
"response_type": "internal",
|
||||
"content": null,
|
||||
"current_turn": true,
|
||||
"tool_calls": [
|
||||
{
|
||||
"function": {
|
||||
"name": "get_outstanding_balance",
|
||||
"arguments": "{\"user_id\":\"345227\"}"
|
||||
},
|
||||
"id": "call_MNAUg7UTszYMt5RL4n5QqUTw",
|
||||
"type": "function"
|
||||
}
|
||||
]
|
||||
"content": "Hello! Yes, I'd be happy to help you. What can I assist you with today?",
|
||||
"agenticResponseType": "external"
|
||||
}
|
||||
],
|
||||
"state": {
|
||||
"agent_data": [
|
||||
// Agents that were involved in this turn
|
||||
{
|
||||
"name": "Credit Card Hub",
|
||||
"instructions": "// agent instructions",
|
||||
"history": [
|
||||
// History of agent-relevant messages
|
||||
// in the same format as "messages"
|
||||
],
|
||||
"child_functions": [
|
||||
"transfer_to_outstanding_payments",
|
||||
"transfer_to_transaction_disputes",
|
||||
"transfer_to_rewards_redemption"
|
||||
],
|
||||
},
|
||||
{
|
||||
"name": "Outstanding Payments",
|
||||
"instructions": // Agent instructions,
|
||||
"history": [
|
||||
// History of agent-relevant messages
|
||||
// in the same format as "messages"
|
||||
],
|
||||
"external_tools": [
|
||||
"get_outstanding_balance",
|
||||
"get_saved_credit_card"
|
||||
],
|
||||
},
|
||||
|
||||
// Other agents - have not yet participated in the conversation
|
||||
{
|
||||
"name": "Rewards Redemption",
|
||||
"instructions": // Agent instructions,
|
||||
"history": [], //
|
||||
}
|
||||
],
|
||||
"last_agent_name": "Outstanding Payments"
|
||||
"last_agent_name": "MainAgent"
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Example where the assistant is expecting a tool response
|
||||
#### Request
|
||||
### Subsequent Turn
|
||||
|
||||
Notice how we include both the previous messages and the state from the last response:
|
||||
|
||||
```bash
|
||||
curl --location 'http://localhost:3000/api/v1/<PROJECT_ID>/chat' \
|
||||
curl --location '<HOST>/api/v1/<PROJECT_ID>/chat' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--header 'Authorization: Bearer <API_KEY>' \
|
||||
--data '{
|
||||
"messages": [
|
||||
{
|
||||
"role": "system",
|
||||
"content": "UserID: 345227"
|
||||
"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 is my outstanding balance and how do I make the payment?"
|
||||
},
|
||||
{
|
||||
"sender": "Credit Card Hub",
|
||||
"role": "assistant",
|
||||
"response_type": "internal",
|
||||
"content": null,
|
||||
"tool_calls": [
|
||||
{
|
||||
"function": {
|
||||
"arguments": "{\"args\":\"\",\"kwargs\":\"\"}",
|
||||
"name": "transfer_to_outstanding_payments"
|
||||
},
|
||||
"id": "call_SLyQKXt9ZMqnxSqJjo9j1fU5",
|
||||
"type": "function"
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
"role": "tool",
|
||||
"tool_name": "transfer_to_outstanding_payments",
|
||||
"content": "{\"assistant\": \"Outstanding Payments\"}",
|
||||
"tool_call_id": "call_SLyQKXt9ZMqnxSqJjo9j1fU5"
|
||||
},
|
||||
{
|
||||
"sender": "Outstanding Payments",
|
||||
"role": "assistant",
|
||||
"response_type": "internal",
|
||||
"content": null,
|
||||
"tool_calls": [
|
||||
{
|
||||
"function": {
|
||||
"arguments": "{\"user_id\":\"345227\"}",
|
||||
"name": "get_outstanding_balance"
|
||||
},
|
||||
"id": "call_MNAUg7UTszYMt5RL4n5QqUTw",
|
||||
"type": "function"
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
// New message is a tool response to the previous tool call
|
||||
"role": "tool",
|
||||
"tool_name": "get_outstanding_balance"
|
||||
"content": "{\"result\":{\"outstanding_balance\":\"$250.00\",\"due_date\":\"2025-02-15\",\"payment_methods\":[\"Credit Card\",\"Bank Transfer\",\"PayPal\"]}}",
|
||||
"tool_call_id": "call_MNAUg7UTszYMt5RL4n5QqUTw",
|
||||
},
|
||||
],
|
||||
"state": {
|
||||
// State returned by the API in the previous turn
|
||||
}
|
||||
}'
|
||||
```
|
||||
#### Response
|
||||
```json
|
||||
{
|
||||
"messages": [
|
||||
{
|
||||
"sender": "Outstanding Payments",
|
||||
"role": "assistant",
|
||||
// Response is not user-facing, to enable further post processing
|
||||
"response_type": "internal",
|
||||
"content": "Your outstanding balance is $250.00, due by February 15, 2025.\n\nYou have several payment options available, including:\n- **Credit Card**\n- **Bank Transfer**\n- **PayPal**\n\nPlease let me know which option you'd like to use, and I'll guide you through the process!",
|
||||
"current_turn": true
|
||||
},
|
||||
{
|
||||
"sender": "Outstanding Payments >> Post process",
|
||||
"role": "assistant",
|
||||
// Response is user-facing
|
||||
"response_type": "external",
|
||||
"content": "Your outstanding balance is $250.00, due by February 15, 2025. \n\nPayment options include:\n- **Credit Card:** You can use your saved Visa card ending in 1234.\n- **Bank Transfer**\n- **PayPal**\n\nLet me know your preferred payment method, and I’ll assist you!",
|
||||
"current_turn": true,
|
||||
"content": "What services do you offer?"
|
||||
}
|
||||
],
|
||||
"state": {
|
||||
"agent_data": [
|
||||
// Omitted for brevity
|
||||
],
|
||||
"last_agent_name": "Outstanding Payments"
|
||||
"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
|
||||
|
|
@ -13,13 +13,68 @@ This is a guide on using the RowBoat Python SDK as an alternative to the [RowBoa
|
|||
|
||||
## Usage
|
||||
|
||||
### Basic Usage
|
||||
### Basic Usage with StatefulChat
|
||||
|
||||
Initialize a client and use the chat method directly:
|
||||
The easiest way to interact with Rowboat is using the `StatefulChat` class, which maintains conversation state automatically:
|
||||
|
||||
```python
|
||||
from rowboat import Client
|
||||
from rowboat.schema import UserMessage, SystemMessage
|
||||
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(
|
||||
|
|
@ -30,57 +85,15 @@ client = Client(
|
|||
|
||||
# Create messages
|
||||
messages = [
|
||||
SystemMessage(role='system', content="You are a helpful assistant"),
|
||||
UserMessage(role='user', content="Hello, how are you?")
|
||||
]
|
||||
|
||||
# Get response
|
||||
response_messages, state = client.chat(messages=messages)
|
||||
print(response_messages[-1].content)
|
||||
response = client.chat(messages=messages)
|
||||
print(response.messages[-1].content)
|
||||
|
||||
# For subsequent messages, include previous messages and state
|
||||
messages.extend(response_messages)
|
||||
# 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_messages, state = client.chat(messages=messages, state=state)
|
||||
```
|
||||
|
||||
### Using Tools
|
||||
|
||||
The SDK supports function calling through tools:
|
||||
|
||||
```python
|
||||
def weather_lookup(city_name: str) -> str:
|
||||
return f"The weather in {city_name} is 22°C."
|
||||
|
||||
# Create a tools dictionary
|
||||
tools = {
|
||||
'weather_lookup': weather_lookup
|
||||
}
|
||||
|
||||
# Use tools with the chat method
|
||||
response_messages, state = client.chat(
|
||||
messages=messages,
|
||||
tools=tools
|
||||
)
|
||||
```
|
||||
The last message in `response_messages` is either a user-facing response or a tool call by the assistant.
|
||||
|
||||
### Stateful Chat (Convenience Wrapper)
|
||||
|
||||
For simpler use cases, the SDK provides a `StatefulChat` class that maintains conversation state automatically:
|
||||
|
||||
```python
|
||||
from rowboat import StatefulChat
|
||||
|
||||
# Initialize stateful chat
|
||||
chat = StatefulChat(
|
||||
client,
|
||||
tools=tools,
|
||||
system_prompt="You are a helpful assistant."
|
||||
)
|
||||
|
||||
# Simply send messages and get responses
|
||||
response = chat.run("Hello, how are you?")
|
||||
print(response)
|
||||
# I'm good, thanks! How can I help you today?
|
||||
response = client.chat(messages=messages, state=response.state)
|
||||
```
|
||||
|
|
@ -8,18 +8,11 @@ nav:
|
|||
- Introduction: index.md
|
||||
- Open Source License: license.md
|
||||
|
||||
- Getting Started:
|
||||
- Option 1 - Open Source: oss_installation.md
|
||||
- Option 2 - Hosted App: hosted_setup.md
|
||||
- Testing Your Setup: testing.md
|
||||
|
||||
- Building in Studio:
|
||||
- Overview: studio_overview.md
|
||||
- Create agents: create_agents.md
|
||||
- Test chats in the playground: playground.md
|
||||
- Add tools: add_tools.md
|
||||
- Update agents: update_agents.md
|
||||
- Simulate scenarios: simulate.md
|
||||
|
||||
- API & SDK:
|
||||
- Using the API: using_the_api.md
|
||||
|
|
@ -27,7 +20,5 @@ nav:
|
|||
|
||||
- Concepts:
|
||||
- Agents: agents.md
|
||||
- Graph: graph.md
|
||||
- Tools: tools.md
|
||||
- Prompts: prompts.md
|
||||
- Data Sources: data_sources.md
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 391 B After Width: | Height: | Size: 391 B |
|
Before Width: | Height: | Size: 1 KiB After Width: | Height: | Size: 1 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 128 B After Width: | Height: | Size: 128 B |
|
Before Width: | Height: | Size: 385 B After Width: | Height: | Size: 385 B |
|
|
@ -12,13 +12,68 @@ pip install rowboat
|
|||
|
||||
## Usage
|
||||
|
||||
### Basic Usage
|
||||
### Basic Usage with StatefulChat
|
||||
|
||||
Initialize a client and use the chat method directly:
|
||||
The easiest way to interact with Rowboat is using the `StatefulChat` class, which maintains conversation state automatically:
|
||||
|
||||
```python
|
||||
from rowboat import Client
|
||||
from rowboat.schema import UserMessage, SystemMessage
|
||||
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(
|
||||
|
|
@ -29,108 +84,15 @@ client = Client(
|
|||
|
||||
# Create messages
|
||||
messages = [
|
||||
SystemMessage(role='system', content="You are a helpful assistant"),
|
||||
UserMessage(role='user', content="Hello, how are you?")
|
||||
]
|
||||
|
||||
# Get response
|
||||
response_messages, state = client.chat(messages=messages)
|
||||
print(response_messages[-1].content)
|
||||
response = client.chat(messages=messages)
|
||||
print(response.messages[-1].content)
|
||||
|
||||
# For subsequent messages, include previous messages and state
|
||||
messages.extend(response_messages)
|
||||
# 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_messages, state = client.chat(messages=messages, state=state)
|
||||
```
|
||||
|
||||
### Using Tools
|
||||
|
||||
The SDK supports function calling through tools:
|
||||
|
||||
```python
|
||||
def weather_lookup(city_name: str) -> str:
|
||||
return f"The weather in {city_name} is 22°C."
|
||||
|
||||
# Create a tools dictionary
|
||||
tools = {
|
||||
'weather_lookup': weather_lookup
|
||||
}
|
||||
|
||||
# Use tools with the chat method
|
||||
response_messages, state = client.chat(
|
||||
messages=messages,
|
||||
tools=tools
|
||||
)
|
||||
```
|
||||
|
||||
### Stateful Chat (Convenience Wrapper)
|
||||
|
||||
For simpler use cases, the SDK provides a `StatefulChat` class that maintains conversation state automatically:
|
||||
|
||||
```python
|
||||
from rowboat import StatefulChat
|
||||
|
||||
# Initialize stateful chat
|
||||
chat = StatefulChat(
|
||||
client,
|
||||
tools=tools,
|
||||
system_prompt="You are a helpful assistant."
|
||||
)
|
||||
|
||||
# Simply send messages and get responses
|
||||
response = chat.run("Hello, how are you?")
|
||||
print(response)
|
||||
# I'm good, thanks! How can I help you today?
|
||||
```
|
||||
|
||||
### Advanced Usage
|
||||
|
||||
#### Using a specific workflow
|
||||
|
||||
```python
|
||||
response_messages, state = client.chat(
|
||||
messages=messages,
|
||||
workflow_id="<WORKFLOW_ID>"
|
||||
)
|
||||
|
||||
# or
|
||||
|
||||
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
|
||||
response_messages, state = client.chat(
|
||||
messages=messages,
|
||||
test_profile_id="<TEST_PROFILE_ID>"
|
||||
)
|
||||
|
||||
# or
|
||||
|
||||
chat = StatefulChat(
|
||||
client,
|
||||
test_profile_id="<TEST_PROFILE_ID>"
|
||||
)
|
||||
```
|
||||
|
||||
#### Skip tool call runs
|
||||
This will surface the tool calls to the SDK instead of running them automatically on the Rowboat server.
|
||||
|
||||
```python
|
||||
response_messages, state = client.chat(
|
||||
messages=messages,
|
||||
skip_tool_calls=True
|
||||
)
|
||||
|
||||
# or
|
||||
|
||||
chat = StatefulChat(
|
||||
client,
|
||||
skip_tool_calls=True
|
||||
)
|
||||
response = client.chat(messages=messages, state=response.state)
|
||||
```
|
||||
|
|
|
|||
|
|
@ -4,9 +4,9 @@ build-backend = "hatchling.build"
|
|||
|
||||
[project]
|
||||
name = "rowboat"
|
||||
version = "2.1.0"
|
||||
version = "3.0.0"
|
||||
authors = [
|
||||
{ name = "Your Name", email = "your.email@example.com" },
|
||||
{ name = "Ramnique Singh", email = "ramnique@rowboatlabs.com" },
|
||||
]
|
||||
description = "Python sdk for the Rowboat API"
|
||||
readme = "README.md"
|
||||
|
|
|
|||
|
|
@ -1,18 +1,14 @@
|
|||
from typing import Dict, List, Optional, Any, Callable, Union, Tuple
|
||||
from typing import Dict, List, Optional, Any, Union
|
||||
import requests
|
||||
import json
|
||||
from .schema import (
|
||||
ApiRequest,
|
||||
ApiResponse,
|
||||
ApiMessage,
|
||||
ToolMessage,
|
||||
UserMessage,
|
||||
SystemMessage,
|
||||
AssistantMessage,
|
||||
AssistantMessageWithToolCalls
|
||||
)
|
||||
|
||||
|
||||
class Client:
|
||||
def __init__(self, host: str, project_id: str, api_key: str) -> None:
|
||||
self.base_url: str = f'{host}/api/v1/{project_id}/chat'
|
||||
|
|
@ -25,16 +21,12 @@ class Client:
|
|||
self,
|
||||
messages: List[ApiMessage],
|
||||
state: Optional[Dict[str, Any]] = None,
|
||||
skip_tool_calls: bool = False,
|
||||
max_turns: int = 3,
|
||||
workflow_id: Optional[str] = None,
|
||||
test_profile_id: Optional[str] = None
|
||||
) -> ApiResponse:
|
||||
request = ApiRequest(
|
||||
messages=messages,
|
||||
state=state,
|
||||
skipToolCalls=skip_tool_calls,
|
||||
maxTurns=max_turns,
|
||||
workflowId=workflow_id,
|
||||
testProfileId=test_profile_id
|
||||
)
|
||||
|
|
@ -55,86 +47,27 @@ class Client:
|
|||
|
||||
return response_data
|
||||
|
||||
def _process_tool_calls(
|
||||
self,
|
||||
tool_calls: List[Any],
|
||||
tools: Dict[str, Callable[..., str]]
|
||||
) -> List[ToolMessage]:
|
||||
"""Process tool calls and return a list of tool response messages"""
|
||||
tool_messages = []
|
||||
for tool_call in tool_calls:
|
||||
tool_name = tool_call.function.name
|
||||
tool_arguments = json.loads(tool_call.function.arguments)
|
||||
|
||||
if tool_name not in tools:
|
||||
raise ValueError(f'Missing tool: {tool_name}')
|
||||
|
||||
tool_response = tools[tool_name](**tool_arguments)
|
||||
tool_msg = ToolMessage(
|
||||
role='tool',
|
||||
content=tool_response,
|
||||
tool_call_id=tool_call.id,
|
||||
tool_name=tool_name
|
||||
)
|
||||
tool_messages.append(tool_msg)
|
||||
return tool_messages
|
||||
|
||||
def chat(
|
||||
self,
|
||||
messages: List[ApiMessage],
|
||||
tools: Optional[Dict[str, Callable[..., str]]] = None,
|
||||
state: Optional[Dict[str, Any]] = None,
|
||||
max_turns: int = 3,
|
||||
skip_tool_calls: bool = False,
|
||||
workflow_id: Optional[str] = None,
|
||||
test_profile_id: Optional[str] = None
|
||||
) -> Tuple[List[ApiMessage], Optional[Dict[str, Any]]]:
|
||||
"""Stateless chat method that handles a single conversation turn with multiple tool call rounds"""
|
||||
) -> ApiResponse:
|
||||
"""Stateless chat method that handles a single conversation turn"""
|
||||
|
||||
current_messages = messages[:]
|
||||
current_state = state
|
||||
turns = 0
|
||||
# call api
|
||||
response_data = self._call_api(
|
||||
messages=messages,
|
||||
state=state,
|
||||
workflow_id=workflow_id,
|
||||
test_profile_id=test_profile_id
|
||||
)
|
||||
|
||||
response_messages = []
|
||||
response_state = None
|
||||
has_tool_calls = False
|
||||
|
||||
while turns < max_turns:
|
||||
# call api
|
||||
response_data = self._call_api(
|
||||
messages=current_messages,
|
||||
state=current_state,
|
||||
skip_tool_calls=skip_tool_calls,
|
||||
max_turns=max_turns,
|
||||
workflow_id=workflow_id,
|
||||
test_profile_id=test_profile_id
|
||||
)
|
||||
|
||||
current_messages.extend(response_data.messages)
|
||||
current_state = response_data.state
|
||||
response_messages = response_data.messages
|
||||
response_state = response_data.state
|
||||
|
||||
# Process tool calls if present and tools are provided
|
||||
last_message = response_data.messages[-1]
|
||||
has_tool_calls = hasattr(last_message, 'tool_calls') and last_message.tool_calls
|
||||
if has_tool_calls:
|
||||
tool_messages = self._process_tool_calls(last_message.tool_calls, tools)
|
||||
current_messages.extend(tool_messages)
|
||||
|
||||
# If no tool calls were made, we're done
|
||||
if not has_tool_calls:
|
||||
break
|
||||
|
||||
turns += 1
|
||||
|
||||
if turns == max_turns and has_tool_calls:
|
||||
raise ValueError("Max turns reached")
|
||||
|
||||
if not last_message.agenticResponseType == 'external':
|
||||
if not response_data.messages[-1].agenticResponseType == 'external':
|
||||
raise ValueError("Last message was not an external message")
|
||||
|
||||
return response_messages, response_state
|
||||
return response_data
|
||||
|
||||
class StatefulChat:
|
||||
"""Maintains conversation state across multiple turns"""
|
||||
|
|
@ -142,23 +75,14 @@ class StatefulChat:
|
|||
def __init__(
|
||||
self,
|
||||
client: Client,
|
||||
tools: Optional[Dict[str, Callable[..., str]]] = None,
|
||||
system_prompt: Optional[str] = None,
|
||||
max_turns: int = 3,
|
||||
skip_tool_calls: bool = False,
|
||||
workflow_id: Optional[str] = None,
|
||||
test_profile_id: Optional[str] = None
|
||||
) -> None:
|
||||
self.client = client
|
||||
self.tools = tools
|
||||
self.messages: List[ApiMessage] = []
|
||||
self.state: Optional[Dict[str, Any]] = None
|
||||
self.max_turns = max_turns
|
||||
self.skip_tool_calls = skip_tool_calls
|
||||
self.workflow_id = workflow_id
|
||||
self.test_profile_id = test_profile_id
|
||||
if system_prompt:
|
||||
self.messages.append(SystemMessage(role='system', content=system_prompt))
|
||||
|
||||
def run(self, message: Union[str]) -> str:
|
||||
"""Handle a single user turn in the conversation"""
|
||||
|
|
@ -168,22 +92,19 @@ class StatefulChat:
|
|||
self.messages.append(user_msg)
|
||||
|
||||
# Get response using the client's chat method
|
||||
new_messages, new_state = self.client.chat(
|
||||
response_data = self.client.chat(
|
||||
messages=self.messages,
|
||||
tools=self.tools,
|
||||
state=self.state,
|
||||
max_turns=self.max_turns,
|
||||
skip_tool_calls=self.skip_tool_calls,
|
||||
workflow_id=self.workflow_id,
|
||||
test_profile_id=self.test_profile_id
|
||||
)
|
||||
|
||||
# Update internal state
|
||||
self.messages = new_messages
|
||||
self.state = new_state
|
||||
self.messages = response_data.messages
|
||||
self.state = response_data.state
|
||||
|
||||
# Return only the final message content
|
||||
last_message = new_messages[-1]
|
||||
last_message = self.messages[-1]
|
||||
return last_message.content
|
||||
|
||||
|
||||
|
|
@ -197,9 +118,13 @@ if __name__ == "__main__":
|
|||
api_key: str = "<API_KEY>"
|
||||
client = Client(host, project_id, api_key)
|
||||
|
||||
tools: Dict[str, Callable[..., str]] = {
|
||||
'weather_lookup': weather_lookup_tool
|
||||
}
|
||||
chat_session = StatefulChat(client, tools)
|
||||
resp = chat_session.run("whats the weather in london?")
|
||||
result = client.chat(
|
||||
messages=[
|
||||
UserMessage(role='user', content="Hello")
|
||||
]
|
||||
)
|
||||
print(result.messages[-1].content)
|
||||
|
||||
chat_session = StatefulChat(client)
|
||||
resp = chat_session.run("Hello")
|
||||
print(resp)
|
||||
|
|
@ -48,8 +48,6 @@ ApiMessage = Union[
|
|||
class ApiRequest(BaseModel):
|
||||
messages: List[ApiMessage]
|
||||
state: Any
|
||||
skipToolCalls: Optional[bool] = None
|
||||
maxTurns: Optional[int] = None
|
||||
workflowId: Optional[str] = None
|
||||
testProfileId: Optional[str] = None
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,18 @@
|
|||
import { redisClient } from "@/app/lib/redis";
|
||||
|
||||
export async function GET(request: Request, { params }: { params: { streamId: string } }) {
|
||||
// Replace with your actual upstream SSE endpoint.
|
||||
const upstreamUrl = `${process.env.AGENTS_API_URL}/chat_stream/${params.streamId}`;
|
||||
console.log('upstreamUrl', upstreamUrl);
|
||||
// get the payload from redis
|
||||
const payload = await redisClient.get(`chat-stream-${params.streamId}`);
|
||||
if (!payload) {
|
||||
return new Response("Stream not found", { status: 404 });
|
||||
}
|
||||
|
||||
// Fetch the upstream SSE stream.
|
||||
const upstreamResponse = await fetch(upstreamUrl, {
|
||||
const upstreamResponse = await fetch(`${process.env.AGENTS_API_URL}/chat_stream`, {
|
||||
method: 'POST',
|
||||
body: payload,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${process.env.AGENTS_API_KEY || 'test'}`,
|
||||
},
|
||||
cache: 'no-store',
|
||||
|
|
|
|||
|
|
@ -3,3 +3,8 @@ export const USE_RAG_UPLOADS = process.env.USE_RAG_UPLOADS === 'true';
|
|||
export const USE_RAG_SCRAPING = process.env.USE_RAG_SCRAPING === 'true';
|
||||
export const USE_CHAT_WIDGET = process.env.USE_CHAT_WIDGET === 'true';
|
||||
export const USE_AUTH = process.env.USE_AUTH === 'true';
|
||||
|
||||
// Hardcoded flags
|
||||
export const USE_MULTIPLE_PROJECTS = true;
|
||||
export const USE_TESTING_FEATURE = false;
|
||||
export const USE_VOICE_FEATURE = false;
|
||||
|
|
@ -111,8 +111,6 @@ export const ApiMessage = z.union([
|
|||
export const ApiRequest = z.object({
|
||||
messages: z.array(ApiMessage),
|
||||
state: z.unknown(),
|
||||
skipToolCalls: z.boolean().nullable().optional(),
|
||||
maxTurns: z.number().nullable().optional(),
|
||||
workflowId: z.string().nullable().optional(),
|
||||
testProfileId: z.string().nullable().optional(),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { z } from "zod";
|
|||
import { generateObject } from "ai";
|
||||
import { ApiMessage } from "./types/types";
|
||||
import { openai } from "@ai-sdk/openai";
|
||||
import { redisClient } from "./redis";
|
||||
|
||||
export async function getAgenticApiResponse(
|
||||
request: z.infer<typeof AgenticAPIChatRequest>,
|
||||
|
|
@ -38,24 +39,20 @@ export async function getAgenticApiResponse(
|
|||
export async function getAgenticResponseStreamId(
|
||||
request: z.infer<typeof AgenticAPIChatRequest>,
|
||||
): Promise<z.infer<typeof AgenticAPIInitStreamResponse>> {
|
||||
// call agentic api
|
||||
console.log(`sending agentic api init stream request`, JSON.stringify(request));
|
||||
const response = await fetch(process.env.AGENTS_API_URL + '/chat_stream_init', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(request),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${process.env.AGENTS_API_KEY || 'test'}`,
|
||||
},
|
||||
// serialize the request
|
||||
const payload = JSON.stringify(request);
|
||||
|
||||
// create a uuid for the stream
|
||||
const streamId = crypto.randomUUID();
|
||||
|
||||
// store payload in redis
|
||||
await redisClient.set(`chat-stream-${streamId}`, payload, {
|
||||
EX: 60 * 10, // expire in 10 minutes
|
||||
});
|
||||
if (!response.ok) {
|
||||
console.error('Failed to call agentic init stream api', response);
|
||||
throw new Error(`Failed to call agentic init stream api: ${response.statusText}`);
|
||||
}
|
||||
const responseJson = await response.json();
|
||||
console.log(`received agentic api init stream response`, JSON.stringify(responseJson));
|
||||
const result: z.infer<typeof AgenticAPIInitStreamResponse> = responseJson;
|
||||
return result;
|
||||
|
||||
return {
|
||||
streamId,
|
||||
};
|
||||
}
|
||||
|
||||
// create a PrefixLogger class that wraps console.log with a prefix
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import { ToolsSection } from './components/tools';
|
|||
import { Panel } from "@/components/common/panel-common";
|
||||
import { Settings, Wrench, Phone } from "lucide-react";
|
||||
import { clsx } from "clsx";
|
||||
import { USE_VOICE_FEATURE } from "@/app/lib/feature_flags";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Project config",
|
||||
|
|
@ -799,7 +800,7 @@ function NavigationMenu({
|
|||
const items = [
|
||||
{ id: 'Project', icon: <Settings className="w-4 h-4" /> },
|
||||
{ id: 'Tools', icon: <Wrench className="w-4 h-4" /> },
|
||||
{ id: 'Voice', icon: <Phone className="w-4 h-4" /> }
|
||||
...(USE_VOICE_FEATURE ? [{ id: 'Voice', icon: <Phone className="w-4 h-4" /> }] : [])
|
||||
];
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import { TestProfile } from "@/app/lib/types/testing_types";
|
|||
import { WithStringId } from "@/app/lib/types/types";
|
||||
import { ProfileSelector } from "@/app/projects/[projectId]/test/[[...slug]]/components/selectors/profile-selector";
|
||||
import { CheckIcon, CopyIcon, PlusIcon, UserIcon } from "lucide-react";
|
||||
import { USE_TESTING_FEATURE } from "@/app/lib/feature_flags";
|
||||
|
||||
const defaultSystemMessage = '';
|
||||
|
||||
|
|
@ -103,15 +104,17 @@ export function App({
|
|||
}
|
||||
rightActions={
|
||||
<div className="flex items-center gap-3">
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() => setIsProfileSelectorOpen(true)}
|
||||
showHoverContent={true}
|
||||
hoverContent={testProfile?.name || 'Select test profile'}
|
||||
>
|
||||
<UserIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
{USE_TESTING_FEATURE && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() => setIsProfileSelectorOpen(true)}
|
||||
showHoverContent={true}
|
||||
hoverContent={testProfile?.name || 'Select test profile'}
|
||||
>
|
||||
<UserIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import { apiV1 } from "rowboat-shared";
|
|||
import { TestProfile } from "@/app/lib/types/testing_types";
|
||||
import { WithStringId } from "@/app/lib/types/types";
|
||||
import { ProfileContextBox } from "./profile-context-box";
|
||||
import { USE_TESTING_FEATURE } from "@/app/lib/feature_flags";
|
||||
|
||||
export function Chat({
|
||||
chat,
|
||||
|
|
@ -225,11 +226,13 @@ export function Chat({
|
|||
|
||||
return <div className="relative max-w-3xl mx-auto h-full flex flex-col">
|
||||
<div className="sticky top-0 z-10 bg-white dark:bg-zinc-900 pt-4 pb-4">
|
||||
<ProfileContextBox
|
||||
content={testProfile?.context || systemMessage || ''}
|
||||
onChange={onSystemMessageChange}
|
||||
locked={testProfile !== null}
|
||||
/>
|
||||
{USE_TESTING_FEATURE && (
|
||||
<ProfileContextBox
|
||||
content={testProfile?.context || systemMessage || ''}
|
||||
onChange={onSystemMessageChange}
|
||||
locked={testProfile !== null}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-auto pr-1
|
||||
|
|
|
|||
|
|
@ -535,6 +535,7 @@ function reducer(state: State, action: Action): State {
|
|||
draft.workflow.tools.push(newTool);
|
||||
}
|
||||
});
|
||||
draft.pendingChanges = true;
|
||||
draft.chatKey++;
|
||||
break;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import {
|
|||
} from "lucide-react";
|
||||
import { getProjectConfig } from "@/app/actions/project_actions";
|
||||
import { useTheme } from "@/app/providers/theme-provider";
|
||||
import { USE_TESTING_FEATURE } from '@/app/lib/feature_flags';
|
||||
|
||||
interface SidebarProps {
|
||||
projectId: string;
|
||||
|
|
@ -56,12 +57,12 @@ export default function Sidebar({ projectId, useRag, useAuth, collapsed = false,
|
|||
icon: WorkflowIcon,
|
||||
requiresProject: true
|
||||
},
|
||||
{
|
||||
...(USE_TESTING_FEATURE ? [{
|
||||
href: 'test',
|
||||
label: 'Test',
|
||||
icon: PlayIcon,
|
||||
requiresProject: true
|
||||
},
|
||||
}] : []),
|
||||
...(useRag ? [{
|
||||
href: 'sources',
|
||||
label: 'RAG',
|
||||
|
|
|
|||
|
|
@ -14,11 +14,18 @@ import { SearchProjects } from "./components/search-projects";
|
|||
import { CustomPromptCard } from "./components/custom-prompt-card";
|
||||
import { Submit } from "./components/submit-button";
|
||||
import { PageHeading } from "@/components/ui/page-heading";
|
||||
import { USE_MULTIPLE_PROJECTS } from "@/app/lib/feature_flags";
|
||||
|
||||
const sectionHeaderStyles = clsx(
|
||||
"text-sm font-medium",
|
||||
"text-gray-900 dark:text-gray-100"
|
||||
);
|
||||
|
||||
const largeSectionHeaderStyles = clsx(
|
||||
"text-lg font-medium",
|
||||
"text-gray-900 dark:text-gray-100"
|
||||
);
|
||||
|
||||
const textareaStyles = clsx(
|
||||
"w-full",
|
||||
"rounded-lg p-3",
|
||||
|
|
@ -34,12 +41,12 @@ export default function App() {
|
|||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
const [selectedCard, setSelectedCard] = useState<'custom' | any>('custom');
|
||||
const [customPrompt, setCustomPrompt] = useState("Create a customer support assistant with one example agent");
|
||||
const [customPrompt, setCustomPrompt] = useState("");
|
||||
const [name, setName] = useState("");
|
||||
const [defaultName, setDefaultName] = useState('Assistant 1');
|
||||
const [isExamplesExpanded, setIsExamplesExpanded] = useState(false);
|
||||
const [selectedTemplate, setSelectedTemplate] = useState<string>('blank');
|
||||
const [showCustomPrompt, setShowCustomPrompt] = useState(false);
|
||||
const [selectedTemplate, setSelectedTemplate] = useState<string>('custom');
|
||||
const [showCustomPrompt, setShowCustomPrompt] = useState(true);
|
||||
const [promptError, setPromptError] = useState<string | null>(null);
|
||||
const [hasEditedPrompt, setHasEditedPrompt] = useState(false);
|
||||
|
||||
|
|
@ -88,7 +95,7 @@ export default function App() {
|
|||
setSelectedCard(card);
|
||||
|
||||
if (card === 'custom') {
|
||||
setCustomPrompt("Create a customer support assistant with one example agent");
|
||||
setCustomPrompt("");
|
||||
} else {
|
||||
setCustomPrompt(card.prompt || card.description);
|
||||
}
|
||||
|
|
@ -178,64 +185,121 @@ export default function App() {
|
|||
"flex-1 px-12 pt-4 pb-32"
|
||||
)}>
|
||||
<PageHeading
|
||||
title="Projects"
|
||||
description="Select an existing project or create a new one"
|
||||
title={USE_MULTIPLE_PROJECTS ? "Projects" : "Let's get started"}
|
||||
description={USE_MULTIPLE_PROJECTS
|
||||
? "Select an existing project or create a new one"
|
||||
: "Create a multi-agent assistant in minutes"
|
||||
}
|
||||
/>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-[1fr,2fr] gap-8 mt-8">
|
||||
<div className={clsx(
|
||||
USE_MULTIPLE_PROJECTS
|
||||
? "grid grid-cols-1 lg:grid-cols-[1fr,2fr] gap-8 mt-8"
|
||||
: "mt-8 -mx-12"
|
||||
)}>
|
||||
{/* Left side: Project Selection */}
|
||||
<div className="overflow-auto">
|
||||
<SearchProjects
|
||||
projects={projects}
|
||||
isLoading={isLoading}
|
||||
heading="Select an existing project"
|
||||
subheading="Choose from your projects"
|
||||
className="h-full"
|
||||
/>
|
||||
</div>
|
||||
{USE_MULTIPLE_PROJECTS && (
|
||||
<div className="overflow-auto">
|
||||
<SearchProjects
|
||||
projects={projects}
|
||||
isLoading={isLoading}
|
||||
heading="Select an existing project"
|
||||
subheading="Choose from your projects"
|
||||
className="h-full"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Right side: Project Creation */}
|
||||
<div className="overflow-auto">
|
||||
<section className="card h-full">
|
||||
<div className="px-4 pt-4">
|
||||
<SectionHeading subheading="Set up a new AI assistant">
|
||||
Create a new project
|
||||
</SectionHeading>
|
||||
</div>
|
||||
<div className={clsx(
|
||||
"overflow-auto",
|
||||
!USE_MULTIPLE_PROJECTS && "max-w-none px-12 py-12"
|
||||
)}>
|
||||
<section className={clsx(
|
||||
"card h-full",
|
||||
!USE_MULTIPLE_PROJECTS && "px-24",
|
||||
USE_MULTIPLE_PROJECTS && "px-8"
|
||||
)}>
|
||||
{USE_MULTIPLE_PROJECTS && (
|
||||
<div className="pt-12">
|
||||
<SectionHeading subheading="Set up a new AI assistant">
|
||||
Create a new project
|
||||
</SectionHeading>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form
|
||||
id="create-project-form"
|
||||
action={handleSubmit}
|
||||
onKeyDown={handleKeyDown}
|
||||
className="px-4 pt-4 pb-8 space-y-8"
|
||||
className="pt-12 pb-16 space-y-12"
|
||||
>
|
||||
{/* Name Section */}
|
||||
<div className="space-y-4">
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className={sectionHeaderStyles}>
|
||||
Name
|
||||
</label>
|
||||
<Textarea
|
||||
required
|
||||
name="name"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
className={clsx(
|
||||
textareaStyles,
|
||||
"min-h-[60px]",
|
||||
"text-base",
|
||||
"text-gray-900 dark:text-gray-100"
|
||||
)}
|
||||
placeholder={defaultName}
|
||||
/>
|
||||
{USE_MULTIPLE_PROJECTS && (
|
||||
<div className="space-y-4">
|
||||
<div className="flex flex-col gap-4">
|
||||
<label className={largeSectionHeaderStyles}>
|
||||
Name
|
||||
</label>
|
||||
<Textarea
|
||||
required
|
||||
name="name"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
className={clsx(
|
||||
textareaStyles,
|
||||
"min-h-[60px]",
|
||||
"text-base",
|
||||
"text-gray-900 dark:text-gray-100"
|
||||
)}
|
||||
placeholder={defaultName}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Custom Prompt Section - Only show when needed */}
|
||||
{showCustomPrompt && (
|
||||
<div className="space-y-4">
|
||||
<div className="flex flex-col gap-4">
|
||||
<label className={largeSectionHeaderStyles}>
|
||||
{selectedTemplate === 'custom' ? 'What do you want to build?' : 'Customize the description'}
|
||||
</label>
|
||||
<div className="space-y-2">
|
||||
<Textarea
|
||||
value={customPrompt}
|
||||
onChange={(e) => {
|
||||
setCustomPrompt(e.target.value);
|
||||
setPromptError(null);
|
||||
}}
|
||||
placeholder="Example: Create a customer support assistant that can handle product inquiries and returns"
|
||||
className={clsx(
|
||||
textareaStyles,
|
||||
"text-base",
|
||||
"text-gray-900 dark:text-gray-100",
|
||||
promptError && "border-red-500 focus:ring-red-500/20"
|
||||
)}
|
||||
style={{ minHeight: "120px" }}
|
||||
autoFocus
|
||||
autoResize
|
||||
required
|
||||
/>
|
||||
{promptError && (
|
||||
<p className="text-sm text-red-500">
|
||||
{promptError}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
{/* Template Selection Section */}
|
||||
<div className="space-y-4">
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className={sectionHeaderStyles}>
|
||||
Choose how to start
|
||||
<div className="flex flex-col gap-4">
|
||||
<label className={largeSectionHeaderStyles}>
|
||||
How do you want to start?
|
||||
</label>
|
||||
<select
|
||||
value={selectedTemplate}
|
||||
|
|
@ -259,9 +323,9 @@ export default function App() {
|
|||
"dark:bg-[url('data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22%23ffffff%22%20stroke-width%3D%222%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Cpolyline%20points%3D%226%209%2012%2015%2018%209%22%3E%3C%2Fpolyline%3E%3C%2Fsvg%3E')]"
|
||||
)}
|
||||
>
|
||||
<option value="blank">Start with a blank template</option>
|
||||
<option value="custom">Write your own starting prompt</option>
|
||||
<optgroup label="Example Prompts">
|
||||
<option value="custom">Tell us what you want to build</option>
|
||||
<option value="blank">I'll provide a description later</option>
|
||||
<optgroup label="Customizable Examples">
|
||||
{starting_copilot_prompts &&
|
||||
Object.entries(starting_copilot_prompts)
|
||||
.filter(([name]) => name !== 'Blank Template')
|
||||
|
|
@ -276,40 +340,6 @@ export default function App() {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{/* Custom Prompt Section - Only show when needed */}
|
||||
{showCustomPrompt && (
|
||||
<div className="space-y-4">
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className={sectionHeaderStyles}>
|
||||
{selectedTemplate === 'custom' ? 'Write your prompt' : 'Customize the prompt'}
|
||||
</label>
|
||||
<div className="space-y-2">
|
||||
<Textarea
|
||||
value={customPrompt}
|
||||
onChange={(e) => {
|
||||
setCustomPrompt(e.target.value);
|
||||
setPromptError(null);
|
||||
}}
|
||||
placeholder="Example: Create a customer support assistant that can handle product inquiries and returns"
|
||||
className={clsx(
|
||||
textareaStyles,
|
||||
"min-h-[100px]",
|
||||
"text-base",
|
||||
"text-gray-900 dark:text-gray-100",
|
||||
promptError && "border-red-500 focus:ring-red-500/20"
|
||||
)}
|
||||
autoResize
|
||||
required
|
||||
/>
|
||||
{promptError && (
|
||||
<p className="text-sm text-red-500">
|
||||
{promptError}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Submit Button */}
|
||||
<div className="pt-6 w-full">
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ export function Submit() {
|
|||
isLoading={pending}
|
||||
startContent={<PlusIcon size={16} />}
|
||||
>
|
||||
Create project
|
||||
Create assistant
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -35,6 +35,11 @@ export const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(({
|
|||
const [validationError, setValidationError] = useState<string | undefined>();
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
|
||||
// update local value when prop value changes
|
||||
useEffect(() => {
|
||||
setLocalValue(propValue as string);
|
||||
}, [propValue]);
|
||||
|
||||
// Sync local state with prop value when not editing
|
||||
useEffect(() => {
|
||||
if (!isEditing) {
|
||||
|
|
|
|||
15
apps/rowboat/package-lock.json
generated
|
|
@ -10275,16 +10275,17 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@modelcontextprotocol/sdk": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.7.0.tgz",
|
||||
"integrity": "sha512-IYPe/FLpvF3IZrd/f5p5ffmWhMc3aEMuM2wGJASDqC2Ge7qatVCdbfPx3n/5xFeb19xN0j/911M2AaFuircsWA==",
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.9.0.tgz",
|
||||
"integrity": "sha512-Jq2EUCQpe0iyO5FGpzVYDNFR6oR53AIrwph9yWl7uSc7IWUMsrmpmSaTGra5hQNunXpM+9oit85p924jWuHzUA==",
|
||||
"dependencies": {
|
||||
"content-type": "^1.0.5",
|
||||
"cors": "^2.8.5",
|
||||
"cross-spawn": "^7.0.3",
|
||||
"eventsource": "^3.0.2",
|
||||
"express": "^5.0.1",
|
||||
"express-rate-limit": "^7.5.0",
|
||||
"pkce-challenge": "^4.1.0",
|
||||
"pkce-challenge": "^5.0.0",
|
||||
"raw-body": "^3.0.0",
|
||||
"zod": "^3.23.8",
|
||||
"zod-to-json-schema": "^3.24.1"
|
||||
|
|
@ -20112,9 +20113,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/pkce-challenge": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-4.1.0.tgz",
|
||||
"integrity": "sha512-ZBmhE1C9LcPoH9XZSdwiPtbPHZROwAnMy+kIFQVrnMCxY4Cudlz3gBOpzilgc0jOgRaiT3sIWfpMomW2ar2orQ==",
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz",
|
||||
"integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==",
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
|
|
|
|||
237
apps/rowboat_agents/poetry.lock
generated
|
|
@ -83,7 +83,6 @@ pytz = "^2024.2"
|
|||
qdrant-client = "*"
|
||||
Quart = "^0.20.0"
|
||||
RapidFuzz = "^3.11.0"
|
||||
redis = "^5.2.1"
|
||||
requests = "^2.32.3"
|
||||
requests-toolbelt = "^1.0.0"
|
||||
setuptools = "^75.8.0"
|
||||
|
|
|
|||
|
|
@ -1,7 +1,11 @@
|
|||
aiohttp==3.9.3
|
||||
aiofiles==24.1.0
|
||||
aiohappyeyeballs==2.6.1
|
||||
aiohttp==3.11.14
|
||||
aiosignal==1.3.2
|
||||
annotated-types==0.7.0
|
||||
anyio==4.8.0
|
||||
asgiref
|
||||
asgiref==3.8.1
|
||||
attrs==25.3.0
|
||||
beautifulsoup4==4.12.3
|
||||
blinker==1.9.0
|
||||
build==1.2.2.post1
|
||||
|
|
@ -11,22 +15,32 @@ cffi==1.17.1
|
|||
charset-normalizer==3.4.1
|
||||
cleo==2.1.0
|
||||
click==8.1.8
|
||||
colorama==0.4.6
|
||||
crashtest==0.4.1
|
||||
distlib==0.3.9
|
||||
distro==1.9.0
|
||||
dnspython==2.7.0
|
||||
dulwich==0.22.7
|
||||
dulwich==0.22.8
|
||||
et_xmlfile==2.0.0
|
||||
eval_type_backport==0.2.2
|
||||
fastjsonschema==2.21.1
|
||||
filelock==3.17.0
|
||||
filelock==3.18.0
|
||||
findpython==0.6.3
|
||||
firecrawl==1.9.0
|
||||
Flask==3.1.0
|
||||
frozenlist==1.5.0
|
||||
griffe==1.6.2
|
||||
grpcio==1.71.0
|
||||
grpcio-tools==1.71.0
|
||||
gunicorn==23.0.0
|
||||
h11==0.14.0
|
||||
h2==4.2.0
|
||||
hpack==4.1.0
|
||||
httpcore==1.0.7
|
||||
httpx==0.27.2
|
||||
hypercorn
|
||||
httpx-sse==0.4.0
|
||||
Hypercorn==0.17.3
|
||||
hyperframe==6.1.0
|
||||
idna==3.10
|
||||
installer==0.7.0
|
||||
itsdangerous==2.2.0
|
||||
|
|
@ -40,24 +54,31 @@ keyring==25.6.0
|
|||
lxml==5.3.0
|
||||
markdownify==0.13.1
|
||||
MarkupSafe==3.0.2
|
||||
mcp
|
||||
mcp==1.5.0
|
||||
more-itertools==10.6.0
|
||||
motor
|
||||
motor==3.7.0
|
||||
msgpack==1.1.0
|
||||
multidict==6.2.0
|
||||
mypy-extensions==1.0.0
|
||||
nest-asyncio==1.6.0
|
||||
numpy==2.2.1
|
||||
openai
|
||||
openai-agents
|
||||
openai==1.68.0
|
||||
openai-agents==0.0.4
|
||||
openpyxl==3.1.5
|
||||
packaging==24.2
|
||||
pandas==2.2.3
|
||||
pkginfo==1.12.0
|
||||
platformdirs==4.3.6
|
||||
poetry==2.0.1
|
||||
poetry-core==2.0.1
|
||||
pbs-installer==2025.3.17
|
||||
pkginfo==1.12.1.2
|
||||
platformdirs==4.3.7
|
||||
poetry==2.1.1
|
||||
poetry-core==2.1.1
|
||||
portalocker==2.10.1
|
||||
priority==2.0.0
|
||||
propcache==0.3.0
|
||||
protobuf==5.29.4
|
||||
pycparser==2.22
|
||||
pydantic==2.10.5
|
||||
pydantic-settings==2.8.1
|
||||
pydantic_core==2.27.2
|
||||
PyJWT==2.10.1
|
||||
pymongo==4.10.1
|
||||
|
|
@ -66,28 +87,35 @@ python-dateutil==2.9.0.post0
|
|||
python-docx==1.1.2
|
||||
python-dotenv==1.0.1
|
||||
pytz==2024.2
|
||||
qdrant-client
|
||||
qdrant-client==1.13.3
|
||||
Quart==0.20.0
|
||||
RapidFuzz==3.11.0
|
||||
redis==5.2.1
|
||||
RapidFuzz==3.12.2
|
||||
requests==2.32.3
|
||||
requests-toolbelt==1.0.0
|
||||
setuptools==75.8.0
|
||||
shellingham==1.5.4
|
||||
six==1.17.0
|
||||
sniffio==1.3.1
|
||||
sounddevice==0.5.1
|
||||
soupsieve==2.6
|
||||
sse-starlette==2.2.1
|
||||
starlette==0.46.1
|
||||
tabulate==0.9.0
|
||||
tomlkit==0.13.2
|
||||
tqdm==4.67.1
|
||||
trove-classifiers==2025.1.15.22
|
||||
trove-classifiers==2025.3.19.19
|
||||
types-requests==2.32.0.20250306
|
||||
typing-inspect==0.9.0
|
||||
typing_extensions==4.12.2
|
||||
tzdata==2024.2
|
||||
urllib3==2.3.0
|
||||
virtualenv==20.29.1
|
||||
uvicorn==0.34.0
|
||||
virtualenv==20.29.3
|
||||
waitress==2.1.2
|
||||
websockets==13.1
|
||||
Werkzeug==3.1.3
|
||||
wheel==0.44.0
|
||||
wsproto==1.2.0
|
||||
xattr==1.1.4
|
||||
yarl==1.18.3
|
||||
zstandard==0.23.0
|
||||
|
|
|
|||
|
|
@ -3,8 +3,6 @@ from quart import Quart, request, jsonify, Response
|
|||
from datetime import datetime
|
||||
from functools import wraps
|
||||
import os
|
||||
import redis
|
||||
import uuid
|
||||
import json
|
||||
from hypercorn.config import Config
|
||||
from hypercorn.asyncio import serve
|
||||
|
|
@ -17,7 +15,6 @@ from src.utils.common import common_logger, read_json_from_file
|
|||
from pprint import pprint
|
||||
|
||||
logger = common_logger
|
||||
redis_client = redis.from_url(os.environ.get('REDIS_URL', 'redis://localhost:6379'))
|
||||
app = Quart(__name__)
|
||||
config = read_json_from_file("./configs/default_config.json")
|
||||
|
||||
|
|
@ -122,31 +119,17 @@ async def chat():
|
|||
logger.error(f"Error: {str(e)}")
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@app.route("/chat_stream_init", methods=["POST"])
|
||||
@require_api_key
|
||||
async def chat_stream_init():
|
||||
# create a uuid for the stream
|
||||
stream_id = str(uuid.uuid4())
|
||||
|
||||
# store the request data in redis with 10 minute TTL
|
||||
data = await request.get_json()
|
||||
redis_client.setex(f"stream_request_{stream_id}", 600, json.dumps(data))
|
||||
|
||||
return jsonify({"streamId": stream_id})
|
||||
|
||||
def format_sse(data: dict, event: str = None) -> str:
|
||||
msg = f"data: {json.dumps(data)}\n\n"
|
||||
if event is not None:
|
||||
msg = f"event: {event}\n{msg}"
|
||||
return msg
|
||||
|
||||
@app.route("/chat_stream/<stream_id>", methods=["GET"])
|
||||
@app.route("/chat_stream", methods=["POST"])
|
||||
@require_api_key
|
||||
async def chat_stream(stream_id):
|
||||
# get the request data from redis
|
||||
request_data = redis_client.get(f"stream_request_{stream_id}")
|
||||
if not request_data:
|
||||
return jsonify({"error": "Stream not found"}), 404
|
||||
async def chat_stream():
|
||||
# get the request data from the request
|
||||
request_data = await request.get_data()
|
||||
|
||||
print("Request:", request_data.decode('utf-8'))
|
||||
request_data = json.loads(request_data)
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ def create_final_response(response, turn_messages, tokens_used, all_agents):
|
|||
|
||||
# Ensure tokens_used is a valid dictionary
|
||||
if not isinstance(tokens_used, dict):
|
||||
tokens_used = {"total": 100, "prompt": 50, "completion": 50} # Default values if not a dictionary
|
||||
tokens_used = {"total": 0, "prompt": 0, "completion": 0} # Default values if not a dictionary
|
||||
|
||||
# Ensure response has a tokens_used attribute that's a dictionary
|
||||
if not hasattr(response, 'tokens_used') or not isinstance(response.tokens_used, dict):
|
||||
|
|
@ -187,8 +187,8 @@ async def run_turn(
|
|||
logger.info("Converted message added to response messages")
|
||||
print("Converted message added to response messages")
|
||||
|
||||
# Use a dictionary for tokens_used instead of a hard-coded integer
|
||||
tokens_used = {"total": 100, "prompt": 50, "completion": 50} # Dummy values as placeholders
|
||||
# Use a dictionary for tokens_used
|
||||
tokens_used = {"total": 0, "prompt": 0, "completion": 0} # Default values as placeholders
|
||||
|
||||
# Ensure turn_messages can be extended with response.messages
|
||||
if hasattr(response, 'messages') and isinstance(response.messages, list):
|
||||
|
|
@ -208,7 +208,7 @@ async def run_turn(
|
|||
|
||||
# Ensure tokens_used remains a proper dictionary
|
||||
if not isinstance(tokens_used, dict):
|
||||
tokens_used = {"total": 100, "prompt": 50, "completion": 50} # Default values if not a dictionary
|
||||
tokens_used = {"total": 0, "prompt": 0, "completion": 0} # Default values if not a dictionary
|
||||
|
||||
response = create_response(
|
||||
messages=[duplicate_msg],
|
||||
|
|
|
|||
|
|
@ -38,62 +38,45 @@ class NewResponse(BaseModel):
|
|||
error_msg: Optional[str] = ""
|
||||
|
||||
async def mock_tool(tool_name: str, args: str, description: str, mock_instructions: str) -> str:
|
||||
"""
|
||||
Handles tool execution by either using mock instructions or generating a response.
|
||||
try:
|
||||
print(f"Mock tool called for: {tool_name}")
|
||||
|
||||
Args:
|
||||
tool_name: The name of the tool
|
||||
args: The arguments passed to the tool
|
||||
tool_config: The configuration of the tool
|
||||
messages = [
|
||||
{"role": "system", "content": f"You are simulating the execution of a tool called '{tool_name}'.Here is the description of the tool: {description}. Here are the instructions for the mock tool: {mock_instructions}. Generate a realistic response as if the tool was actually executed with the given parameters."},
|
||||
{"role": "user", "content": f"Generate a realistic response for the tool '{tool_name}' with these parameters: {args}. The response should be concise and focused on what the tool would actually return."}
|
||||
]
|
||||
|
||||
Returns:
|
||||
The response from the tool
|
||||
"""
|
||||
print(f"Mock tool called for: {tool_name}")
|
||||
|
||||
|
||||
messages = [
|
||||
{"role": "system", "content": f"You are simulating the execution of a tool called '{tool_name}'.Here is the description of the tool: {description}. Here are the instructions for the mock tool: {mock_instructions}. Generate a realistic response as if the tool was actually executed with the given parameters."},
|
||||
{"role": "user", "content": f"Generate a realistic response for the tool '{tool_name}' with these parameters: {args}. The response should be concise and focused on what the tool would actually return."}
|
||||
]
|
||||
|
||||
print(f"Generating simulated response for tool: {tool_name}")
|
||||
response_content = generate_openai_output(messages, output_type='text', model="gpt-4o")
|
||||
return response_content
|
||||
print(f"Generating simulated response for tool: {tool_name}")
|
||||
response_content = generate_openai_output(messages, output_type='text', model="gpt-4o")
|
||||
return response_content
|
||||
except Exception as e:
|
||||
logger.error(f"Error in mock_tool: {str(e)}")
|
||||
return f"Error: {str(e)}"
|
||||
|
||||
async def call_webhook(tool_name: str, args: str, webhook_url: str, signing_secret: str) -> str:
|
||||
"""
|
||||
Calls the webhook with the given tool name and arguments.
|
||||
|
||||
Args:
|
||||
tool_name (str): The name of the tool to call.
|
||||
args (str): The arguments for the tool as a JSON string.
|
||||
|
||||
Returns:
|
||||
str: The response from the webhook, or an error message if the call fails.
|
||||
"""
|
||||
content_dict = {
|
||||
"toolCall": {
|
||||
"function": {
|
||||
"name": tool_name,
|
||||
"arguments": args
|
||||
try:
|
||||
print(f"Calling webhook for tool: {tool_name}")
|
||||
content_dict = {
|
||||
"toolCall": {
|
||||
"function": {
|
||||
"name": tool_name,
|
||||
"arguments": args
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
request_body = {
|
||||
"content": json.dumps(content_dict)
|
||||
}
|
||||
request_body = {
|
||||
"content": json.dumps(content_dict)
|
||||
}
|
||||
|
||||
# Prepare headers
|
||||
headers = {}
|
||||
if signing_secret:
|
||||
content_str = request_body["content"]
|
||||
body_hash = hashlib.sha256(content_str.encode('utf-8')).hexdigest()
|
||||
payload = {"bodyHash": body_hash}
|
||||
signature_jwt = jwt.encode(payload, signing_secret, algorithm="HS256")
|
||||
headers["X-Signature-Jwt"] = signature_jwt
|
||||
# Prepare headers
|
||||
headers = {}
|
||||
if signing_secret:
|
||||
content_str = request_body["content"]
|
||||
body_hash = hashlib.sha256(content_str.encode('utf-8')).hexdigest()
|
||||
payload = {"bodyHash": body_hash}
|
||||
signature_jwt = jwt.encode(payload, signing_secret, algorithm="HS256")
|
||||
headers["X-Signature-Jwt"] = signature_jwt
|
||||
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.post(webhook_url, json=request_body, headers=headers) as response:
|
||||
if response.status == 200:
|
||||
|
|
@ -104,58 +87,60 @@ async def call_webhook(tool_name: str, args: str, webhook_url: str, signing_secr
|
|||
print(f"Webhook error: {error_msg}")
|
||||
return f"Error: {error_msg}"
|
||||
except Exception as e:
|
||||
print(f"Exception in call_webhook: {str(e)}")
|
||||
logger.error(f"Exception in call_webhook: {str(e)}")
|
||||
return f"Error: Failed to call webhook - {str(e)}"
|
||||
|
||||
async def call_mcp(tool_name: str, args: str, mcp_server_url: str) -> str:
|
||||
"""
|
||||
Calls the MCP with the given tool name and arguments.
|
||||
"""
|
||||
try:
|
||||
print(f"MCP tool called for: {tool_name}")
|
||||
async with sse_client(url=mcp_server_url) as streams:
|
||||
async with ClientSession(*streams) as session:
|
||||
await session.initialize()
|
||||
jargs = json.loads(args)
|
||||
response = await session.call_tool(tool_name, arguments=jargs)
|
||||
json_output = json.dumps([item.__dict__ for item in response.content], indent=2)
|
||||
|
||||
async with sse_client(url=mcp_server_url) as streams:
|
||||
async with ClientSession(*streams) as session:
|
||||
await session.initialize()
|
||||
jargs = json.loads(args)
|
||||
response = await session.call_tool(tool_name, arguments=jargs)
|
||||
json_output = json.dumps([item.__dict__ for item in response.content], indent=2)
|
||||
|
||||
return json_output
|
||||
return json_output
|
||||
except Exception as e:
|
||||
logger.error(f"Error in call_mcp: {str(e)}")
|
||||
return f"Error: {str(e)}"
|
||||
|
||||
async def catch_all(ctx: RunContextWrapper[Any], args: str, tool_name: str, tool_config: dict, complete_request: dict) -> str:
|
||||
"""
|
||||
Handles all tool calls by dispatching to appropriate functions.
|
||||
"""
|
||||
print(f"Catch all called for tool: {tool_name}")
|
||||
print(f"Args: {args}")
|
||||
print(f"Tool config: {tool_config}")
|
||||
|
||||
# Create event loop for async operations
|
||||
try:
|
||||
loop = asyncio.get_event_loop()
|
||||
except RuntimeError:
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
print(f"Catch all called for tool: {tool_name}")
|
||||
print(f"Args: {args}")
|
||||
print(f"Tool config: {tool_config}")
|
||||
|
||||
response_content = None
|
||||
if tool_config.get("mockTool", False) or complete_request.get("testProfile", {}).get("mockTools", False):
|
||||
# Call mock_tool to handle the response (it will decide whether to use mock instructions or generate a response)
|
||||
if complete_request.get("testProfile", {}).get("mockPrompt", ""):
|
||||
response_content = await mock_tool(tool_name, args, tool_config.get("description", ""), complete_request.get("testProfile", {}).get("mockPrompt", ""))
|
||||
# Create event loop for async operations
|
||||
try:
|
||||
loop = asyncio.get_event_loop()
|
||||
except RuntimeError:
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
|
||||
response_content = None
|
||||
if tool_config.get("mockTool", False) or complete_request.get("testProfile", {}).get("mockTools", False):
|
||||
# Call mock_tool to handle the response (it will decide whether to use mock instructions or generate a response)
|
||||
if complete_request.get("testProfile", {}).get("mockPrompt", ""):
|
||||
response_content = await mock_tool(tool_name, args, tool_config.get("description", ""), complete_request.get("testProfile", {}).get("mockPrompt", ""))
|
||||
else:
|
||||
response_content = await mock_tool(tool_name, args, tool_config.get("description", ""), tool_config.get("mockInstructions", ""))
|
||||
print(response_content)
|
||||
elif tool_config.get("isMcp", False):
|
||||
mcp_server_name = tool_config.get("mcpServerName", "")
|
||||
mcp_servers = complete_request.get("mcpServers", {})
|
||||
mcp_server_url = next((server.get("url", "") for server in mcp_servers if server.get("name") == mcp_server_name), "")
|
||||
response_content = await call_mcp(tool_name, args, mcp_server_url)
|
||||
else:
|
||||
response_content = await mock_tool(tool_name, args, tool_config.get("description", ""), tool_config.get("mockInstructions", ""))
|
||||
print(response_content)
|
||||
elif tool_config.get("isMcp", False):
|
||||
mcp_server_name = tool_config.get("mcpServerName", "")
|
||||
mcp_servers = complete_request.get("mcpServers", {})
|
||||
mcp_server_url = next((server.get("url", "") for server in mcp_servers if server.get("name") == mcp_server_name), "")
|
||||
response_content = await call_mcp(tool_name, args, mcp_server_url)
|
||||
else:
|
||||
collection = db["projects"]
|
||||
doc = collection.find_one({"_id": complete_request.get("projectId", "")})
|
||||
signing_secret = doc.get("secret", "")
|
||||
webhook_url = complete_request.get("toolWebhookUrl", "")
|
||||
response_content = await call_webhook(tool_name, args, webhook_url, signing_secret)
|
||||
return response_content
|
||||
collection = db["projects"]
|
||||
doc = collection.find_one({"_id": complete_request.get("projectId", "")})
|
||||
signing_secret = doc.get("secret", "")
|
||||
webhook_url = complete_request.get("toolWebhookUrl", "")
|
||||
response_content = await call_webhook(tool_name, args, webhook_url, signing_secret)
|
||||
return response_content
|
||||
except Exception as e:
|
||||
logger.error(f"Error in catch_all: {str(e)}")
|
||||
return f"Error: {str(e)}"
|
||||
|
||||
|
||||
def get_rag_tool(config: dict, complete_request: dict) -> FunctionTool:
|
||||
|
|
@ -236,12 +221,11 @@ def get_agents(agent_configs, tool_configs, complete_request):
|
|||
"type": "function",
|
||||
"function": tool_config
|
||||
})
|
||||
#TODO: Remove this once we have a way to handle the additionalProperties
|
||||
tool_config['parameters']['additionalProperties'] = False
|
||||
tool = FunctionTool(
|
||||
name=tool_name,
|
||||
description=tool_config["description"],
|
||||
params_json_schema=tool_config["parameters"],
|
||||
strict_json_schema=False,
|
||||
on_invoke_tool=lambda ctx, args, _tool_name=tool_name, _tool_config=tool_config, _complete_request=complete_request:
|
||||
catch_all(ctx, args, _tool_name, _tool_config, _complete_request)
|
||||
)
|
||||
|
|
@ -255,10 +239,13 @@ def get_agents(agent_configs, tool_configs, complete_request):
|
|||
# Create the agent object
|
||||
logger.debug(f"Creating Agent object for {agent_config['name']}")
|
||||
print(f"Creating Agent object for {agent_config['name']}")
|
||||
|
||||
# add the name and description to the agent instructions
|
||||
agent_instructions = f"## Your Name\n{agent_config['name']}\n\n## Description\n{agent_config['description']}\n\n## Instructions\n{agent_config['instructions']}"
|
||||
try:
|
||||
new_agent = NewAgent(
|
||||
name=agent_config["name"],
|
||||
instructions=agent_config["instructions"],
|
||||
instructions=agent_instructions,
|
||||
handoff_description=agent_config["description"],
|
||||
tools=new_tools,
|
||||
model=agent_config["model"],
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ services:
|
|||
- "3000:3000"
|
||||
environment:
|
||||
- OPENAI_API_KEY=${OPENAI_API_KEY}
|
||||
- MONGODB_CONNECTION_STRING=${MONGODB_CONNECTION_STRING}
|
||||
- MONGODB_CONNECTION_STRING=mongodb://mongo:27017/rowboat
|
||||
- USE_AUTH=${USE_AUTH}
|
||||
- AUTH0_SECRET=${AUTH0_SECRET}
|
||||
- AUTH0_BASE_URL=${AUTH0_BASE_URL}
|
||||
|
|
@ -49,7 +49,7 @@ services:
|
|||
- OPENAI_API_KEY=${OPENAI_API_KEY}
|
||||
- API_KEY=${AGENTS_API_KEY}
|
||||
- REDIS_URL=redis://redis:6379
|
||||
- MONGODB_URI=${MONGODB_CONNECTION_STRING}
|
||||
- MONGODB_URI=mongodb://mongo:27017/rowboat
|
||||
- QDRANT_URL=${QDRANT_URL}
|
||||
- QDRANT_API_KEY=${QDRANT_API_KEY}
|
||||
restart: unless-stopped
|
||||
|
|
@ -65,25 +65,25 @@ services:
|
|||
- API_KEY=${COPILOT_API_KEY}
|
||||
restart: unless-stopped
|
||||
|
||||
tools_webhook:
|
||||
build:
|
||||
context: ./apps/tools_webhook
|
||||
dockerfile: Dockerfile
|
||||
ports:
|
||||
- "3005:3005"
|
||||
environment:
|
||||
- SIGNING_SECRET=${SIGNING_SECRET}
|
||||
restart: unless-stopped
|
||||
# tools_webhook:
|
||||
# build:
|
||||
# context: ./apps/experimental/tools_webhook
|
||||
# dockerfile: Dockerfile
|
||||
# ports:
|
||||
# - "3005:3005"
|
||||
# environment:
|
||||
# - SIGNING_SECRET=${SIGNING_SECRET}
|
||||
# restart: unless-stopped
|
||||
|
||||
simulation_runner:
|
||||
build:
|
||||
context: ./apps/simulation_runner
|
||||
dockerfile: Dockerfile
|
||||
environment:
|
||||
- MONGODB_URI=${MONGODB_CONNECTION_STRING}
|
||||
- ROWBOAT_API_HOST=http://rowboat:3000
|
||||
- OPENAI_API_KEY=${OPENAI_API_KEY}
|
||||
restart: unless-stopped
|
||||
# simulation_runner:
|
||||
# build:
|
||||
# context: ./apps/experimental/simulation_runner
|
||||
# dockerfile: Dockerfile
|
||||
# environment:
|
||||
# - MONGODB_URI=mongodb://mongo:27017/rowboat
|
||||
# - ROWBOAT_API_HOST=http://rowboat:3000
|
||||
# - OPENAI_API_KEY=${OPENAI_API_KEY}
|
||||
# restart: unless-stopped
|
||||
|
||||
setup_qdrant:
|
||||
build:
|
||||
|
|
@ -115,7 +115,7 @@ services:
|
|||
profiles: [ "rag_files_worker" ]
|
||||
environment:
|
||||
- OPENAI_API_KEY=${OPENAI_API_KEY}
|
||||
- MONGODB_CONNECTION_STRING=${MONGODB_CONNECTION_STRING}
|
||||
- MONGODB_CONNECTION_STRING=mongodb://mongo:27017/rowboat
|
||||
- GOOGLE_API_KEY=${GOOGLE_API_KEY}
|
||||
- AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID}
|
||||
- AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY}
|
||||
|
|
@ -133,7 +133,7 @@ services:
|
|||
profiles: [ "rag_urls_worker" ]
|
||||
environment:
|
||||
- OPENAI_API_KEY=${OPENAI_API_KEY}
|
||||
- MONGODB_CONNECTION_STRING=${MONGODB_CONNECTION_STRING}
|
||||
- MONGODB_CONNECTION_STRING=mongodb://mongo:27017/rowboat
|
||||
- FIRECRAWL_API_KEY=${FIRECRAWL_API_KEY}
|
||||
- QDRANT_URL=${QDRANT_URL}
|
||||
- QDRANT_API_KEY=${QDRANT_API_KEY}
|
||||
|
|
@ -147,23 +147,32 @@ services:
|
|||
profiles: [ "rag_text_worker" ]
|
||||
environment:
|
||||
- OPENAI_API_KEY=${OPENAI_API_KEY}
|
||||
- MONGODB_CONNECTION_STRING=${MONGODB_CONNECTION_STRING}
|
||||
- MONGODB_CONNECTION_STRING=mongodb://mongo:27017/rowboat
|
||||
- QDRANT_URL=${QDRANT_URL}
|
||||
- QDRANT_API_KEY=${QDRANT_API_KEY}
|
||||
restart: unless-stopped
|
||||
|
||||
chat_widget:
|
||||
build:
|
||||
context: ./apps/chat_widget
|
||||
dockerfile: Dockerfile
|
||||
profiles: [ "chat_widget" ]
|
||||
# chat_widget:
|
||||
# build:
|
||||
# context: ./apps/experimental/chat_widget
|
||||
# dockerfile: Dockerfile
|
||||
# profiles: [ "chat_widget" ]
|
||||
# ports:
|
||||
# - "3006:3006"
|
||||
# environment:
|
||||
# - PORT=3006
|
||||
# - CHAT_WIDGET_HOST=http://localhost:3006
|
||||
# - ROWBOAT_HOST=http://localhost:3000
|
||||
# restart: unless-stopped
|
||||
|
||||
mongo:
|
||||
image: mongo:latest
|
||||
ports:
|
||||
- "3006:3006"
|
||||
environment:
|
||||
- PORT=3006
|
||||
- CHAT_WIDGET_HOST=http://localhost:3006
|
||||
- ROWBOAT_HOST=http://localhost:3000
|
||||
- "27017:27017"
|
||||
restart: unless-stopped
|
||||
attach: false
|
||||
volumes:
|
||||
- ./data/mongo:/data/db
|
||||
|
||||
redis:
|
||||
image: redis:latest
|
||||
|
|
@ -181,12 +190,12 @@ services:
|
|||
|
||||
# twilio_handler:
|
||||
# build:
|
||||
# context: ./apps/twilio_handler
|
||||
# context: ./apps/experimental/twilio_handler
|
||||
# dockerfile: Dockerfile
|
||||
# ports:
|
||||
# - "4010:4010"
|
||||
# environment:
|
||||
# - ELEVENLABS_API_KEY=${ELEVENLABS_API_KEY}
|
||||
# - ROWBOAT_API_HOST=http://rowboat:3000
|
||||
# - MONGODB_URI=${MONGODB_CONNECTION_STRING}
|
||||
# - MONGODB_URI=mongodb://mongo:27017/rowboat
|
||||
# restart: unless-stopped
|
||||
|
|
|
|||