Merge pull request #60 from kortix-ai/public-replay

Public replay
This commit is contained in:
Adam Cohen Hillel 2025-04-20 18:45:59 +01:00 committed by GitHub
commit c1d3071392
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 1924 additions and 69 deletions

View File

@ -126,7 +126,7 @@ You'll need the following components:
- Generate an API key from your account settings
- Go to [Images](https://app.daytona.io/dashboard/images)
- Click "Add Image"
- Enter `adamcohenhillel/kortix-suna:0.0.13` as the image name
- Enter `adamcohenhillel/kortix-suna:0.0.16` as the image name
- Set `exec /usr/bin/supervisord -n -c /etc/supervisor/conf.d/supervisord.conf` as the Entrypoint
4. **LLM API Keys**:
@ -251,3 +251,7 @@ python api.py
## License
Kortix Suna is licensed under the Apache License, Version 2.0. See [LICENSE](./LICENSE) for the full license text.
## Co-Creators:
Adam Cohen Hillel, Marko Kraemer, Dom

View File

@ -342,22 +342,21 @@ class BrowserAutomation:
self.browser = await playwright.chromium.launch(**launch_options)
print("Browser launched with minimal options")
print("Creating new page...")
try:
await self.get_current_page()
print("Found existing page, using it")
self.current_page_index = 0
except Exception as page_error:
print(f"Error finding existing page, creating new one. ( {page_error})")
page = await self.browser.new_page()
print("New page created successfully")
self.pages.append(page)
self.current_page_index = 0
# Navigate to about:blank to ensure page is ready
await page.goto("about:blank", timeout=30000)
print("Navigated to about:blank")
# await page.goto("google.com", timeout=30000)
print("Navigated to google.com")
print("Browser initialization completed successfully")
except Exception as page_error:
print(f"Error creating page: {page_error}")
traceback.print_exc()
raise RuntimeError(f"Failed to initialize browser page: {page_error}")
except Exception as e:
print(f"Browser startup error: {str(e)}")
traceback.print_exc()

View File

@ -6,7 +6,7 @@ services:
dockerfile: ${DOCKERFILE:-Dockerfile}
args:
TARGETPLATFORM: ${TARGETPLATFORM:-linux/amd64}
image: adamcohenhillel/kortix-suna:0.0.13
image: adamcohenhillel/kortix-suna:0.0.16
ports:
- "6080:6080" # noVNC web interface
- "5901:5901" # VNC port

View File

@ -96,7 +96,7 @@ def create_sandbox(password: str):
logger.debug("OPENAI_API_KEY configured for sandbox")
sandbox = daytona.create(CreateSandboxParams(
image="adamcohenhillel/kortix-suna:0.0.14",
image="adamcohenhillel/kortix-suna:0.0.16",
public=False,
env_vars={
"CHROME_PERSISTENT_SESSION": "true",

View File

@ -268,7 +268,7 @@ select exists(
);
$$;
grant execute on function basejump.has_role_on_account(uuid, basejump.account_role) to authenticated;
grant execute on function basejump.has_role_on_account(uuid, basejump.account_role) to authenticated, anon, public, service_role;
/**

View File

@ -15,6 +15,7 @@ CREATE TABLE threads (
thread_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
account_id UUID REFERENCES basejump.accounts(id) ON DELETE CASCADE,
project_id UUID REFERENCES projects(project_id) ON DELETE CASCADE,
is_public BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT TIMEZONE('utc'::text, NOW()) NOT NULL,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT TIMEZONE('utc'::text, NOW()) NOT NULL
);
@ -113,6 +114,7 @@ CREATE POLICY project_delete_policy ON projects
CREATE POLICY thread_select_policy ON threads
FOR SELECT
USING (
is_public = TRUE OR
basejump.has_role_on_account(account_id) = true OR
EXISTS (
SELECT 1 FROM projects
@ -163,6 +165,7 @@ CREATE POLICY agent_run_select_policy ON agent_runs
LEFT JOIN projects ON threads.project_id = projects.project_id
WHERE threads.thread_id = agent_runs.thread_id
AND (
threads.is_public = TRUE OR
basejump.has_role_on_account(threads.account_id) = true OR
basejump.has_role_on_account(projects.account_id) = true
)
@ -220,6 +223,7 @@ CREATE POLICY message_select_policy ON messages
LEFT JOIN projects ON threads.project_id = projects.project_id
WHERE threads.thread_id = messages.thread_id
AND (
threads.is_public = TRUE OR
basejump.has_role_on_account(threads.account_id) = true OR
basejump.has_role_on_account(projects.account_id) = true
)
@ -270,8 +274,8 @@ CREATE POLICY message_delete_policy ON messages
-- Grant permissions to roles
GRANT ALL PRIVILEGES ON TABLE projects TO authenticated, service_role;
GRANT ALL PRIVILEGES ON TABLE threads TO authenticated, service_role;
GRANT ALL PRIVILEGES ON TABLE messages TO authenticated, service_role;
GRANT SELECT ON TABLE threads TO authenticated, anon, service_role;
GRANT SELECT ON TABLE messages TO authenticated, anon, service_role;
GRANT ALL PRIVILEGES ON TABLE agent_runs TO authenticated, service_role;
-- Create a function that matches the Python get_messages behavior
@ -286,12 +290,18 @@ DECLARE
current_role TEXT;
latest_summary_id UUID;
latest_summary_time TIMESTAMP WITH TIME ZONE;
is_thread_public BOOLEAN;
BEGIN
-- Get current role
SELECT current_user INTO current_role;
-- Skip access check for service_role
IF current_role = 'authenticated' THEN
-- Check if thread is public
SELECT is_public INTO is_thread_public
FROM threads
WHERE thread_id = p_thread_id;
-- Skip access check for service_role or public threads
IF current_role = 'authenticated' AND NOT is_thread_public THEN
-- Check if thread exists and user has access
SELECT EXISTS (
SELECT 1 FROM threads t
@ -361,4 +371,4 @@ END;
$$;
-- Grant execute permissions
GRANT EXECUTE ON FUNCTION get_llm_formatted_messages TO authenticated, service_role;
GRANT EXECUTE ON FUNCTION get_llm_formatted_messages TO authenticated, anon, service_role;

View File

@ -128,6 +128,11 @@ async def verify_thread_access(client, thread_id: str, user_id: str):
raise HTTPException(status_code=404, detail="Thread not found")
thread_data = thread_result.data[0]
# Check if thread is public
if thread_data.get('is_public'):
return True
account_id = thread_data.get('account_id')
# When using service role, we need to manually check account membership instead of using current_user_account_role
if account_id:

View File

@ -0,0 +1,19 @@
import { Metadata } from 'next';
export const metadata: Metadata = {
title: 'Shared Conversation',
description: 'View a shared AI conversation',
openGraph: {
title: 'Shared AI Conversation',
description: 'View a shared AI conversation from Kortix Manus',
images: ['/kortix-logo.png'],
},
};
export default function ThreadLayout({
children,
}: {
children: React.ReactNode;
}) {
return <>{children}</>;
}

File diff suppressed because it is too large Load Diff

View File

@ -16,11 +16,10 @@ const apiCache = {
getThreads: (projectId: string) => apiCache.threads.get(projectId || 'all'),
setThreads: (projectId: string, data: any) => apiCache.threads.set(projectId || 'all', data),
invalidateThreads: (projectId: string) => apiCache.threads.delete(projectId || 'all'),
getThreadMessages: (threadId: string) => apiCache.threadMessages.get(threadId),
setThreadMessages: (threadId: string, data: any) => apiCache.threadMessages.set(threadId, data),
// Helper to clear parts of the cache when data changes
invalidateThreadMessages: (threadId: string) => apiCache.threadMessages.delete(threadId),
// Functions to clear all cache
@ -67,7 +66,8 @@ export type Project = {
export type Thread = {
thread_id: string;
account_id: string | null;
project_id: string | null;
project_id?: string | null;
is_public?: boolean;
created_at: string;
updated_at: string;
[key: string]: any; // Allow additional properties to handle database fields
@ -151,58 +151,70 @@ export const getProject = async (projectId: string): Promise<Project> => {
}
const supabase = createClient();
const { data, error } = await supabase
.from('projects')
.select('*')
.eq('project_id', projectId)
.single();
if (error) throw error;
try {
const { data, error } = await supabase
.from('projects')
.select('*')
.eq('project_id', projectId)
.single();
console.log('Raw project data from database:', data);
// If project has a sandbox, ensure it's started
if (data.sandbox?.id) {
try {
const { data: { session } } = await supabase.auth.getSession();
if (session?.access_token) {
console.log(`Ensuring sandbox is active for project ${projectId}...`);
const response = await fetch(`${API_URL}/project/${projectId}/sandbox/ensure-active`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${session.access_token}`,
'Content-Type': 'application/json',
},
});
if (!response.ok) {
const errorText = await response.text().catch(() => 'No error details available');
console.warn(`Failed to ensure sandbox is active: ${response.status} ${response.statusText}`, errorText);
} else {
console.log('Sandbox activation successful');
}
if (error) {
// Handle the specific "no rows returned" error from Supabase
if (error.code === 'PGRST116') {
throw new Error(`Project not found or not accessible: ${projectId}`);
}
} catch (sandboxError) {
console.warn('Failed to ensure sandbox is active:', sandboxError);
// Non-blocking error - continue with the project data
throw error;
}
console.log('Raw project data from database:', data);
// If project has a sandbox, ensure it's started
if (data.sandbox?.id) {
try {
const { data: { session } } = await supabase.auth.getSession();
if (session?.access_token) {
console.log(`Ensuring sandbox is active for project ${projectId}...`);
const response = await fetch(`${API_URL}/project/${projectId}/sandbox/ensure-active`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${session.access_token}`,
'Content-Type': 'application/json',
},
});
if (!response.ok) {
const errorText = await response.text().catch(() => 'No error details available');
console.warn(`Failed to ensure sandbox is active: ${response.status} ${response.statusText}`, errorText);
} else {
console.log('Sandbox activation successful');
}
}
} catch (sandboxError) {
console.warn('Failed to ensure sandbox is active:', sandboxError);
// Non-blocking error - continue with the project data
}
}
// Map database fields to our Project type
const mappedProject: Project = {
id: data.project_id,
name: data.name || '',
description: data.description || '',
account_id: data.account_id,
created_at: data.created_at,
sandbox: data.sandbox || { id: "", pass: "", vnc_preview: "", sandbox_url: "" }
};
console.log('Mapped project data for frontend:', mappedProject);
// Cache the result
apiCache.setProject(projectId, mappedProject);
return mappedProject;
} catch (error) {
console.error(`Error fetching project ${projectId}:`, error);
throw error;
}
// Map database fields to our Project type
const mappedProject: Project = {
id: data.project_id,
name: data.name || '',
description: data.description || '',
account_id: data.account_id,
created_at: data.created_at,
sandbox: data.sandbox || { id: "", pass: "", vnc_preview: "", sandbox_url: "" }
};
console.log('Mapped project data for frontend:', mappedProject);
// Cache the result
apiCache.setProject(projectId, mappedProject);
return mappedProject;
};
export const createProject = async (
@ -1003,3 +1015,35 @@ export const getSandboxFileContent = async (sandboxId: string, path: string): Pr
export const clearApiCache = () => {
apiCache.clearAll();
};
export const updateThread = async (threadId: string, data: Partial<Thread>): Promise<Thread> => {
const supabase = createClient();
// Format the data for update
const updateData = { ...data };
// Update the thread
const { data: updatedThread, error } = await supabase
.from('threads')
.update(updateData)
.eq('thread_id', threadId)
.select()
.single();
if (error) {
console.error('Error updating thread:', error);
throw new Error(`Error updating thread: ${error.message}`);
}
// Invalidate thread cache if we're updating thread data
if (updatedThread.project_id) {
apiCache.invalidateThreads(updatedThread.project_id);
}
apiCache.invalidateThreads('all');
return updatedThread;
};
export const toggleThreadPublicStatus = async (threadId: string, isPublic: boolean): Promise<Thread> => {
return updateThread(threadId, { is_public: isPublic });
};