Skip to content

Integrate

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.

  1. 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:

    • An id which you can use when attaching feedback to an answer.
    • A 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);
    }
    }
    });
  2. 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>