Building an AI application from scratch can be a surprisingly difficult task. Whether you want a simple chatbot or a more advanced assistant, getting to a usable product can easily take weeks. This is exactly the kind of situation where Julep becomes helpful.
Julep is a platform for building stateful, functional, LLM-driven applications. With it, you can get a working AI application running with only a small amount of code.
Platforms like OpenAI GPT, Azure Bot Service, and Dialogflow can also be used to build AI applications. What makes Julep stand out is its support for:
In this article, we will build a small AI movie companion application that can recommend and describe movies based on what the user asks for. The goal is to walk through the core Julep workflow step by step.
Make sure Node.js is installed on your machine:
Learn more about Julep:
https://github.com/julep-ai/julep/blob/dev/README-CN.md
Start by creating a React project:
pnpm create viteThen install ChatUI:
pnpm add @chatui/core -DIn App.tsx, build a minimal chat interface with a <Chat> component:
<Chat
navbar={{ title: "Movie Companion" }}
messages={messages}
renderMessageContent={renderMessageContent}
quickReplies={defaultQuickReplies}
onQuickReplyClick={handleQuickReplyClick}
onSend={handleSend}
/>Once the overall structure is there, the rest comes down to filling in a few core methods.
renderMessageContent function renderMessageContent(msg: MessageProps) {
const { type, content } = msg;
switch (type) {
case "text":
return <Bubble content={content.text} />;
case "image":
return (
<Bubble type="image">
<img src={content.picUrl} alt="Movie-related result image" />
</Bubble>
);
default:
return null;
}
}This uses ChatUI's render hook to display different message types differently.
defaultQuickReplies const defaultQuickReplies: Array<QuickReplie> = [
{
icon: "message",
name: "Recommend a comedy",
isNew: true,
isHighlight: true,
},
{
name: "Recommend any movie",
isNew: true,
},
];These quick replies help users start interacting immediately.
handleSend and handleQuickReplyClick function handleSend(type: string, val: string) {
if (type === "text" && val.trim()) {
appendMsg({
type: "text",
content: { text: val },
position: "right",
});
setTyping(true);
axios
.post("http://127.0.0.1:3000/chat", {
query: val,
})
.then((res) => {
const agentResponse = res.data.response;
appendMsg({
type: "text",
content: { text: agentResponse },
});
})
.catch(() => {
appendMsg({
type: "text",
content: { text: "Something went wrong. Please try again." },
});
});
}
}
function handleQuickReplyClick(item: QuickReplie) {
handleSend("text", item.name);
}This is where the frontend hands the user's message to the backend service.
App.tsx import Chat, { Bubble, useMessages, MessageProps } from "@chatui/core";
import axios from "axios";
const initialMessages = [
{
type: "text",
content: { text: "Hi, I'm your movie companion." },
user: {
avatar: "//gw.alicdn.com/tfs/TB1DYHLwMHqK1RjSZFEXXcGMXXa-56-62.svg",
},
},
];
interface QuickReplie {
icon?: string;
name: string;
isNew?: boolean;
isHighlight?: boolean;
}
const defaultQuickReplies: Array<QuickReplie> = [
{
icon: "message",
name: "Recommend a comedy",
isNew: true,
isHighlight: true,
},
{
name: "Recommend any movie",
isNew: true,
},
];
const App = () => {
const { messages, appendMsg, setTyping } = useMessages(initialMessages);
function handleSend(type: string, val: string) {
if (type === "text" && val.trim()) {
appendMsg({
type: "text",
content: { text: val },
position: "right",
});
setTyping(true);
axios
.post("http://127.0.0.1:3000/chat", {
query: val,
})
.then((res) => {
const agentResponse = res.data.response;
appendMsg({
type: "text",
content: { text: agentResponse },
});
})
.catch(() => {
appendMsg({
type: "text",
content: { text: "Something went wrong. Please try again." },
});
});
}
}
function handleQuickReplyClick(item: QuickReplie) {
handleSend("text", item.name);
}
function renderMessageContent(msg: MessageProps) {
const { type, content } = msg;
switch (type) {
case "text":
return <Bubble content={content.text} />;
case "image":
return (
<Bubble type="image">
<img src={content.picUrl} alt="Movie-related result image" />
</Bubble>
);
default:
return null;
}
}
return (
<Chat
navbar={{ title: "Movie Companion" }}
messages={messages}
renderMessageContent={renderMessageContent}
quickReplies={defaultQuickReplies}
onQuickReplyClick={handleQuickReplyClick}
onSend={handleSend}
/>
);
};
export default App;Frontend result:
Now that the frontend is ready, the next step is integrating Julep.
For this movie companion application, install:
express@julep/sdkbody-parsercorsdotenvaxiospnpm add express @julep/sdk cors body-parser dotenv axios -DTo use Julep, you first need an API key.
Go to:
Log in with your Google account and copy the API token shown in the top-right area.
Create a .env file in the project:
JULEP_API_KEY="api_key"Replace api_key with your real token.
Then create src/server.js.
import express from "express";
import julep from "@julep/sdk";
import bodyParser from "body-parser";
import cors from "cors";
import { fileURLToPath } from "url";
import path from "path";
import dotenv from "dotenv";const apiKey = process.env.JULEP_API_KEY;
const client = new julep.Client({ apiKey });const app = express();
app.use(bodyParser.json());
app.use(cors());/chat endpoint app.post("/chat", async (req, res) => {
try {
const query = req.body.query;
} catch (error) {
res.status(500).send(error.message);
}
});Inside the handler, the first useful piece is just the user's query:
try {
const query = req.body.query;
} catch (error) {
res.status(500).send(error.message);
}In Julep, a user represents the person or system interacting with the app.
Julep provides users.create() for this:
const user = await client.users.create({
name: "xiaohong",
about: "Machine Learning Developer and AI Enthusiast",
docs: [{ title: "AI Efficiency Report", content: "...", metadata: { page: 1 } }],
metadata: { db_uuid: "1234" },
});For this application:
const user = await client.users.create({
name: "Xiaoxu",
about: "Frontend engineer",
});An agent acts as the intelligence layer between the user and the application.
For example:
const agent = await client.agents.create({
name: "Movie suggesting assistant",
model: "gpt-4-turbo",
});Julep supports multiple LLMs, so this is only one possible choice.
A session represents one conversational context between the user and the agent.
const session = await client.sessions.create({
agentId: agent.id,
userId: user.id,
situation: "You are a movie companion. Tell people about the movies they ask for and recommend movies to them.",
});The situation field matters because it gives the agent contextual framing for the conversation.
Once the user, agent, and session exist, the actual conversation can happen through sessions.chat().
const chatParams = {
messages: [
{
role: "user",
name: "Ayush",
content: query,
},
],
};
const chatResponse = await client.sessions.chat(session.id, chatParams);
const responseMessage = chatResponse.response[0][0].content;
res.json({ response: responseMessage });The message object includes:
role: "user"name: the user's namecontent: the actual questionThen the response is extracted from chatResponse and returned to the frontend.
Use a catch block to return backend errors cleanly:
catch (error) {
res.status(500).json({ error: error.message });
}app.listen(3000, () => {
console.log("Server is running on port 3000");
});That hosts the backend at localhost:3000.
Start the backend first:
node src/server.jsThen run the frontend:
npm run devThis will bring the project up locally.
Demo screenshots:
https://github.com/Xutaotaotao/movie-companion-app
By combining ChatUI and Julep, you can build an AI application with both:
The whole process involves UI work on the frontend, AI integration on the backend, and API-based communication between the two.
Once the project works locally, it can be deployed and opened up to real users. This article is only an introductory version. A production-grade version would still need more polishing and optimization, but it is already enough to help you build your own small AI application from scratch.