mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-05-09 15:22:39 +02:00
add openai style /chat api
This commit is contained in:
parent
7cec090ace
commit
76533d26c9
7 changed files with 276 additions and 7 deletions
45
README.md
45
README.md
|
|
@ -77,6 +77,51 @@ Before running RowBoat, ensure you have:
|
||||||
4. **Access the App**
|
4. **Access the App**
|
||||||
- Visit [http://localhost:3000](http://localhost:3000).
|
- Visit [http://localhost:3000](http://localhost:3000).
|
||||||
|
|
||||||
|
5. **Use the API**
|
||||||
|
|
||||||
|
You can use the API at [http://localhost:3000/api/v1/](http://localhost:3000/api/v1/)
|
||||||
|
- Project ID is available in the URL of the project page
|
||||||
|
- Project Secret is available in the project config page
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl --location 'http://localhost:3000/api/v1/<PROJECT_ID>/chat' \
|
||||||
|
--header 'Content-Type: application/json' \
|
||||||
|
--header 'Authorization: Bearer <PROJECT_SECRET>' \
|
||||||
|
--data '{
|
||||||
|
"messages": [
|
||||||
|
{
|
||||||
|
"role": "user",
|
||||||
|
"content": "tell me the weather in london in metric units"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
1. **MongoDB Connection Issues**
|
1. **MongoDB Connection Issues**
|
||||||
|
|
|
||||||
|
|
@ -467,7 +467,11 @@ export async function getAssistantResponse(
|
||||||
await projectAuthCheck(projectId);
|
await projectAuthCheck(projectId);
|
||||||
|
|
||||||
const response = await getAgenticApiResponse(request);
|
const response = await getAgenticApiResponse(request);
|
||||||
return response;
|
return {
|
||||||
|
messages: convertFromAgenticAPIChatMessages(response.messages),
|
||||||
|
state: response.state,
|
||||||
|
rawAPIResponse: response.rawAPIResponse,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getCopilotResponse(
|
export async function getCopilotResponse(
|
||||||
|
|
|
||||||
70
apps/rowboat/app/api/v1/[projectId]/chat/route.ts
Normal file
70
apps/rowboat/app/api/v1/[projectId]/chat/route.ts
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
import { NextRequest } from "next/server";
|
||||||
|
import { agentWorkflowsCollection, db, projectsCollection } from "@/app/lib/mongodb";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { ObjectId } from "mongodb";
|
||||||
|
import { authCheck } from "@/app/api/v1/utils";
|
||||||
|
import { convertFromApiToAgenticApiMessages, convertFromAgenticApiToApiMessages, AgenticAPIChatRequest, ApiRequest, ApiResponse, convertWorkflowToAgenticAPI } from "@/app/lib/types";
|
||||||
|
import { getAgenticApiResponse } from "@/app/lib/utils";
|
||||||
|
|
||||||
|
// get next turn / agent response
|
||||||
|
export async function POST(
|
||||||
|
req: NextRequest,
|
||||||
|
{ params }: { params: Promise<{ projectId: string }> }
|
||||||
|
): Promise<Response> {
|
||||||
|
const { projectId } = await params;
|
||||||
|
|
||||||
|
return await authCheck(projectId, req, async () => {
|
||||||
|
// parse and validate the request body
|
||||||
|
let body;
|
||||||
|
try {
|
||||||
|
body = await req.json();
|
||||||
|
} catch (e) {
|
||||||
|
return Response.json({ error: "Invalid JSON in request body" }, { status: 400 });
|
||||||
|
}
|
||||||
|
const result = ApiRequest.safeParse(body);
|
||||||
|
if (!result.success) {
|
||||||
|
return Response.json({ error: `Invalid request body: ${result.error.message}` }, { status: 400 });
|
||||||
|
}
|
||||||
|
const reqMessages = result.data.messages;
|
||||||
|
const reqState = result.data.state;
|
||||||
|
|
||||||
|
// fetch published workflow id
|
||||||
|
const project = await projectsCollection.findOne({
|
||||||
|
_id: projectId,
|
||||||
|
});
|
||||||
|
if (!project) {
|
||||||
|
return Response.json({ error: "Project not found" }, { status: 404 });
|
||||||
|
}
|
||||||
|
if (!project.publishedWorkflowId) {
|
||||||
|
return Response.json({ error: "Project has no published workflow" }, { status: 404 });
|
||||||
|
}
|
||||||
|
// fetch workflow
|
||||||
|
const workflow = await agentWorkflowsCollection.findOne({
|
||||||
|
projectId: projectId,
|
||||||
|
_id: new ObjectId(project.publishedWorkflowId),
|
||||||
|
});
|
||||||
|
if (!workflow) {
|
||||||
|
return Response.json({ error: "Workflow not found" }, { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// get assistant response
|
||||||
|
const { agents, tools, prompts, startAgent } = convertWorkflowToAgenticAPI(workflow);
|
||||||
|
const request: z.infer<typeof AgenticAPIChatRequest> = {
|
||||||
|
messages: convertFromApiToAgenticApiMessages(reqMessages),
|
||||||
|
state: reqState ?? { last_agent_name: startAgent },
|
||||||
|
agents,
|
||||||
|
tools,
|
||||||
|
prompts,
|
||||||
|
startAgent,
|
||||||
|
};
|
||||||
|
console.log("turn: sending agentic request from /chat api", JSON.stringify(request, null, 2));
|
||||||
|
const { messages, state } = await getAgenticApiResponse(request);
|
||||||
|
|
||||||
|
const response: z.infer<typeof ApiResponse> = {
|
||||||
|
messages: convertFromAgenticApiToApiMessages(messages),
|
||||||
|
state,
|
||||||
|
};
|
||||||
|
|
||||||
|
return Response.json(response);
|
||||||
|
});
|
||||||
|
}
|
||||||
24
apps/rowboat/app/api/v1/utils.ts
Normal file
24
apps/rowboat/app/api/v1/utils.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
import { NextRequest } from "next/server";
|
||||||
|
import { projectsCollection } from "@/app/lib/mongodb";
|
||||||
|
|
||||||
|
export async function authCheck(projectId: string, req: NextRequest, handler: () => Promise<Response>): Promise<Response> {
|
||||||
|
const authHeader = req.headers.get('Authorization');
|
||||||
|
if (!authHeader?.startsWith('Bearer ')) {
|
||||||
|
return Response.json({ error: "Authorization header must be a Bearer token" }, { status: 400 });
|
||||||
|
}
|
||||||
|
const token = authHeader.split(' ')[1];
|
||||||
|
if (!token) {
|
||||||
|
return Response.json({ error: "Missing API key in request" }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// check the key in project settings
|
||||||
|
const project = await projectsCollection.findOne({
|
||||||
|
_id: projectId,
|
||||||
|
secret: token,
|
||||||
|
});
|
||||||
|
if (!project) {
|
||||||
|
return Response.json({ error: "Invalid API key" }, { status: 403 });
|
||||||
|
}
|
||||||
|
|
||||||
|
return await handler();
|
||||||
|
}
|
||||||
|
|
@ -4,7 +4,7 @@ import { agentWorkflowsCollection, db, projectsCollection } from "@/app/lib/mong
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { ObjectId, WithId } from "mongodb";
|
import { ObjectId, WithId } from "mongodb";
|
||||||
import { authCheck } from "../../../utils";
|
import { authCheck } from "../../../utils";
|
||||||
import { AgenticAPIChatRequest, convertToAgenticAPIChatMessages, convertWorkflowToAgenticAPI } from "@/app/lib/types";
|
import { AgenticAPIChatRequest, convertFromAgenticAPIChatMessages, convertToAgenticAPIChatMessages, convertWorkflowToAgenticAPI } from "@/app/lib/types";
|
||||||
import { callClientToolWebhook, getAgenticApiResponse } from "@/app/lib/utils";
|
import { callClientToolWebhook, getAgenticApiResponse } from "@/app/lib/utils";
|
||||||
|
|
||||||
const chatsCollection = db.collection<z.infer<typeof apiV1.Chat>>("chats");
|
const chatsCollection = db.collection<z.infer<typeof apiV1.Chat>>("chats");
|
||||||
|
|
@ -96,7 +96,8 @@ export async function POST(
|
||||||
if (response.messages.length === 0) {
|
if (response.messages.length === 0) {
|
||||||
throw new Error("No messages returned from assistant");
|
throw new Error("No messages returned from assistant");
|
||||||
}
|
}
|
||||||
unsavedMessages.push(...response.messages.map(m => ({
|
const convertedMessages = convertFromAgenticAPIChatMessages(response.messages);
|
||||||
|
unsavedMessages.push(...convertedMessages.map(m => ({
|
||||||
...m,
|
...m,
|
||||||
version: 'v1' as const,
|
version: 'v1' as const,
|
||||||
chatId,
|
chatId,
|
||||||
|
|
@ -104,7 +105,7 @@ export async function POST(
|
||||||
})));
|
})));
|
||||||
|
|
||||||
// if the last messages is tool call, execute them
|
// if the last messages is tool call, execute them
|
||||||
const lastMessage = response.messages[response.messages.length - 1];
|
const lastMessage = convertedMessages[convertedMessages.length - 1];
|
||||||
if (lastMessage.role === 'assistant' && 'tool_calls' in lastMessage) {
|
if (lastMessage.role === 'assistant' && 'tool_calls' in lastMessage) {
|
||||||
// execute tool calls
|
// execute tool calls
|
||||||
console.log("Executing tool calls", lastMessage.tool_calls);
|
console.log("Executing tool calls", lastMessage.tool_calls);
|
||||||
|
|
|
||||||
|
|
@ -623,3 +623,128 @@ export function convertToCopilotWorkflow(workflow: z.infer<typeof Workflow>): z.
|
||||||
...rest,
|
...rest,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const ApiMessage = z.union([
|
||||||
|
apiV1.SystemMessage,
|
||||||
|
apiV1.UserMessage,
|
||||||
|
apiV1.AssistantMessage,
|
||||||
|
apiV1.AssistantMessageWithToolCalls,
|
||||||
|
apiV1.ToolMessage,
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const ApiRequest = z.object({
|
||||||
|
messages: z.array(ApiMessage),
|
||||||
|
state: z.unknown(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const ApiResponse = z.object({
|
||||||
|
messages: z.array(ApiMessage),
|
||||||
|
state: z.unknown(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export function convertFromApiToAgenticApiMessages(messages: z.infer<typeof ApiMessage>[]): z.infer<typeof AgenticAPIChatMessage>[] {
|
||||||
|
return messages.map(m => {
|
||||||
|
switch (m.role) {
|
||||||
|
case 'system':
|
||||||
|
return {
|
||||||
|
role: 'system',
|
||||||
|
content: m.content,
|
||||||
|
tool_calls: null,
|
||||||
|
tool_call_id: null,
|
||||||
|
tool_name: null,
|
||||||
|
sender: null,
|
||||||
|
};
|
||||||
|
case 'user':
|
||||||
|
return {
|
||||||
|
role: 'user',
|
||||||
|
content: m.content,
|
||||||
|
tool_calls: null,
|
||||||
|
tool_call_id: null,
|
||||||
|
tool_name: null,
|
||||||
|
sender: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'assistant':
|
||||||
|
if ('tool_calls' in m) {
|
||||||
|
return {
|
||||||
|
role: 'assistant',
|
||||||
|
content: m.content ?? null,
|
||||||
|
tool_calls: m.tool_calls,
|
||||||
|
tool_call_id: null,
|
||||||
|
tool_name: null,
|
||||||
|
sender: m.agenticSender ?? null,
|
||||||
|
response_type: m.agenticResponseType ?? 'external',
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
role: 'assistant',
|
||||||
|
content: m.content ?? null,
|
||||||
|
sender: m.agenticSender ?? null,
|
||||||
|
response_type: m.agenticResponseType ?? 'external',
|
||||||
|
tool_call_id: null,
|
||||||
|
tool_calls: null,
|
||||||
|
tool_name: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case 'tool':
|
||||||
|
return {
|
||||||
|
role: 'tool',
|
||||||
|
content: m.content ?? null,
|
||||||
|
tool_calls: null,
|
||||||
|
tool_call_id: m.tool_call_id ?? null,
|
||||||
|
tool_name: m.tool_name ?? null,
|
||||||
|
sender: null,
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
return {
|
||||||
|
role: "user",
|
||||||
|
content: "foo",
|
||||||
|
tool_calls: null,
|
||||||
|
tool_call_id: null,
|
||||||
|
tool_name: null,
|
||||||
|
sender: null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function convertFromAgenticApiToApiMessages(messages: z.infer<typeof AgenticAPIChatMessage>[]): z.infer<typeof ApiMessage>[] {
|
||||||
|
const converted: z.infer<typeof ApiMessage>[] = [];
|
||||||
|
|
||||||
|
for (const m of messages) {
|
||||||
|
switch (m.role) {
|
||||||
|
case 'user':
|
||||||
|
converted.push({
|
||||||
|
role: 'user',
|
||||||
|
content: m.content ?? '',
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case 'assistant':
|
||||||
|
if (m.tool_calls) {
|
||||||
|
converted.push({
|
||||||
|
role: 'assistant',
|
||||||
|
tool_calls: m.tool_calls,
|
||||||
|
agenticSender: m.sender ?? undefined,
|
||||||
|
agenticResponseType: m.response_type ?? 'internal',
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
converted.push({
|
||||||
|
role: 'assistant',
|
||||||
|
content: m.content ?? '',
|
||||||
|
agenticSender: m.sender ?? undefined,
|
||||||
|
agenticResponseType: m.response_type ?? 'internal',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'tool':
|
||||||
|
converted.push({
|
||||||
|
role: 'tool',
|
||||||
|
content: m.content ?? '',
|
||||||
|
tool_call_id: m.tool_call_id ?? '',
|
||||||
|
tool_name: m.tool_name ?? '',
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return converted;
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { AgenticAPIChatRequest, AgenticAPIChatResponse, ClientToolCallJwt, ClientToolCallRequest, ClientToolCallRequestBody, convertFromAgenticAPIChatMessages, Workflow } from "@/app/lib/types";
|
import { AgenticAPIChatMessage, AgenticAPIChatRequest, AgenticAPIChatResponse, ClientToolCallJwt, ClientToolCallRequest, ClientToolCallRequestBody, convertFromAgenticAPIChatMessages, Workflow } from "@/app/lib/types";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { projectsCollection } from "./mongodb";
|
import { projectsCollection } from "./mongodb";
|
||||||
import { apiV1 } from "rowboat-shared";
|
import { apiV1 } from "rowboat-shared";
|
||||||
|
|
@ -172,7 +172,7 @@ export async function callClientToolWebhook(
|
||||||
export async function getAgenticApiResponse(
|
export async function getAgenticApiResponse(
|
||||||
request: z.infer<typeof AgenticAPIChatRequest>,
|
request: z.infer<typeof AgenticAPIChatRequest>,
|
||||||
): Promise<{
|
): Promise<{
|
||||||
messages: z.infer<typeof apiV1.ChatMessage>[],
|
messages: z.infer<typeof AgenticAPIChatMessage>[],
|
||||||
state: unknown,
|
state: unknown,
|
||||||
rawAPIResponse: unknown,
|
rawAPIResponse: unknown,
|
||||||
}> {
|
}> {
|
||||||
|
|
@ -191,7 +191,7 @@ export async function getAgenticApiResponse(
|
||||||
const responseJson = await response.json();
|
const responseJson = await response.json();
|
||||||
const result: z.infer<typeof AgenticAPIChatResponse> = responseJson;
|
const result: z.infer<typeof AgenticAPIChatResponse> = responseJson;
|
||||||
return {
|
return {
|
||||||
messages: convertFromAgenticAPIChatMessages(result.messages),
|
messages: result.messages,
|
||||||
state: result.state,
|
state: result.state,
|
||||||
rawAPIResponse: result,
|
rawAPIResponse: result,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue