Install Inconvo
Section titled “Install Inconvo”npm i @inconvoai/node
Integrate an analytics agent into your application with Inconvo.
Build your in-app analytics agent using Inconvo, a backend API that manages database connection, semantic translation, multi-tennancy and more.
This example uses a React frontend and a Node.js backend, but you can use any frontend and backend technology you like.
npm i @inconvoai/node
INCONVO_API_KEY="$YOUR_API_KEY"INCONVO_API_BASE_URL="https://app.inconvo.ai/api/v1"
const client = new Inconvo({ baseURL: process.env.INCONVO_API_BASE_URL, apiKey: process.env.INCONVO_API_KEY,});
Add an endpoint on your server to create a conversation.
POST /api/v1/conversations
responds with an id
which is used to create conversation responses.
app.post("/create-conversation", async (_req, res, next) => { // Placeholder context – you should get this from the server session. const context = { organisationId: 1, }; try { const conversation = await client.conversations.create({ context: context, }); return res.json(conversation); } catch (error) { next(error); }});
Add an endpoint on your server to create a response.
POST /conversations/:id/response
responds with a message and includes:
id
which you can use when attaching feedback to an answer.conversationId
which you can optionally pass to the next call to continue the conversation.See the complete response type reference here.
app.post("/create-response", async (req, res, next) => { const { message, conversationId, stream = false } = req.body; try { const response = client.conversations.response.create(conversationId, { message, stream, });
if (!stream) return res.json(await response);
res.writeHead(200, { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", Connection: "keep-alive", }); for await (const chunk of response) { res.write(`data: ${JSON.stringify(chunk)}\n\n`); } res.write("data: [DONE]\n\n"); res.end(); } catch (error) { if (stream && res.headersSent) { res.write(`data: ${JSON.stringify({ error: error.message })}\n\n`); res.end(); } else { next(error); } }});
Initialize some state to keep track of the message-response pairs, and manage the user interface.
const Assistant = () => { const [message, setMessage] = useState(""); const [messages, setMessages] = useState([]); const [isStreaming, setIsStreaming] = useState(false); const [streamingEnabled, setStreamingEnabled] = useState(true); const [isCreatingConversation, setIsCreatingConversation] = useState(false); const [conversationId, setConversationId] = useState(null);}
const createNewConversation = async () => { setConversationId(null); setMessage(""); setMessageResponsePairs([]); try { const res = await fetch(`http://localhost:4242/create-conversation`, { method: "POST", }); const conversation = await res.json(); setConversationId(conversation.id); } catch (err) { console.error("Error creating conversation:", err); }};
const MessageInput = ({ message, setMessage, conversationId, onSubmit, isStreaming }) => ( <form onSubmit={onSubmit}> <label> Enter message: <input id="message" type="text" disabled={!conversationId} value={message} onChange={(e) => setMessage(e.target.value)} placeholder={conversationId ? "What is our most popular product?" : "Start a new conversation"} /> </label> <button disabled={!conversationId || isStreaming} id="submit"> Submit </button> </form>);
Create a function to send messages to an Inconvo conversation.
const sendMessage = async (userMessage, conversationId, options = {}) => { const { stream = false, onUpdate } = options;
const response = await fetch(`http://localhost:4242/create-response`, { method: "POST", headers: { "Content-Type": "application/json", ...(stream && { Accept: "text/event-stream" }), }, body: JSON.stringify({ message: userMessage, conversationId, stream }), });
if (!stream) return response.json();
const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = "";
try { while (true) { const { done, value } = await reader.read(); if (done) break;
buffer += decoder.decode(value, { stream: true }); const lines = buffer.split("\n"); buffer = lines.pop() || "";
for (const line of lines) { if (!line.startsWith("data: ")) continue;
const data = line.slice(6); if (data === "[DONE]") return;
try { onUpdate(JSON.parse(data)); } catch (e) { console.error("Parse error:", e); } } } } catch (error) { onUpdate({ type: "error", error }); }};
Listen to the MessageInput
submit event.
const handleSubmit = async (e) => { e.preventDefault(); if (!message.trim()) return;
const userMessage = message; setMessage(""); setMessages(prev => [...prev, { message: userMessage, timestamp: Date.now() }]);
try { if (streamingEnabled) { setIsStreaming(true);
const tempMessageId = `temp-${Date.now()}`; setMessages(prev => [...prev, { message: "Thinking...", type: "text", id: tempMessageId }]);
await sendMessage(userMessage, conversationId, { stream: true, onUpdate: (data) => { if (data.type === "response.agent_step") { // Update the temporary message with the agent step message setMessages(prev => prev.map(msg => msg.id === tempMessageId ? { ...msg, message: data.message } : msg ) ); } else if (data.type === "response.completed") { // Replace the temporary message with the final response setMessages(prev => prev.map(msg => msg.id === tempMessageId ? { ...data.response, id: data.id } : msg ) ); setIsStreaming(false); } else if (data.type === "error") { // Remove the temporary message on error setMessages(prev => prev.filter(msg => msg.id !== tempMessageId)); setIsStreaming(false); } } }); } else { const response = await sendMessage(userMessage, conversationId); setMessages(prev => [...prev, { ...response.response, id: response.id }]); } } catch (error) { console.error("Request error:", error); } };
Create a component to render the response. This component takes an Inconvo Response and renders it based on its type [text, chart, table]
.
import { BarChart, LineChart } from "react-chartkick";import "chartkick/chart.js";
const TableRenderer = ({ response }) => ( <table> <caption>{response.message}</caption> <thead> <tr> {response.table.head.map((h, i) => ( <th key={i}>{h}</th> ))} </tr> </thead> <tbody> {response.table.body.map((row, i) => ( <tr key={i}> {row.map((cell, j) => ( <td key={j}>{cell}</td> ))} </tr> ))} </tbody> </table>);
const ChartRenderer = ({ response }) => { const data = response.chart.data.map((item) => [item.label, item.value]);
const chartProps = { data, round: 2, thousands: ",", width: "400px" };
return ( <div className="chart-container"> <div>{response.message}</div> {response.chart.type === "bar" && <BarChart {...chartProps} />} {response.chart.type === "line" && <LineChart {...chartProps} />} {!["bar", "line"].includes(response.chart.type) && ( <div>Unsupported chart type</div> )} </div> );};
const MessageRenderer = ({ response }) => { if (!response || Object.keys(response).length === 0) { return <div>Send a message to see a response here</div>; }
switch (response.type) { case "text": return <div>{response.message}</div>; case "table": return <TableRenderer response={response} />; case "chart": return <ChartRenderer response={response} />; default: return <div>Unsupported response type</div>; }};
Finally, display the agent interface using the components we created above.
<div> <div style={{ textAlign: "center" }}> <button onClick={handleNewConversation} disabled={isCreatingConversation}> {isCreatingConversation ? "Creating..." : "New conversation"} </button> <button onClick={() => setStreamingEnabled(!streamingEnabled)} style={{ marginLeft: "10px" }} > {streamingEnabled ? "Disable" : "Enable"} Streaming </button> {conversationId && ( <div className="conversation-id">{conversationId}</div> )} </div>
<MessageList messages={messages} />
<MessageInput message={message} setMessage={setMessage} conversationId={conversationId} onSubmit={handleSubmit} isStreaming={isStreaming} /></div>