llama-stack-mirror/llama_stack/ui/app/chat-playground/page.tsx
Francisco Arceo 75fad445a6
Some checks failed
Integration Tests (Replay) / Integration Tests (, , , client=, vision=) (push) Failing after 2s
Test External Providers Installed via Module / test-external-providers-from-module (venv) (push) Has been skipped
Python Package Build Test / build (3.13) (push) Failing after 1s
Integration Auth Tests / test-matrix (oauth2_token) (push) Failing after 5s
Pre-commit / pre-commit (push) Failing after 3s
Unit Tests / unit-tests (3.12) (push) Failing after 1s
Vector IO Integration Tests / test-matrix (push) Failing after 5s
Test External API and Providers / test-external (venv) (push) Failing after 4s
Python Package Build Test / build (3.12) (push) Failing after 5s
Update ReadTheDocs / update-readthedocs (push) Failing after 2s
Unit Tests / unit-tests (3.13) (push) Failing after 5s
UI Tests / ui-tests (22) (push) Failing after 6s
SqlStore Integration Tests / test-postgres (3.12) (push) Failing after 12s
SqlStore Integration Tests / test-postgres (3.13) (push) Failing after 13s
feat(UI): Implementing File Upload and VectorDB Creation/Configuration in Playground (#3266)
2025-08-28 05:03:31 -06:00

1893 lines
65 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import { useState, useEffect, useCallback, useRef } from "react";
import { flushSync } from "react-dom";
import { Button } from "@/components/ui/button";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Card } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Trash2 } from "lucide-react";
import { Chat } from "@/components/chat-playground/chat";
import { type Message } from "@/components/chat-playground/chat-message";
import { VectorDBCreator } from "@/components/chat-playground/vector-db-creator";
import { useAuthClient } from "@/hooks/use-auth-client";
import type { Model } from "llama-stack-client/resources/models";
import type { TurnCreateParams } from "llama-stack-client/resources/agents/turn";
import {
SessionUtils,
type ChatSession,
} from "@/components/chat-playground/conversations";
import {
cleanMessageContent,
extractCleanText,
} from "@/lib/message-content-utils";
export default function ChatPlaygroundPage() {
const [currentSession, setCurrentSession] = useState<ChatSession | null>(
null
);
const [input, setInput] = useState("");
const [isGenerating, setIsGenerating] = useState(false);
const [error, setError] = useState<string | null>(null);
const [models, setModels] = useState<Model[]>([]);
const [selectedModel, setSelectedModel] = useState<string>("");
const [modelsLoading, setModelsLoading] = useState(true);
const [modelsError, setModelsError] = useState<string | null>(null);
const [agents, setAgents] = useState<
Array<{
agent_id: string;
agent_config?: {
agent_name?: string;
name?: string;
instructions?: string;
};
[key: string]: unknown;
}>
>([]);
const [selectedAgentConfig, setSelectedAgentConfig] = useState<{
toolgroups?: Array<
string | { name: string; args: Record<string, unknown> }
>;
} | null>(null);
const [selectedAgentId, setSelectedAgentId] = useState<string>("");
const [agentsLoading, setAgentsLoading] = useState(true);
const [showCreateAgent, setShowCreateAgent] = useState(false);
const [newAgentName, setNewAgentName] = useState("");
const [newAgentInstructions, setNewAgentInstructions] = useState(
"You are a helpful assistant."
);
const [selectedToolgroups, setSelectedToolgroups] = useState<string[]>([]);
const [availableToolgroups, setAvailableToolgroups] = useState<
Array<{
identifier: string;
provider_id: string;
type: string;
provider_resource_id?: string;
}>
>([]);
const [showCreateVectorDB, setShowCreateVectorDB] = useState(false);
const [availableVectorDBs, setAvailableVectorDBs] = useState<
Array<{
identifier: string;
vector_db_name?: string;
embedding_model: string;
}>
>([]);
const [uploadNotification, setUploadNotification] = useState<{
show: boolean;
message: string;
type: "success" | "error" | "loading";
}>({ show: false, message: "", type: "success" });
const [selectedVectorDBs, setSelectedVectorDBs] = useState<string[]>([]);
const client = useAuthClient();
const abortControllerRef = useRef<AbortController | null>(null);
const isModelsLoading = modelsLoading ?? true;
const loadAgentConfig = useCallback(
async (agentId: string) => {
try {
// try to load from cache first
const cachedConfig = SessionUtils.loadAgentConfig(agentId);
if (cachedConfig) {
setSelectedAgentConfig({
toolgroups: cachedConfig.toolgroups,
});
return;
}
const agentDetails = await client.agents.retrieve(agentId);
// cache config
SessionUtils.saveAgentConfig(agentId, {
...agentDetails.agent_config,
toolgroups: agentDetails.agent_config?.toolgroups,
});
setSelectedAgentConfig({
toolgroups: agentDetails.agent_config?.toolgroups,
});
} catch (error) {
console.error("Error loading agent config:", error);
setSelectedAgentConfig(null);
}
},
[client]
);
const createDefaultSession = useCallback(
async (agentId: string) => {
try {
const response = await client.agents.session.create(agentId, {
session_name: "Default Session",
});
const defaultSession: ChatSession = {
id: response.session_id,
name: "Default Session",
messages: [],
selectedModel: selectedModel, // use current selected model
systemMessage: "You are a helpful assistant.",
agentId,
createdAt: Date.now(),
updatedAt: Date.now(),
};
setCurrentSession(defaultSession);
SessionUtils.saveCurrentSessionId(defaultSession.id, agentId);
// cache entire session data
SessionUtils.saveSessionData(agentId, defaultSession);
} catch (error) {
console.error("Error creating default session:", error);
}
},
[client, selectedModel]
);
const loadSessionMessages = useCallback(
async (agentId: string, sessionId: string): Promise<Message[]> => {
try {
const session = await client.agents.session.retrieve(
agentId,
sessionId
);
if (!session || !session.turns || !Array.isArray(session.turns)) {
return [];
}
const messages: Message[] = [];
for (const turn of session.turns) {
if (turn.input_messages && Array.isArray(turn.input_messages)) {
for (const input of turn.input_messages) {
if (input.role === "user" && input.content) {
messages.push({
id: `${turn.turn_id}-user-${messages.length}`,
role: "user",
content:
typeof input.content === "string"
? input.content
: JSON.stringify(input.content),
createdAt: new Date(turn.started_at || Date.now()),
});
}
}
}
if (turn.output_message && turn.output_message.content) {
console.log("Raw message content:", turn.output_message.content);
console.log("Content type:", typeof turn.output_message.content);
const cleanContent = cleanMessageContent(
turn.output_message.content
);
messages.push({
id: `${turn.turn_id}-assistant-${messages.length}`,
role: "assistant",
content: cleanContent,
createdAt: new Date(
turn.completed_at || turn.started_at || Date.now()
),
});
}
}
return messages;
} catch (error) {
console.error("Error loading session messages:", error);
return [];
}
},
[client]
);
const loadAgentSessions = useCallback(
async (agentId: string) => {
try {
const response = await client.agents.session.list(agentId);
if (
response.data &&
Array.isArray(response.data) &&
response.data.length > 0
) {
// check for saved session ID for this agent
const savedSessionId = SessionUtils.loadCurrentSessionId(agentId);
// try to load cached agent session data first
if (savedSessionId) {
const cachedSession = SessionUtils.loadSessionData(
agentId,
savedSessionId
);
if (cachedSession) {
setCurrentSession(cachedSession);
SessionUtils.saveCurrentSessionId(cachedSession.id, agentId);
return;
}
console.log("📡 Cache miss, fetching session from API...");
}
let sessionToLoad = response.data[0] as {
session_id: string;
session_name?: string;
started_at?: string;
};
console.log(
"Default session to load (first in list):",
sessionToLoad.session_id
);
// try to find saved session id in available sessions
if (savedSessionId) {
const foundSession = response.data.find(
(s: { [key: string]: unknown }) =>
(s as { session_id: string }).session_id === savedSessionId
);
console.log("Found saved session in list:", foundSession);
if (foundSession) {
sessionToLoad = foundSession as {
session_id: string;
session_name?: string;
started_at?: string;
};
console.log(
"✅ Restored previously selected session:",
savedSessionId
);
} else {
console.log(
"❌ Previously selected session not found, using latest session"
);
}
} else {
console.log("❌ No saved session ID found, using latest session");
}
const messages = await loadSessionMessages(
agentId,
sessionToLoad.session_id
);
const session: ChatSession = {
id: sessionToLoad.session_id,
name: sessionToLoad.session_name || "Session",
messages,
selectedModel: selectedModel || "",
systemMessage: "You are a helpful assistant.",
agentId,
createdAt: sessionToLoad.started_at
? new Date(sessionToLoad.started_at).getTime()
: Date.now(),
updatedAt: Date.now(),
};
setCurrentSession(session);
console.log(`💾 Saving session ID for agent ${agentId}:`, session.id);
SessionUtils.saveCurrentSessionId(session.id, agentId);
// cache session data
SessionUtils.saveSessionData(agentId, session);
} else {
// no sessions, create a new one
await createDefaultSession(agentId);
}
} catch (error) {
console.error("Error loading agent sessions:", error);
// fallback to creating a new session
await createDefaultSession(agentId);
}
},
[client, loadSessionMessages, createDefaultSession, selectedModel]
);
useEffect(() => {
const fetchAgents = async () => {
try {
setAgentsLoading(true);
const agentList = await client.agents.list();
setAgents(
(agentList.data as Array<{
agent_id: string;
agent_config?: {
agent_name?: string;
name?: string;
instructions?: string;
};
[key: string]: unknown;
}>) || []
);
if (agentList.data && agentList.data.length > 0) {
// check if there's a previously selected agent
const savedAgentId = SessionUtils.loadCurrentAgentId();
let agentToSelect = agentList.data[0] as {
agent_id: string;
agent_config?: {
agent_name?: string;
name?: string;
instructions?: string;
};
[key: string]: unknown;
};
// if we have a saved agent ID, find it in the available agents
if (savedAgentId) {
const foundAgent = agentList.data.find(
(a: { [key: string]: unknown }) =>
(a as { agent_id: string }).agent_id === savedAgentId
);
if (foundAgent) {
agentToSelect = foundAgent as typeof agentToSelect;
} else {
console.log("Previously slelected agent not found:");
}
}
setSelectedAgentId(agentToSelect.agent_id);
SessionUtils.saveCurrentAgentId(agentToSelect.agent_id);
// load agent config immediately
await loadAgentConfig(agentToSelect.agent_id);
// Note: loadAgentSessions will be called after models are loaded
}
} catch (error) {
console.error("Error fetching agents:", error);
} finally {
setAgentsLoading(false);
}
};
fetchAgents();
const fetchToolgroups = async () => {
try {
const toolgroups = await client.toolgroups.list();
const toolGroupsArray = Array.isArray(toolgroups)
? toolgroups
: toolgroups &&
typeof toolgroups === "object" &&
"data" in toolgroups &&
Array.isArray((toolgroups as { data: unknown }).data)
? (
toolgroups as {
data: Array<{
identifier: string;
provider_id: string;
type: string;
provider_resource_id?: string;
}>;
}
).data
: [];
if (toolGroupsArray && Array.isArray(toolGroupsArray)) {
setAvailableToolgroups(toolGroupsArray);
} else {
console.error("Invalid toolgroups data format:", toolgroups);
}
} catch (error) {
console.error("Error fetching toolgroups:", error);
if (error instanceof Error) {
console.error("Error details:", {
name: error.name,
message: error.message,
stack: error.stack,
});
}
}
};
fetchToolgroups();
const fetchVectorDBs = async () => {
try {
const vectorDBs = await client.vectorDBs.list();
const vectorDBsArray = Array.isArray(vectorDBs) ? vectorDBs : [];
if (vectorDBsArray && Array.isArray(vectorDBsArray)) {
setAvailableVectorDBs(vectorDBsArray);
} else {
console.error("Invalid vector DBs data format:", vectorDBs);
}
} catch (error) {
console.error("Error fetching vector DBs:", error);
}
};
fetchVectorDBs();
}, [client, loadAgentSessions, loadAgentConfig]);
const createNewAgent = useCallback(
async (
name: string,
instructions: string,
model: string,
toolgroups: string[] = [],
vectorDBs: string[] = []
) => {
try {
const processedToolgroups = toolgroups.map(toolgroup => {
if (toolgroup === "builtin::rag" && vectorDBs.length > 0) {
return {
name: "builtin::rag/knowledge_search",
args: {
vector_db_ids: vectorDBs,
},
};
}
return toolgroup;
});
const agentConfig = {
model,
instructions,
name: name || undefined,
enable_session_persistence: true,
toolgroups:
processedToolgroups.length > 0 ? processedToolgroups : undefined,
};
const response = await client.agents.create({
agent_config: agentConfig,
});
const agentList = await client.agents.list();
setAgents(
(agentList.data as Array<{
agent_id: string;
agent_config?: {
agent_name?: string;
name?: string;
instructions?: string;
};
[key: string]: unknown;
}>) || []
);
setSelectedAgentId(response.agent_id);
await loadAgentConfig(response.agent_id);
await loadAgentSessions(response.agent_id);
return response.agent_id;
} catch (error) {
console.error("Error creating agent:", error);
throw error;
}
},
[client, loadAgentSessions, loadAgentConfig]
);
const handleVectorDBCreated = useCallback(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async (_vectorDbId: string) => {
setShowCreateVectorDB(false);
try {
const vectorDBs = await client.vectorDBs.list();
const vectorDBsArray = Array.isArray(vectorDBs) ? vectorDBs : [];
if (vectorDBsArray && Array.isArray(vectorDBsArray)) {
setAvailableVectorDBs(vectorDBsArray);
}
} catch (error) {
console.error("Error refreshing vector DBs:", error);
}
},
[client]
);
const deleteAgent = useCallback(
async (agentId: string) => {
if (
confirm(
"Are you sure you want to delete this agent? This action cannot be undone and will delete the agent and all its sessions."
)
) {
try {
// there's a known error where the delete API returns 500 even on success
try {
await client.agents.delete(agentId);
console.log("Agent deleted successfully");
} catch (deleteError) {
// log the error but don't re-throw - we know deletion succeeded
console.log(
"Agent delete API returned error (but deletion likely succeeded):",
deleteError
);
}
SessionUtils.clearAgentCache(agentId);
const agentList = await client.agents.list();
setAgents(
(agentList.data as Array<{
agent_id: string;
agent_config?: {
agent_name?: string;
name?: string;
instructions?: string;
};
[key: string]: unknown;
}>) || []
);
// if we delete current agent, switch to another
if (selectedAgentId === agentId) {
const remainingAgents = agentList.data?.filter(
(a: { [key: string]: unknown }) =>
(a as { agent_id: string }).agent_id !== agentId
);
if (remainingAgents && remainingAgents.length > 0) {
const newAgent = remainingAgents[0] as {
agent_id: string;
agent_config?: {
agent_name?: string;
name?: string;
instructions?: string;
};
[key: string]: unknown;
};
setSelectedAgentId(newAgent.agent_id);
SessionUtils.saveCurrentAgentId(newAgent.agent_id);
await loadAgentConfig(newAgent.agent_id);
await loadAgentSessions(newAgent.agent_id);
} else {
// no agents left
setSelectedAgentId("");
setCurrentSession(null);
setSelectedAgentConfig(null);
}
}
} catch (error) {
console.error("Error deleting agent:", error);
// check if this is known server bug where deletion succeeds but returns 500
// The error message will typically contain status codes or "Could not find agent"
const errorMessage =
error instanceof Error ? error.message : String(error);
const isKnownServerBug =
errorMessage.includes("500") ||
errorMessage.includes("Internal Server Error") ||
errorMessage.includes("Could not find agent") ||
errorMessage.includes("400");
if (isKnownServerBug) {
console.log(
"Agent deletion succeeded despite error, cleaning up UI"
);
SessionUtils.clearAgentCache(agentId);
try {
const agentList = await client.agents.list();
setAgents(
(agentList.data as Array<{
agent_id: string;
agent_config?: {
agent_name?: string;
name?: string;
instructions?: string;
};
[key: string]: unknown;
}>) || []
);
if (selectedAgentId === agentId) {
const remainingAgents = agentList.data?.filter(
(a: { [key: string]: unknown }) =>
(a as { agent_id: string }).agent_id !== agentId
);
if (remainingAgents && remainingAgents.length > 0) {
const newAgent = remainingAgents[0] as {
agent_id: string;
agent_config?: {
agent_name?: string;
name?: string;
instructions?: string;
};
[key: string]: unknown;
};
setSelectedAgentId(newAgent.agent_id);
SessionUtils.saveCurrentAgentId(newAgent.agent_id);
await loadAgentConfig(newAgent.agent_id);
await loadAgentSessions(newAgent.agent_id);
} else {
// no agents left
setSelectedAgentId("");
setCurrentSession(null);
setSelectedAgentConfig(null);
}
}
} catch (refreshError) {
console.error("Error refreshing agents list:", refreshError);
}
} else {
// show error that we don't know about to user
console.error("Unexpected error during agent deletion:", error);
if (error instanceof Error) {
alert(`Failed to delete agent: ${error.message}`);
}
}
}
}
},
[client, selectedAgentId, loadAgentConfig, loadAgentSessions]
);
const handleModelChange = useCallback((newModel: string) => {
setSelectedModel(newModel);
setCurrentSession(prev =>
prev
? {
...prev,
selectedModel: newModel,
updatedAt: Date.now(),
}
: prev
);
}, []);
useEffect(() => {
if (currentSession) {
SessionUtils.saveCurrentSessionId(
currentSession.id,
currentSession.agentId
);
// cache session data
SessionUtils.saveSessionData(currentSession.agentId, currentSession);
// only update selectedModel if the session has a valid model and it's different from current
if (
currentSession.selectedModel &&
currentSession.selectedModel !== selectedModel
) {
setSelectedModel(currentSession.selectedModel);
}
}
}, [currentSession, selectedModel]);
useEffect(() => {
const fetchModels = async () => {
try {
setModelsLoading(true);
setModelsError(null);
const modelList = await client.models.list();
// store all models (including embedding models for vector DB creation)
setModels(modelList);
// set default LLM model for chat
const llmModels = modelList.filter(model => model.model_type === "llm");
if (llmModels.length > 0) {
handleModelChange(llmModels[0].identifier);
}
} catch (err) {
console.error("Error fetching models:", err);
setModelsError("Failed to fetch available models");
} finally {
setModelsLoading(false);
}
};
fetchModels();
}, [client, handleModelChange]);
// load agent sessions after both agents and models are ready
useEffect(() => {
if (
selectedAgentId &&
!agentsLoading &&
!modelsLoading &&
selectedModel &&
!currentSession
) {
loadAgentSessions(selectedAgentId);
}
}, [
selectedAgentId,
agentsLoading,
modelsLoading,
selectedModel,
currentSession,
loadAgentSessions,
]);
const handleInputChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
setInput(e.target.value);
};
const handleSubmit = async (event?: { preventDefault?: () => void }) => {
event?.preventDefault?.();
if (!input.trim()) return;
const userMessage: Message = {
id: Date.now().toString(),
role: "user",
content: input.trim(),
createdAt: new Date(),
};
setCurrentSession(prev => {
if (!prev) return prev;
const updatedSession = {
...prev,
messages: [...prev.messages, userMessage],
updatedAt: Date.now(),
};
// update cache with new message
SessionUtils.saveSessionData(prev.agentId, updatedSession);
return updatedSession;
});
setInput("");
await handleSubmitWithContent(userMessage.content);
};
const handleSubmitWithContent = async (content: string) => {
if (!currentSession || !selectedAgentId) return;
setIsGenerating(true);
setError(null);
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
const abortController = new AbortController();
abortControllerRef.current = abortController;
try {
const userMessage = {
role: "user" as const,
content,
};
const turnParams: TurnCreateParams = {
messages: [userMessage],
stream: true,
};
const response = await client.agents.turn.create(
selectedAgentId,
currentSession.id,
turnParams,
{
signal: abortController.signal,
timeout: 300000, // 5 minutes timeout for RAG queries
} as { signal: AbortSignal; timeout: number }
);
const assistantMessage: Message = {
id: (Date.now() + 1).toString(),
role: "assistant",
content: "",
createdAt: new Date(),
};
const processChunk = (
chunk: unknown
): { text: string | null; isToolCall: boolean } => {
const chunkObj = chunk as Record<string, unknown>;
// helper to check if content contains function call JSON
const containsToolCall = (content: string): boolean => {
return (
content.includes('"type": "function"') ||
content.includes('"name": "knowledge_search"') ||
content.includes('"parameters":') ||
!!content.match(/\{"type":\s*"function".*?\}/)
);
};
let isToolCall = false;
let potentialContent = "";
if (typeof chunk === "string") {
potentialContent = chunk;
isToolCall = containsToolCall(chunk);
}
if (
chunkObj?.delta &&
typeof chunkObj.delta === "object" &&
chunkObj.delta !== null
) {
const delta = chunkObj.delta as Record<string, unknown>;
if ("tool_calls" in delta) {
isToolCall = true;
}
if (typeof delta.text === "string") {
potentialContent = delta.text;
if (containsToolCall(delta.text)) {
isToolCall = true;
}
}
}
if (
chunkObj?.event &&
typeof chunkObj.event === "object" &&
chunkObj.event !== null
) {
const event = chunkObj.event as Record<string, unknown>;
if (
event?.payload &&
typeof event.payload === "object" &&
event.payload !== null
) {
const payload = event.payload as Record<string, unknown>;
if (typeof payload.content === "string") {
potentialContent = payload.content;
if (containsToolCall(payload.content)) {
isToolCall = true;
}
}
if (
payload?.delta &&
typeof payload.delta === "object" &&
payload.delta !== null
) {
const delta = payload.delta as Record<string, unknown>;
if (typeof delta.text === "string") {
potentialContent = delta.text;
if (containsToolCall(delta.text)) {
isToolCall = true;
}
}
}
}
if (
event?.delta &&
typeof event.delta === "object" &&
event.delta !== null
) {
const delta = event.delta as Record<string, unknown>;
if (typeof delta.text === "string") {
potentialContent = delta.text;
if (containsToolCall(delta.text)) {
isToolCall = true;
}
}
if (typeof delta.content === "string") {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
potentialContent = delta.content;
if (containsToolCall(delta.content)) {
isToolCall = true;
}
}
}
}
// if it's a tool call, skip it (don't display in chat)
if (isToolCall) {
return { text: null, isToolCall: true };
}
let text: string | null = null;
if (
chunkObj?.delta &&
typeof chunkObj.delta === "object" &&
chunkObj.delta !== null
) {
const delta = chunkObj.delta as Record<string, unknown>;
if (typeof delta.text === "string") {
text = extractCleanText(delta.text);
}
}
if (
!text &&
chunkObj?.event &&
typeof chunkObj.event === "object" &&
chunkObj.event !== null
) {
const event = chunkObj.event as Record<string, unknown>;
if (
event?.payload &&
typeof event.payload === "object" &&
event.payload !== null
) {
const payload = event.payload as Record<string, unknown>;
if (typeof payload.content === "string") {
text = extractCleanText(payload.content);
}
if (
!text &&
payload?.turn &&
typeof payload.turn === "object" &&
payload.turn !== null
) {
const turn = payload.turn as Record<string, unknown>;
if (
turn?.output_message &&
typeof turn.output_message === "object" &&
turn.output_message !== null
) {
const outputMessage = turn.output_message as Record<
string,
unknown
>;
if (typeof outputMessage.content === "string") {
text = extractCleanText(outputMessage.content);
}
}
if (
!text &&
turn?.steps &&
Array.isArray(turn.steps) &&
turn.steps.length > 0
) {
for (const step of turn.steps) {
if (step && typeof step === "object" && step !== null) {
const stepObj = step as Record<string, unknown>;
if (
stepObj?.model_response &&
typeof stepObj.model_response === "object" &&
stepObj.model_response !== null
) {
const modelResponse = stepObj.model_response as Record<
string,
unknown
>;
if (typeof modelResponse.content === "string") {
text = extractCleanText(modelResponse.content);
break;
}
}
}
}
}
}
if (
!text &&
payload?.delta &&
typeof payload.delta === "object" &&
payload.delta !== null
) {
const delta = payload.delta as Record<string, unknown>;
if (typeof delta.text === "string") {
text = extractCleanText(delta.text);
}
}
}
if (
!text &&
event?.delta &&
typeof event.delta === "object" &&
event.delta !== null
) {
const delta = event.delta as Record<string, unknown>;
if (typeof delta.text === "string") {
text = extractCleanText(delta.text);
}
if (!text && typeof delta.content === "string") {
text = extractCleanText(delta.content);
}
}
}
if (
!text &&
chunkObj?.choices &&
Array.isArray(chunkObj.choices) &&
chunkObj.choices.length > 0
) {
const choice = chunkObj.choices[0] as Record<string, unknown>;
if (
choice?.delta &&
typeof choice.delta === "object" &&
choice.delta !== null
) {
const delta = choice.delta as Record<string, unknown>;
if (typeof delta.content === "string") {
text = extractCleanText(delta.content);
}
}
}
if (!text && typeof chunk === "string") {
text = extractCleanText(chunk);
}
return { text, isToolCall: false };
};
setCurrentSession(prev => {
if (!prev) return null;
const updatedSession = {
...prev,
messages: [...prev.messages, assistantMessage],
updatedAt: Date.now(),
};
// update cache with assistant message
SessionUtils.saveSessionData(prev.agentId, updatedSession);
return updatedSession;
});
let fullContent = "";
for await (const chunk of response) {
const { text: deltaText } = processChunk(chunk);
// logging for debugging function calls
// if (deltaText && deltaText.includes("knowledge_search")) {
// console.log("🔍 Function call detected in text output:", deltaText);
// console.log("🔍 Original chunk:", JSON.stringify(chunk, null, 2));
// }
if (chunk && typeof chunk === "object" && "event" in chunk) {
const event = (
chunk as {
event: {
payload?: {
event_type?: string;
turn?: { output_message?: { content?: string } };
};
};
}
).event;
if (event?.payload?.event_type === "turn_complete") {
const content = event?.payload?.turn?.output_message?.content;
if (content && content.includes("knowledge_search")) {
console.log("🔍 Function call found in turn_complete:", content);
}
}
}
if (deltaText) {
fullContent += deltaText;
flushSync(() => {
setCurrentSession(prev => {
if (!prev) return null;
const newMessages = [...prev.messages];
const last = newMessages[newMessages.length - 1];
if (last.role === "assistant") {
last.content = fullContent;
}
const updatedSession = {
...prev,
messages: newMessages,
updatedAt: Date.now(),
};
// update cache with streaming content
if (fullContent.length % 100 === 0) {
// Only cache every 100 characters
SessionUtils.saveSessionData(prev.agentId, updatedSession);
}
return updatedSession;
});
});
}
}
} catch (err) {
if (err instanceof Error && err.name === "AbortError") {
console.log("Request aborted");
return;
}
console.error("Error sending message:", err);
setError("Failed to send message. Please try again.");
setCurrentSession(prev =>
prev
? {
...prev,
messages: prev.messages.slice(0, -1),
updatedAt: Date.now(),
}
: prev
);
} finally {
setIsGenerating(false);
abortControllerRef.current = null;
// cache final session state after streaming completes
setCurrentSession(prev => {
if (prev) {
SessionUtils.saveSessionData(prev.agentId, prev);
}
return prev;
});
}
};
const suggestions = [
"Write a Python function that prints 'Hello, World!'",
"Explain step-by-step how to solve this math problem: If x² + 6x + 9 = 25, what is x?",
"Design a simple algorithm to find the longest palindrome in a string.",
];
const append = (message: { role: "user"; content: string }) => {
const newMessage: Message = {
id: Date.now().toString(),
role: message.role,
content: message.content,
createdAt: new Date(),
};
setCurrentSession(prev =>
prev
? {
...prev,
messages: [...prev.messages, newMessage],
updatedAt: Date.now(),
}
: prev
);
handleSubmitWithContent(newMessage.content);
};
const clearChat = () => {
if (abortControllerRef.current) {
abortControllerRef.current.abort();
abortControllerRef.current = null;
setIsGenerating(false);
}
setCurrentSession(prev =>
prev ? { ...prev, messages: [], updatedAt: Date.now() } : prev
);
setError(null);
};
const handleRAGFileUpload = async (file: File) => {
if (!selectedAgentConfig?.toolgroups || !selectedAgentId) {
setError("No agent selected or agent has no RAG tools configured");
return;
}
// find RAG toolgroups that have vector_db_ids configured
const ragToolgroups = selectedAgentConfig.toolgroups.filter(toolgroup => {
if (typeof toolgroup === "object" && toolgroup.name?.includes("rag")) {
return toolgroup.args && "vector_db_ids" in toolgroup.args;
}
return false;
});
if (ragToolgroups.length === 0) {
setError("Current agent has no vector databases configured for RAG");
return;
}
try {
setError(null);
console.log("Uploading file using RAG tool...");
setUploadNotification({
show: true,
message: `📄 Uploading and indexing "${file.name}"...`,
type: "loading",
});
const vectorDbIds = ragToolgroups.flatMap(toolgroup => {
if (
typeof toolgroup === "object" &&
toolgroup.args &&
"vector_db_ids" in toolgroup.args
) {
return toolgroup.args.vector_db_ids as string[];
}
return [];
});
// determine mime type from file extension - this should be in the Llama Stack Client IMO
const getContentType = (filename: string): string => {
const ext = filename.toLowerCase().split(".").pop();
switch (ext) {
case "pdf":
return "application/pdf";
case "txt":
return "text/plain";
case "md":
return "text/markdown";
case "html":
return "text/html";
case "csv":
return "text/csv";
case "json":
return "application/json";
case "docx":
return "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
case "doc":
return "application/msword";
default:
return "application/octet-stream";
}
};
const mimeType = getContentType(file.name);
let fileContent: string;
// handle text files vs binary files differently
const isTextFile =
mimeType.startsWith("text/") ||
mimeType === "application/json" ||
mimeType === "text/markdown" ||
mimeType === "text/html" ||
mimeType === "text/csv";
if (isTextFile) {
fileContent = await file.text();
} else {
// for PDFs and other binary files, create a data URL
// use FileReader for efficient base64 conversion
fileContent = await new Promise<string>((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result as string);
reader.onerror = () => reject(reader.error);
reader.readAsDataURL(file);
});
}
for (const vectorDbId of vectorDbIds) {
await client.toolRuntime.ragTool.insert({
documents: [
{
content: fileContent,
document_id: `${file.name}-${Date.now()}`,
metadata: {
filename: file.name,
file_size: file.size,
uploaded_at: new Date().toISOString(),
agent_id: selectedAgentId,
},
mime_type: mimeType,
},
],
vector_db_id: vectorDbId,
// TODO: parameterize this somewhere, probably in settings
chunk_size_in_tokens: 512,
});
}
console.log("✅ File successfully uploaded using RAG tool");
setUploadNotification({
show: true,
message: `📄 File "${file.name}" uploaded and indexed successfully!`,
type: "success",
});
setTimeout(() => {
setUploadNotification(prev => ({ ...prev, show: false }));
}, 4000);
} catch (err) {
console.error("Error uploading file using RAG tool:", err);
const errorMessage =
err instanceof Error
? `Failed to upload file: ${err.message}`
: "Failed to upload file using RAG tool";
setUploadNotification({
show: true,
message: errorMessage,
type: "error",
});
setTimeout(() => {
setUploadNotification(prev => ({ ...prev, show: false }));
}, 6000);
}
};
return (
<div className="flex flex-col h-full w-full max-w-7xl mx-auto">
{/* Upload Notification */}
{uploadNotification.show && (
<div
className={`fixed top-4 right-4 z-50 p-4 rounded-lg shadow-lg transition-all duration-300 ${
uploadNotification.type === "success"
? "bg-green-100 border border-green-300 text-green-800"
: uploadNotification.type === "error"
? "bg-red-100 border border-red-300 text-red-800"
: "bg-blue-100 border border-blue-300 text-blue-800"
}`}
>
<div className="flex items-center gap-2">
{uploadNotification.type === "loading" && (
<div className="animate-spin rounded-full h-4 w-4 border-2 border-blue-600 border-t-transparent"></div>
)}
<span className="text-sm font-medium">
{uploadNotification.message}
</span>
{uploadNotification.type !== "loading" && (
<button
onClick={() =>
setUploadNotification(prev => ({ ...prev, show: false }))
}
className="ml-2 text-gray-400 hover:text-gray-600"
>
</button>
)}
</div>
</div>
)}
{/* Header */}
<div className="mb-6">
<div className="flex justify-between items-center mb-4">
<h1 className="text-3xl font-bold">Agent Session</h1>
<div className="flex items-center gap-3">
{!agentsLoading && agents.length > 0 && (
<div className="flex items-center gap-2">
<label className="text-sm font-medium">Agent Session:</label>
<Select
value={selectedAgentId}
onValueChange={agentId => {
setSelectedAgentId(agentId);
SessionUtils.saveCurrentAgentId(agentId);
loadAgentConfig(agentId);
loadAgentSessions(agentId);
}}
disabled={agentsLoading}
>
<SelectTrigger className="w-[200px]">
<SelectValue
placeholder={
agentsLoading ? "Loading..." : "Select Agent Session"
}
/>
</SelectTrigger>
<SelectContent>
{agents.map(agent => (
<SelectItem key={agent.agent_id} value={agent.agent_id}>
{(() => {
if (
agent.agent_config &&
"name" in agent.agent_config &&
typeof agent.agent_config.name === "string"
) {
return agent.agent_config.name;
}
if (
agent.agent_config &&
"agent_name" in agent.agent_config &&
typeof agent.agent_config.agent_name === "string"
) {
return agent.agent_config.agent_name;
}
return `Agent ${agent.agent_id.slice(0, 8)}...`;
})()}
</SelectItem>
))}
</SelectContent>
</Select>
{selectedAgentId && (
<Button
onClick={() => deleteAgent(selectedAgentId)}
variant="outline"
size="sm"
className="text-destructive hover:text-destructive hover:bg-destructive/10"
title="Delete current agent"
>
<Trash2 className="h-3 w-3" />
</Button>
)}
</div>
)}
<Button
onClick={() => setShowCreateAgent(true)}
variant="outline"
size="sm"
>
+ New Agent
</Button>
{!agentsLoading && agents.length > 0 && (
<Button
variant="outline"
onClick={clearChat}
disabled={isGenerating}
>
Clear Chat
</Button>
)}
</div>
</div>
</div>
{/* Main Two-Column Layout */}
<div className="flex flex-1 gap-6 min-h-0 flex-col lg:flex-row">
{/* Left Column - Configuration Panel */}
<div className="w-full lg:w-80 lg:flex-shrink-0 space-y-6 p-4 border border-border rounded-lg bg-muted/30">
<h2 className="text-lg font-semibold border-b pb-2 text-left">
Settings
</h2>
{/* Model Configuration */}
<div className="space-y-4 text-left">
<h3 className="text-lg font-semibold border-b pb-2 text-left">
Model Configuration
</h3>
<div className="space-y-3">
<div>
<label className="text-sm font-medium block mb-2">Model</label>
<Select
value={selectedModel}
onValueChange={handleModelChange}
disabled={isModelsLoading || isGenerating}
>
<SelectTrigger className="w-full">
<SelectValue
placeholder={
isModelsLoading ? "Loading..." : "Select Model"
}
/>
</SelectTrigger>
<SelectContent>
{models
.filter(model => model.model_type === "llm")
.map(model => (
<SelectItem
key={model.identifier}
value={model.identifier}
>
{model.identifier}
</SelectItem>
))}
</SelectContent>
</Select>
{modelsError && (
<p className="text-destructive text-xs mt-1">{modelsError}</p>
)}
</div>
<div>
<label className="text-sm font-medium block mb-2">
Agent Instructions
</label>
<div className="w-full h-24 px-3 py-2 text-sm border border-input rounded-md bg-muted text-muted-foreground">
{(selectedAgentId &&
agents.find(a => a.agent_id === selectedAgentId)
?.agent_config?.instructions) ||
"No agent selected"}
</div>
<p className="text-xs text-muted-foreground mt-1">
Instructions are set when creating an agent and cannot be
changed.
</p>
</div>
</div>
</div>
{/* Agent Tools */}
<div className="space-y-4 text-left">
<h3 className="text-lg font-semibold border-b pb-2 text-left">
Agent Tools
</h3>
<div className="space-y-3">
<div>
<label className="text-sm font-medium block mb-2 text-muted-foreground">
Configured Tools (Coming Soon)
</label>
<div className="space-y-2">
{selectedAgentConfig?.toolgroups &&
selectedAgentConfig.toolgroups.length > 0 ? (
selectedAgentConfig.toolgroups.map(
(
toolgroup:
| string
| { name: string; args: Record<string, unknown> },
index: number
) => {
const toolName =
typeof toolgroup === "string"
? toolgroup
: toolgroup.name;
const toolArgs =
typeof toolgroup === "object" ? toolgroup.args : null;
const isRAGTool = toolName.includes("rag");
const displayName = isRAGTool ? "RAG Search" : toolName;
const displayIcon = isRAGTool
? "🔍"
: toolName.includes("search")
? "🌐"
: "🔧";
return (
<div
key={index}
className="p-3 border border-input rounded-md bg-muted text-muted-foreground"
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="text-sm">{displayIcon}</span>
<span className="text-sm font-medium text-primary">
{displayName}
</span>
</div>
</div>
{isRAGTool && toolArgs && toolArgs.vector_db_ids ? (
<div className="mt-2 text-xs text-muted-foreground">
<span className="font-medium">
Vector Databases:
</span>
<div className="mt-1 flex flex-wrap gap-1">
{Array.isArray(toolArgs.vector_db_ids) ? (
toolArgs.vector_db_ids.map(
(dbId: string, idx: number) => (
<code
key={idx}
className="px-1.5 py-0.5 bg-muted-foreground/10 rounded text-xs"
>
{dbId}
</code>
)
)
) : (
<code className="px-1.5 py-0.5 bg-muted-foreground/10 rounded text-xs">
{String(toolArgs.vector_db_ids)}
</code>
)}
</div>
</div>
) : null}
{!isRAGTool &&
toolArgs &&
Object.keys(toolArgs).length > 0 && (
<div className="mt-2 text-xs text-muted-foreground">
<span className="font-medium">
Configuration:
</span>{" "}
{Object.keys(toolArgs).length} parameter
{Object.keys(toolArgs).length > 1 ? "s" : ""}
</div>
)}
</div>
);
}
)
) : (
<div className="p-3 border border-input rounded-md bg-muted text-center">
<p className="text-sm text-muted-foreground">
No tools configured
</p>
<p className="text-xs text-muted-foreground mt-1">
This agent only has text generation capabilities
</p>
</div>
)}
</div>
<p className="text-xs text-muted-foreground mt-2">
Tools are configured when creating an agent and provide
additional capabilities like web search, math calculations, or
RAG document retrieval.
</p>
</div>
</div>
</div>
</div>
{/* Right Column - Chat Interface */}
<div className="flex-1 flex flex-col min-h-0 p-4 border border-border rounded-lg bg-background">
{error && (
<div className="mb-4 p-3 bg-destructive/10 border border-destructive/20 rounded-md">
<p className="text-destructive text-sm">{error}</p>
</div>
)}
{!agentsLoading && agents.length === 0 ? (
<div className="flex-1 flex items-center justify-center">
<div className="text-center space-y-4 max-w-md">
<div className="text-6xl mb-4">🦙</div>
<h2 className="text-2xl font-semibold text-muted-foreground">
Create an Agent with Llama Stack
</h2>
<p className="text-muted-foreground">
To get started, create your first agent. Each agent is
configured with specific instructions, models, and tools to
help you with different tasks.
</p>
<Button
onClick={() => setShowCreateAgent(true)}
size="lg"
className="mt-4"
>
Create Your First Agent
</Button>
</div>
</div>
) : (
<Chat
className="flex-1"
messages={currentSession?.messages || []}
handleSubmit={handleSubmit}
input={input}
handleInputChange={handleInputChange}
isGenerating={isGenerating}
append={append}
suggestions={suggestions}
setMessages={messages =>
setCurrentSession(prev =>
prev ? { ...prev, messages, updatedAt: Date.now() } : prev
)
}
onRAGFileUpload={handleRAGFileUpload}
/>
)}
</div>
</div>
{/* Create Agent Modal */}
{showCreateAgent && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<Card className="w-[500px] p-6 space-y-4">
<h3 className="text-lg font-semibold">Create New Agent</h3>
<div className="space-y-4">
<div>
<label className="text-sm font-medium block mb-2">
Agent Name (optional)
</label>
<Input
value={newAgentName}
onChange={e => setNewAgentName(e.target.value)}
placeholder="My Custom Agent"
/>
</div>
<div>
<label className="text-sm font-medium block mb-2">Model</label>
<Select value={selectedModel} onValueChange={setSelectedModel}>
<SelectTrigger>
<SelectValue placeholder="Select Model" />
</SelectTrigger>
<SelectContent>
{models
.filter(model => model.model_type === "llm")
.map(model => (
<SelectItem
key={model.identifier}
value={model.identifier}
>
{model.identifier}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<label className="text-sm font-medium block mb-2">
System Instructions
</label>
<textarea
value={newAgentInstructions}
onChange={e => setNewAgentInstructions(e.target.value)}
placeholder="You are a helpful assistant."
className="w-full h-32 px-3 py-2 text-sm border border-input rounded-md resize-none focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
/>
</div>
<div>
<label className="text-sm font-medium block mb-2">
Tools (optional)
</label>
<label className="text-sm font-small block mb-2">
NOTE: Tools are not yet implemented
</label>
<p className="text-xs text-muted-foreground mb-2">
Available toolgroups: {availableToolgroups.length} found
</p>
<div className="space-y-2">
{availableToolgroups.length === 0 ? (
<p className="text-sm text-muted-foreground">
Loading toolgroups...
</p>
) : (
availableToolgroups.map(toolgroup => (
<label
key={toolgroup.identifier}
className="flex items-center space-x-2"
>
<input
type="checkbox"
checked={selectedToolgroups.includes(
toolgroup.identifier
)}
onChange={e => {
if (e.target.checked) {
setSelectedToolgroups(prev => {
const newSelection = [
...prev,
toolgroup.identifier,
];
return newSelection;
});
} else {
setSelectedToolgroups(prev => {
const newSelection = prev.filter(
id => id !== toolgroup.identifier
);
return newSelection;
});
}
}}
className="rounded border-input"
/>
<span className="text-sm">
<code className="bg-muted px-1 rounded text-xs">
{toolgroup.identifier}
</code>
<span className="text-muted-foreground ml-2">
({toolgroup.provider_id})
</span>
</span>
</label>
))
)}
</div>
{selectedToolgroups.length === 0 && (
<p className="text-xs text-muted-foreground mt-1">
No tools selected - agent will only have text generation
capabilities.
</p>
)}
<p className="text-xs text-muted-foreground mt-2 p-2 bg-muted/50 border border-border rounded">
<strong>Note:</strong> Selected tools will be configured for
the agent. Some tools like RAG may require additional vector
DB configuration, and web search tools need API keys. Basic
text generation agents work without tools.
</p>
</div>
{/* Vector DB Configuration for RAG */}
{selectedToolgroups.includes("builtin::rag") && (
<div>
<label className="text-sm font-medium block mb-2">
Vector Databases for RAG
</label>
<div className="flex items-center gap-2 mb-2">
<Button
type="button"
variant="outline"
size="sm"
onClick={() => setShowCreateVectorDB(true)}
>
+ Create Vector DB
</Button>
<span className="text-xs text-muted-foreground">
{availableVectorDBs.length} available
</span>
</div>
<div className="space-y-2 max-h-32 overflow-y-auto">
{availableVectorDBs.length === 0 ? (
<p className="text-sm text-muted-foreground">
No vector databases available. Create one to use RAG
tools.
</p>
) : (
availableVectorDBs.map(vectorDB => (
<label
key={vectorDB.identifier}
className="flex items-center space-x-2"
>
<input
type="checkbox"
checked={selectedVectorDBs.includes(
vectorDB.identifier
)}
onChange={e => {
if (e.target.checked) {
setSelectedVectorDBs(prev => [
...prev,
vectorDB.identifier,
]);
} else {
setSelectedVectorDBs(prev =>
prev.filter(id => id !== vectorDB.identifier)
);
}
}}
className="rounded border-input"
/>
<span className="text-sm">
<code className="bg-muted px-1 rounded text-xs">
{vectorDB.identifier}
</code>
{vectorDB.vector_db_name && (
<span className="text-muted-foreground ml-2">
({vectorDB.vector_db_name})
</span>
)}
</span>
</label>
))
)}
</div>
{selectedVectorDBs.length === 0 &&
selectedToolgroups.includes("builtin::rag") && (
<p className="text-xs text-muted-foreground mt-1">
RAG tool selected but no vector databases chosen.
Create or select a vector database.
</p>
)}
</div>
)}
</div>
<div className="flex gap-2 pt-4">
<Button
onClick={async () => {
try {
await createNewAgent(
newAgentName,
newAgentInstructions,
selectedModel,
selectedToolgroups,
selectedVectorDBs
);
setShowCreateAgent(false);
setNewAgentName("");
setNewAgentInstructions("You are a helpful assistant.");
setSelectedToolgroups([]);
setSelectedVectorDBs([]);
} catch (error) {
console.error("Failed to create agent:", error);
}
}}
className="flex-1"
disabled={!selectedModel || !newAgentInstructions.trim()}
>
Create Agent
</Button>
<Button
variant="outline"
onClick={() => {
setShowCreateAgent(false);
setNewAgentName("");
setNewAgentInstructions("You are a helpful assistant.");
setSelectedToolgroups([]);
setSelectedVectorDBs([]);
}}
className="flex-1"
>
Cancel
</Button>
</div>
</Card>
</div>
)}
{/* Create Vector DB Modal */}
{showCreateVectorDB && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<VectorDBCreator
models={models}
onVectorDBCreated={handleVectorDBCreated}
onCancel={() => setShowCreateVectorDB(false)}
/>
</div>
)}
</div>
);
}