core/apps/webapp/app/routes/api.v1.conversation._index.tsx
Harshith Mullapudi c869096be8
Feat: Space v3
* feat: space v3

* feat: connected space creation

* fix:

* fix: session_id for memory ingestion

* chore: simplify gitignore patterns for agent directories

---------

Co-authored-by: Manoj <saimanoj58@gmail.com>
2025-10-30 12:30:56 +05:30

160 lines
4.2 KiB
TypeScript

import {
convertToModelMessages,
streamText,
validateUIMessages,
type LanguageModel,
experimental_createMCPClient as createMCPClient,
generateId,
stepCountIs,
} from "ai";
import { z } from "zod";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
import { createHybridActionApiRoute } from "~/services/routeBuilders/apiBuilder.server";
import {
createConversationHistory,
getConversationAndHistory,
} from "~/services/conversation.server";
import { getModel } from "~/lib/model.server";
import { UserTypeEnum } from "@core/types";
import { nanoid } from "nanoid";
import {
deletePersonalAccessToken,
getOrCreatePersonalAccessToken,
} from "~/services/personalAccessToken.server";
import {
hasAnswer,
hasQuestion,
REACT_SYSTEM_PROMPT,
} from "~/lib/prompt.server";
import { enqueueCreateConversationTitle } from "~/lib/queue-adapter.server";
import { env } from "~/env.server";
const ChatRequestSchema = z.object({
message: z.object({
id: z.string().optional(),
parts: z.array(z.any()),
role: z.string(),
}),
id: z.string(),
});
const { loader, action } = createHybridActionApiRoute(
{
body: ChatRequestSchema,
allowJWT: true,
authorization: {
action: "conversation",
},
corsStrategy: "all",
},
async ({ body, authentication }) => {
const randomKeyName = `chat_${nanoid(10)}`;
const pat = await getOrCreatePersonalAccessToken({
name: randomKeyName,
userId: authentication.userId,
});
const message = body.message.parts[0].text;
const id = body.message.id;
const apiEndpoint = `${env.APP_ORIGIN}/api/v1/mcp?source=core`;
const url = new URL(apiEndpoint);
const mcpClient = await createMCPClient({
transport: new StreamableHTTPClientTransport(url, {
requestInit: {
headers: pat.token
? {
Authorization: `Bearer ${pat.token}`,
}
: {},
},
}),
});
const conversation = await getConversationAndHistory(
body.id,
authentication.userId,
);
const conversationHistory = conversation?.ConversationHistory ?? [];
if (conversationHistory.length === 0) {
// Trigger conversation title task
await enqueueCreateConversationTitle({
conversationId: body.id,
message,
});
}
if (conversationHistory.length > 1) {
await createConversationHistory(message, body.id, UserTypeEnum.User);
}
const messages = conversationHistory.map((history: any) => {
return {
parts: [{ text: history.message, type: "text" }],
role: "user",
id: history.id,
};
});
const tools = { ...(await mcpClient.tools()) };
const finalMessages = [
...messages,
{
parts: [{ text: message, type: "text" }],
role: "user",
id: id ?? generateId(),
},
];
const validatedMessages = await validateUIMessages({
messages: finalMessages,
});
const result = streamText({
model: getModel() as LanguageModel,
messages: [
{
role: "system",
content: REACT_SYSTEM_PROMPT,
},
...convertToModelMessages(validatedMessages),
],
tools,
stopWhen: [stepCountIs(10), hasAnswer, hasQuestion],
});
result.consumeStream(); // no await
await deletePersonalAccessToken(pat?.id);
return result.toUIMessageStreamResponse({
originalMessages: validatedMessages,
onFinish: async ({ messages }) => {
const lastMessage = messages.pop();
let message = "";
lastMessage?.parts.forEach((part) => {
if (part.type === "text") {
message += part.text;
}
});
await createConversationHistory(message, body.id, UserTypeEnum.Agent);
},
// async consumeSseStream({ stream }) {
// // Create a resumable stream from the SSE stream
// const streamContext = createResumableStreamContext({ waitUntil: null });
// await streamContext.createNewResumableStream(
// conversation.conversationHistoryId,
// () => stream,
// );
// },
});
},
);
export { loader, action };