"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( null ); const [input, setInput] = useState(""); const [isGenerating, setIsGenerating] = useState(false); const [error, setError] = useState(null); const [models, setModels] = useState([]); const [selectedModel, setSelectedModel] = useState(""); const [modelsLoading, setModelsLoading] = useState(true); const [modelsError, setModelsError] = useState(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 } >; } | null>(null); const [selectedAgentId, setSelectedAgentId] = useState(""); 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([]); 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([]); const client = useAuthClient(); const abortControllerRef = useRef(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 => { 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) => { 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; // 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; 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; if ( event?.payload && typeof event.payload === "object" && event.payload !== null ) { const payload = event.payload as Record; 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; 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; 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; 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; if ( event?.payload && typeof event.payload === "object" && event.payload !== null ) { const payload = event.payload as Record; 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; 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; 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; 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; 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; if ( choice?.delta && typeof choice.delta === "object" && choice.delta !== null ) { const delta = choice.delta as Record; 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((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 (
{/* Upload Notification */} {uploadNotification.show && (
{uploadNotification.type === "loading" && (
)} {uploadNotification.message} {uploadNotification.type !== "loading" && ( )}
)} {/* Header */}

Agent Session

{!agentsLoading && agents.length > 0 && (
{selectedAgentId && ( )}
)} {!agentsLoading && agents.length > 0 && ( )}
{/* Main Two-Column Layout */}
{/* Left Column - Configuration Panel */}

Settings

{/* Model Configuration */}

Model Configuration

{modelsError && (

{modelsError}

)}
{(selectedAgentId && agents.find(a => a.agent_id === selectedAgentId) ?.agent_config?.instructions) || "No agent selected"}

Instructions are set when creating an agent and cannot be changed.

{/* Agent Tools */}

Agent Tools

{selectedAgentConfig?.toolgroups && selectedAgentConfig.toolgroups.length > 0 ? ( selectedAgentConfig.toolgroups.map( ( toolgroup: | string | { name: string; args: Record }, 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 (
{displayIcon} {displayName}
{isRAGTool && toolArgs && toolArgs.vector_db_ids ? (
Vector Databases:
{Array.isArray(toolArgs.vector_db_ids) ? ( toolArgs.vector_db_ids.map( (dbId: string, idx: number) => ( {dbId} ) ) ) : ( {String(toolArgs.vector_db_ids)} )}
) : null} {!isRAGTool && toolArgs && Object.keys(toolArgs).length > 0 && (
Configuration: {" "} {Object.keys(toolArgs).length} parameter {Object.keys(toolArgs).length > 1 ? "s" : ""}
)}
); } ) ) : (

No tools configured

This agent only has text generation capabilities

)}

Tools are configured when creating an agent and provide additional capabilities like web search, math calculations, or RAG document retrieval.

{/* Right Column - Chat Interface */}
{error && (

{error}

)} {!agentsLoading && agents.length === 0 ? (
🦙

Create an Agent with Llama Stack

To get started, create your first agent. Each agent is configured with specific instructions, models, and tools to help you with different tasks.

) : ( setCurrentSession(prev => prev ? { ...prev, messages, updatedAt: Date.now() } : prev ) } onRAGFileUpload={handleRAGFileUpload} /> )}
{/* Create Agent Modal */} {showCreateAgent && (

Create New Agent

setNewAgentName(e.target.value)} placeholder="My Custom Agent" />