2025-03-31 06:22:00 +08:00
|
|
|
import { createClient } from '@/utils/supabase/client';
|
2025-03-30 14:48:57 +08:00
|
|
|
|
|
|
|
const API_URL = process.env.NEXT_PUBLIC_BACKEND_URL || '';
|
|
|
|
|
|
|
|
export type Project = {
|
|
|
|
id: string;
|
|
|
|
name: string;
|
|
|
|
description: string;
|
|
|
|
user_id: string;
|
|
|
|
created_at: string;
|
|
|
|
}
|
|
|
|
|
|
|
|
export type Thread = {
|
|
|
|
thread_id: string;
|
|
|
|
user_id: string | null;
|
2025-03-31 06:22:00 +08:00
|
|
|
project_id?: string | null;
|
2025-03-31 13:27:21 +08:00
|
|
|
messages: Message[];
|
2025-03-30 14:48:57 +08:00
|
|
|
created_at: string;
|
|
|
|
}
|
|
|
|
|
|
|
|
export type Message = {
|
2025-03-31 06:22:00 +08:00
|
|
|
role: string;
|
2025-03-30 14:48:57 +08:00
|
|
|
content: string;
|
|
|
|
}
|
|
|
|
|
|
|
|
export type AgentRun = {
|
|
|
|
id: string;
|
|
|
|
thread_id: string;
|
|
|
|
status: 'running' | 'completed' | 'stopped' | 'error';
|
|
|
|
started_at: string;
|
|
|
|
completed_at: string | null;
|
2025-03-31 13:27:21 +08:00
|
|
|
responses: Message[];
|
2025-03-30 14:48:57 +08:00
|
|
|
error: string | null;
|
|
|
|
}
|
|
|
|
|
2025-03-31 13:27:21 +08:00
|
|
|
export type ToolCall = {
|
|
|
|
name: string;
|
|
|
|
arguments: Record<string, unknown>;
|
|
|
|
}
|
|
|
|
|
2025-03-30 14:48:57 +08:00
|
|
|
// Project APIs
|
|
|
|
export const getProjects = async (): Promise<Project[]> => {
|
2025-03-31 06:22:00 +08:00
|
|
|
const supabase = createClient();
|
2025-03-30 14:48:57 +08:00
|
|
|
const { data, error } = await supabase
|
|
|
|
.from('projects')
|
|
|
|
.select('*');
|
|
|
|
|
|
|
|
if (error) throw error;
|
|
|
|
return data || [];
|
|
|
|
};
|
|
|
|
|
|
|
|
export const getProject = async (projectId: string): Promise<Project> => {
|
2025-03-31 06:22:00 +08:00
|
|
|
const supabase = createClient();
|
2025-03-30 14:48:57 +08:00
|
|
|
const { data, error } = await supabase
|
|
|
|
.from('projects')
|
|
|
|
.select('*')
|
|
|
|
.eq('project_id', projectId)
|
|
|
|
.single();
|
|
|
|
|
|
|
|
if (error) throw error;
|
|
|
|
return data;
|
|
|
|
};
|
|
|
|
|
|
|
|
export const createProject = async (projectData: { name: string; description: string }): Promise<Project> => {
|
2025-03-31 06:22:00 +08:00
|
|
|
const supabase = createClient();
|
2025-03-30 14:48:57 +08:00
|
|
|
const { data: userData, error: userError } = await supabase.auth.getUser();
|
|
|
|
|
|
|
|
if (userError) throw userError;
|
|
|
|
if (!userData.user) throw new Error('You must be logged in to create a project');
|
|
|
|
|
|
|
|
const { data, error } = await supabase
|
|
|
|
.from('projects')
|
|
|
|
.insert({
|
|
|
|
name: projectData.name,
|
|
|
|
description: projectData.description || null,
|
|
|
|
user_id: userData.user.id
|
|
|
|
})
|
|
|
|
.select()
|
|
|
|
.single();
|
|
|
|
|
|
|
|
if (error) throw error;
|
|
|
|
|
|
|
|
// Map the database response to our Project type
|
|
|
|
return {
|
|
|
|
id: data.project_id,
|
|
|
|
name: data.name,
|
|
|
|
description: data.description || '',
|
|
|
|
user_id: data.user_id,
|
|
|
|
created_at: data.created_at
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
export const updateProject = async (projectId: string, data: Partial<Project>): Promise<Project> => {
|
2025-03-31 06:22:00 +08:00
|
|
|
const supabase = createClient();
|
2025-03-30 14:48:57 +08:00
|
|
|
const { data: updatedData, error } = await supabase
|
|
|
|
.from('projects')
|
|
|
|
.update(data)
|
|
|
|
.eq('project_id', projectId)
|
|
|
|
.select()
|
|
|
|
.single();
|
|
|
|
|
|
|
|
if (error) throw error;
|
|
|
|
return updatedData;
|
|
|
|
};
|
|
|
|
|
|
|
|
export const deleteProject = async (projectId: string): Promise<void> => {
|
2025-03-31 06:22:00 +08:00
|
|
|
const supabase = createClient();
|
2025-03-30 14:48:57 +08:00
|
|
|
const { error } = await supabase
|
|
|
|
.from('projects')
|
|
|
|
.delete()
|
|
|
|
.eq('project_id', projectId);
|
|
|
|
|
|
|
|
if (error) throw error;
|
|
|
|
};
|
|
|
|
|
|
|
|
// Thread APIs
|
|
|
|
export const getThreads = async (projectId?: string): Promise<Thread[]> => {
|
2025-03-31 06:22:00 +08:00
|
|
|
const supabase = createClient();
|
2025-03-30 14:48:57 +08:00
|
|
|
let query = supabase.from('threads').select('*');
|
|
|
|
|
|
|
|
if (projectId) {
|
|
|
|
query = query.eq('project_id', projectId);
|
|
|
|
}
|
|
|
|
|
|
|
|
const { data, error } = await query;
|
|
|
|
|
|
|
|
if (error) throw error;
|
|
|
|
|
|
|
|
// Parse messages from JSON string
|
|
|
|
return (data || []).map(thread => ({
|
|
|
|
...thread,
|
|
|
|
messages: thread.messages ? JSON.parse(thread.messages) : []
|
|
|
|
}));
|
|
|
|
};
|
|
|
|
|
|
|
|
export const getThread = async (threadId: string): Promise<Thread> => {
|
2025-03-31 06:22:00 +08:00
|
|
|
const supabase = createClient();
|
2025-03-30 14:48:57 +08:00
|
|
|
const { data, error } = await supabase
|
|
|
|
.from('threads')
|
|
|
|
.select('*')
|
|
|
|
.eq('thread_id', threadId)
|
|
|
|
.single();
|
|
|
|
|
|
|
|
if (error) throw error;
|
|
|
|
|
|
|
|
return {
|
|
|
|
...data,
|
|
|
|
messages: data.messages ? JSON.parse(data.messages) : []
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
export const createThread = async (projectId?: string): Promise<Thread> => {
|
2025-03-31 06:22:00 +08:00
|
|
|
const supabase = createClient();
|
2025-03-30 14:48:57 +08:00
|
|
|
// Generate a random UUID for the thread
|
|
|
|
const threadId = crypto.randomUUID();
|
|
|
|
|
|
|
|
const { data: userData, error: userError } = await supabase.auth.getUser();
|
|
|
|
|
|
|
|
if (userError) throw userError;
|
|
|
|
|
|
|
|
const { data, error } = await supabase
|
|
|
|
.from('threads')
|
|
|
|
.insert({
|
|
|
|
thread_id: threadId,
|
|
|
|
project_id: projectId || null,
|
|
|
|
user_id: userData.user?.id || null,
|
|
|
|
messages: JSON.stringify([])
|
|
|
|
})
|
|
|
|
.select()
|
|
|
|
.single();
|
|
|
|
|
|
|
|
if (error) {
|
|
|
|
console.error('Error creating thread:', error);
|
|
|
|
throw new Error(`Error creating thread: ${error.message}`);
|
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
|
|
|
...data,
|
|
|
|
messages: []
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
export const addMessage = async (threadId: string, message: { role: string, content: string }): Promise<void> => {
|
2025-03-31 06:22:00 +08:00
|
|
|
const supabase = createClient();
|
2025-03-30 14:48:57 +08:00
|
|
|
// First, get the current thread messages
|
|
|
|
const { data: threadData, error: threadError } = await supabase
|
|
|
|
.from('threads')
|
|
|
|
.select('messages')
|
|
|
|
.eq('thread_id', threadId)
|
|
|
|
.single();
|
|
|
|
|
|
|
|
if (threadError) {
|
|
|
|
console.error('Error fetching thread messages:', threadError);
|
|
|
|
throw new Error(`Error adding message: ${threadError.message}`);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Parse existing messages
|
|
|
|
const existingMessages = threadData.messages ? JSON.parse(threadData.messages) : [];
|
|
|
|
|
|
|
|
// Add the new message
|
|
|
|
const updatedMessages = [...existingMessages, message];
|
|
|
|
|
|
|
|
// Update the thread with the new messages
|
|
|
|
const { error: updateError } = await supabase
|
|
|
|
.from('threads')
|
|
|
|
.update({
|
|
|
|
messages: JSON.stringify(updatedMessages)
|
|
|
|
})
|
|
|
|
.eq('thread_id', threadId);
|
|
|
|
|
|
|
|
if (updateError) {
|
|
|
|
console.error('Error updating thread messages:', updateError);
|
|
|
|
throw new Error(`Error adding message: ${updateError.message}`);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
export const getMessages = async (threadId: string, hideToolMsgs: boolean = false): Promise<Message[]> => {
|
2025-03-31 06:22:00 +08:00
|
|
|
const supabase = createClient();
|
2025-03-30 14:48:57 +08:00
|
|
|
const { data, error } = await supabase
|
|
|
|
.from('threads')
|
|
|
|
.select('messages')
|
|
|
|
.eq('thread_id', threadId)
|
|
|
|
.single();
|
|
|
|
|
|
|
|
if (error) {
|
|
|
|
console.error('Error fetching messages:', error);
|
|
|
|
throw new Error(`Error getting messages: ${error.message}`);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Parse messages from JSON string
|
|
|
|
const messages = data.messages ? JSON.parse(data.messages) : [];
|
|
|
|
|
|
|
|
// Filter out tool messages if requested
|
|
|
|
if (hideToolMsgs) {
|
|
|
|
return messages.filter((msg: Message) => msg.role !== 'tool');
|
|
|
|
}
|
|
|
|
|
|
|
|
return messages;
|
|
|
|
};
|
|
|
|
|
|
|
|
// Agent APIs
|
|
|
|
export const startAgent = async (threadId: string): Promise<{ agent_run_id: string }> => {
|
2025-03-31 06:22:00 +08:00
|
|
|
const supabase = createClient();
|
|
|
|
const { data: { session } } = await supabase.auth.getSession();
|
2025-03-30 14:48:57 +08:00
|
|
|
|
2025-03-31 06:22:00 +08:00
|
|
|
if (!session?.access_token) {
|
|
|
|
throw new Error('No access token available');
|
|
|
|
}
|
|
|
|
|
2025-03-30 14:48:57 +08:00
|
|
|
const response = await fetch(`${API_URL}/thread/${threadId}/agent/start`, {
|
|
|
|
method: 'POST',
|
2025-03-31 06:22:00 +08:00
|
|
|
headers: {
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
'Authorization': `Bearer ${session.access_token}`,
|
|
|
|
},
|
2025-03-30 14:48:57 +08:00
|
|
|
});
|
|
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
throw new Error(`Error starting agent: ${response.statusText}`);
|
|
|
|
}
|
|
|
|
|
|
|
|
return response.json();
|
|
|
|
};
|
|
|
|
|
|
|
|
export const stopAgent = async (agentRunId: string): Promise<void> => {
|
2025-03-31 06:22:00 +08:00
|
|
|
const supabase = createClient();
|
|
|
|
const { data: { session } } = await supabase.auth.getSession();
|
2025-03-30 14:48:57 +08:00
|
|
|
|
2025-03-31 06:22:00 +08:00
|
|
|
if (!session?.access_token) {
|
|
|
|
throw new Error('No access token available');
|
|
|
|
}
|
|
|
|
|
2025-03-30 14:48:57 +08:00
|
|
|
const response = await fetch(`${API_URL}/agent-run/${agentRunId}/stop`, {
|
|
|
|
method: 'POST',
|
2025-03-31 06:22:00 +08:00
|
|
|
headers: {
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
'Authorization': `Bearer ${session.access_token}`,
|
|
|
|
},
|
2025-03-30 14:48:57 +08:00
|
|
|
});
|
|
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
throw new Error(`Error stopping agent: ${response.statusText}`);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
export const getAgentStatus = async (agentRunId: string): Promise<AgentRun> => {
|
2025-03-31 06:22:00 +08:00
|
|
|
const supabase = createClient();
|
|
|
|
const { data: { session } } = await supabase.auth.getSession();
|
2025-03-30 14:48:57 +08:00
|
|
|
|
2025-03-31 06:22:00 +08:00
|
|
|
if (!session?.access_token) {
|
|
|
|
throw new Error('No access token available');
|
|
|
|
}
|
|
|
|
|
2025-03-30 14:48:57 +08:00
|
|
|
const response = await fetch(`${API_URL}/agent-run/${agentRunId}`, {
|
2025-03-31 06:22:00 +08:00
|
|
|
headers: {
|
|
|
|
'Authorization': `Bearer ${session.access_token}`,
|
|
|
|
},
|
2025-03-30 14:48:57 +08:00
|
|
|
});
|
|
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
throw new Error(`Error getting agent status: ${response.statusText}`);
|
|
|
|
}
|
|
|
|
|
|
|
|
return response.json();
|
|
|
|
};
|
|
|
|
|
|
|
|
export const getAgentRuns = async (threadId: string): Promise<AgentRun[]> => {
|
2025-03-31 06:22:00 +08:00
|
|
|
const supabase = createClient();
|
|
|
|
const { data: { session } } = await supabase.auth.getSession();
|
2025-03-30 14:48:57 +08:00
|
|
|
|
2025-03-31 06:22:00 +08:00
|
|
|
if (!session?.access_token) {
|
|
|
|
throw new Error('No access token available');
|
|
|
|
}
|
|
|
|
|
2025-03-30 14:48:57 +08:00
|
|
|
const response = await fetch(`${API_URL}/thread/${threadId}/agent-runs`, {
|
2025-03-31 06:22:00 +08:00
|
|
|
headers: {
|
|
|
|
'Authorization': `Bearer ${session.access_token}`,
|
|
|
|
},
|
2025-03-30 14:48:57 +08:00
|
|
|
});
|
|
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
throw new Error(`Error getting agent runs: ${response.statusText}`);
|
|
|
|
}
|
|
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
return data.agent_runs || [];
|
|
|
|
};
|
|
|
|
|
|
|
|
export const streamAgent = (agentRunId: string, callbacks: {
|
|
|
|
onMessage: (content: string) => void;
|
2025-03-31 13:27:21 +08:00
|
|
|
onToolCall: (name: string, args: Record<string, unknown>) => void;
|
|
|
|
onError: (error: Error | string) => void;
|
2025-03-30 14:48:57 +08:00
|
|
|
onClose: () => void;
|
|
|
|
}): () => void => {
|
|
|
|
let eventSourceInstance: EventSource | null = null;
|
|
|
|
let isClosing = false;
|
|
|
|
|
|
|
|
console.log(`[STREAM] Setting up stream for agent run ${agentRunId}`);
|
|
|
|
|
|
|
|
const setupStream = async () => {
|
|
|
|
try {
|
|
|
|
if (isClosing) {
|
|
|
|
console.log(`[STREAM] Already closing, not setting up stream for ${agentRunId}`);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2025-03-31 06:22:00 +08:00
|
|
|
const supabase = createClient();
|
|
|
|
const { data: { session } } = await supabase.auth.getSession();
|
|
|
|
|
|
|
|
if (!session?.access_token) {
|
2025-03-30 14:48:57 +08:00
|
|
|
console.error('[STREAM] No auth token available');
|
|
|
|
callbacks.onError(new Error('Authentication required'));
|
|
|
|
callbacks.onClose();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const url = new URL(`${API_URL}/agent-run/${agentRunId}/stream`);
|
2025-03-31 06:22:00 +08:00
|
|
|
url.searchParams.append('token', session.access_token);
|
2025-03-30 14:48:57 +08:00
|
|
|
|
|
|
|
console.log(`[STREAM] Creating EventSource for ${agentRunId}`);
|
|
|
|
eventSourceInstance = new EventSource(url.toString());
|
|
|
|
|
|
|
|
eventSourceInstance.onopen = () => {
|
|
|
|
console.log(`[STREAM] Connection opened for ${agentRunId}`);
|
|
|
|
};
|
|
|
|
|
|
|
|
eventSourceInstance.onmessage = (event) => {
|
|
|
|
try {
|
|
|
|
const rawData = event.data;
|
|
|
|
if (rawData.includes('"type":"ping"')) return;
|
|
|
|
|
|
|
|
// Log raw data for debugging
|
|
|
|
console.log(`[STREAM] Received data: ${rawData.substring(0, 100)}${rawData.length > 100 ? '...' : ''}`);
|
|
|
|
|
2025-04-01 13:47:32 +08:00
|
|
|
// Pass the raw data directly to onMessage for handling in the component
|
|
|
|
callbacks.onMessage(rawData);
|
2025-03-30 14:48:57 +08:00
|
|
|
|
2025-04-01 13:47:32 +08:00
|
|
|
// Try to parse for tool calls
|
|
|
|
try {
|
|
|
|
const data = JSON.parse(rawData);
|
|
|
|
if (data.content?.startsWith('data: ')) {
|
|
|
|
const innerJson = data.content.replace('data: ', '');
|
|
|
|
const innerData = JSON.parse(innerJson);
|
2025-03-30 14:48:57 +08:00
|
|
|
|
2025-04-01 13:47:32 +08:00
|
|
|
if (innerData.type === 'tool_call') {
|
|
|
|
callbacks.onToolCall(innerData.name || '', innerData.arguments || '');
|
2025-03-30 14:48:57 +08:00
|
|
|
}
|
|
|
|
}
|
2025-04-01 13:47:32 +08:00
|
|
|
} catch (parseError) {
|
|
|
|
// Ignore parsing errors for tool calls
|
|
|
|
console.debug('[STREAM] Could not parse tool call data:', parseError);
|
2025-03-30 14:48:57 +08:00
|
|
|
}
|
2025-04-01 13:47:32 +08:00
|
|
|
|
2025-03-30 14:48:57 +08:00
|
|
|
} catch (error) {
|
2025-04-01 13:47:32 +08:00
|
|
|
console.error(`[STREAM] Error handling message:`, error);
|
2025-03-31 13:27:21 +08:00
|
|
|
callbacks.onError(error instanceof Error ? error : String(error));
|
2025-03-30 14:48:57 +08:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2025-03-31 13:27:21 +08:00
|
|
|
eventSourceInstance.onerror = () => {
|
2025-03-30 14:48:57 +08:00
|
|
|
// EventSource errors are often just connection closures
|
|
|
|
// For clean closures (manual or completed), we don't need to log an error
|
|
|
|
if (isClosing) {
|
|
|
|
console.log(`[STREAM] EventSource closed as expected for ${agentRunId}`);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Only log as error for unexpected closures
|
|
|
|
console.log(`[STREAM] EventSource connection closed for ${agentRunId}`);
|
|
|
|
|
|
|
|
if (!isClosing) {
|
|
|
|
console.log(`[STREAM] Handling connection close for ${agentRunId}`);
|
|
|
|
|
|
|
|
// Close the connection
|
|
|
|
if (eventSourceInstance) {
|
|
|
|
eventSourceInstance.close();
|
|
|
|
eventSourceInstance = null;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Then notify error (once)
|
|
|
|
isClosing = true;
|
|
|
|
callbacks.onClose();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
console.error(`[STREAM] Error setting up stream:`, error);
|
|
|
|
|
|
|
|
if (!isClosing) {
|
|
|
|
isClosing = true;
|
2025-03-31 13:27:21 +08:00
|
|
|
callbacks.onError(error instanceof Error ? error : String(error));
|
2025-03-30 14:48:57 +08:00
|
|
|
callbacks.onClose();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
// Set up the stream once
|
|
|
|
setupStream();
|
|
|
|
|
|
|
|
// Return cleanup function
|
|
|
|
return () => {
|
|
|
|
console.log(`[STREAM] Manual cleanup called for ${agentRunId}`);
|
|
|
|
|
|
|
|
if (isClosing) {
|
|
|
|
console.log(`[STREAM] Already closing, ignoring duplicate cleanup for ${agentRunId}`);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
isClosing = true;
|
|
|
|
|
|
|
|
if (eventSourceInstance) {
|
|
|
|
console.log(`[STREAM] Manually closing EventSource for ${agentRunId}`);
|
|
|
|
eventSourceInstance.close();
|
|
|
|
eventSourceInstance = null;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
};
|