Add AI Chat
This example adds streaming chat and document-backed RAG to an EAI App Template project.
The template's tenantId code field carries the EAI workspace ID. Keep this
technical field name unchanged.
1. Confirm Workspace And Workflow
eai login
eai workspace select <workspace-slug>
eai workflow status <workflow-key> --workspace <workspace-id>
Use the workflow ID from the platform workspace configuration. Store runtime values in local or deployment environment configuration, not in committed source.
2. Stream Chat From A Client Component
"use client";
import { useState } from "react";
import { useChat } from "@/hooks/useChat";
interface Message {
role: "user" | "assistant";
content: string;
}
export function ChatPanel({
workflowId,
tenantId,
}: {
workflowId: string;
tenantId: string;
}) {
const { stream } = useChat(workflowId, "chat", tenantId);
const [messages, setMessages] = useState<Message[]>([]);
const [input, setInput] = useState("");
const [conversationId] = useState(() => crypto.randomUUID());
async function handleSend(event: React.FormEvent) {
event.preventDefault();
const message = input.trim();
if (!message) return;
setInput("");
setMessages((current) => [...current, { role: "user", content: message }]);
const reader = await stream({
message,
conversationId,
params: {},
});
const decoder = new TextDecoder();
let assistantText = "";
const assistantIndex = messages.length + 1;
setMessages((current) => [...current, { role: "assistant", content: "" }]);
while (true) {
const { done, value } = await reader.read();
if (done) break;
assistantText += decoder.decode(value, { stream: true });
setMessages((current) =>
current.map((item, index) =>
index === assistantIndex ? { ...item, content: assistantText } : item,
),
);
}
}
return (
<form onSubmit={handleSend}>
<textarea
value={input}
onChange={(event) => setInput(event.target.value)}
/>
<button type="submit">Send</button>
{messages.map((message, index) => (
<p key={index}>
<strong>{message.role}:</strong> {message.content}
</p>
))}
</form>
);
}
The hook calls the app BFF and stream route. Browser code should not call model providers or PublicAPI directly.
3. Submit A Document For Classification
"use client";
import { useState } from "react";
import { useDocuments } from "@/hooks/useDocuments";
export function DocumentUploader({ tenantId }: { tenantId: string }) {
const { classify } = useDocuments(tenantId);
const [status, setStatus] = useState("");
async function handleUpload(event: React.ChangeEvent<HTMLInputElement>) {
const file = event.target.files?.[0];
if (!file) return;
setStatus("Submitting");
try {
const response = await classify([file], {
verticalKey: "business-docs",
workflowKey: "document-review",
});
if (!response.ok) {
setStatus(`Submission failed (${response.status})`);
return;
}
const queued = await response.json();
setStatus(`Queued: ${queued.jobId}`);
} catch {
setStatus("Submission failed");
}
}
return (
<label>
Classify business document
<input type="file" onChange={handleUpload} />
{status && <span>{status}</span>}
</label>
);
}
Use ResourceAPI file routes when a file is a property on a business resource. Use document upload, classification, and RAG routes when the platform should process document content.
This example requires a published classifier target for the named app/workflow
with the business-document-v1 lifecycle and ready private storage. It submits
once. Poll the returned job with getJobStatus, then read persisted results
with getRecord; a queued job does not prove classification completed. The
business lifecycle currently supports classification and storage, not RAG
indexing. Keep DAISY/Assess planning context for their existing indexed flows.
4. Verify From The CLI
eai chat send "What can you help with?" --workflow <workflow-id> --stage chat
eai docs classify ./sample.pdf --vertical-key <app-key> --workflow-key <workflow-key> --format json
eai publicapi get /v4/data/documents/jobs/<returned-job-id> --format json
Repeat only the job-status read while processing. Do not submit the file again.
Read the saved record after successful completion using the returned document ID
and storage_target=resourceapi; queue acceptance alone is not a result.
If a named command does not exist for a route you need, use eai publicapi only
after confirming the route is an authorized PublicAPI V4 surface.