add openai style /chat api

This commit is contained in:
ramnique 2025-01-14 17:47:45 +05:30
parent 7cec090ace
commit 76533d26c9
7 changed files with 276 additions and 7 deletions

View file

@ -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**

View file

@ -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(

View 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);
});
}

View 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();
}

View file

@ -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);

View file

@ -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;
}

View file

@ -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,
};