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**
|
||||
- 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
|
||||
|
||||
1. **MongoDB Connection Issues**
|
||||
|
|
|
|||
|
|
@ -467,7 +467,11 @@ export async function getAssistantResponse(
|
|||
await projectAuthCheck(projectId);
|
||||
|
||||
const response = await getAgenticApiResponse(request);
|
||||
return response;
|
||||
return {
|
||||
messages: convertFromAgenticAPIChatMessages(response.messages),
|
||||
state: response.state,
|
||||
rawAPIResponse: response.rawAPIResponse,
|
||||
};
|
||||
}
|
||||
|
||||
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 { ObjectId, WithId } from "mongodb";
|
||||
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";
|
||||
|
||||
const chatsCollection = db.collection<z.infer<typeof apiV1.Chat>>("chats");
|
||||
|
|
@ -96,7 +96,8 @@ export async function POST(
|
|||
if (response.messages.length === 0) {
|
||||
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,
|
||||
version: 'v1' as const,
|
||||
chatId,
|
||||
|
|
@ -104,7 +105,7 @@ export async function POST(
|
|||
})));
|
||||
|
||||
// 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) {
|
||||
// execute tool calls
|
||||
console.log("Executing tool calls", lastMessage.tool_calls);
|
||||
|
|
|
|||
|
|
@ -623,3 +623,128 @@ export function convertToCopilotWorkflow(workflow: z.infer<typeof Workflow>): z.
|
|||
...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 { projectsCollection } from "./mongodb";
|
||||
import { apiV1 } from "rowboat-shared";
|
||||
|
|
@ -172,7 +172,7 @@ export async function callClientToolWebhook(
|
|||
export async function getAgenticApiResponse(
|
||||
request: z.infer<typeof AgenticAPIChatRequest>,
|
||||
): Promise<{
|
||||
messages: z.infer<typeof apiV1.ChatMessage>[],
|
||||
messages: z.infer<typeof AgenticAPIChatMessage>[],
|
||||
state: unknown,
|
||||
rawAPIResponse: unknown,
|
||||
}> {
|
||||
|
|
@ -191,7 +191,7 @@ export async function getAgenticApiResponse(
|
|||
const responseJson = await response.json();
|
||||
const result: z.infer<typeof AgenticAPIChatResponse> = responseJson;
|
||||
return {
|
||||
messages: convertFromAgenticAPIChatMessages(result.messages),
|
||||
messages: result.messages,
|
||||
state: result.state,
|
||||
rawAPIResponse: result,
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue