|
|
|
|
|
CREATE TABLE projects ( |
|
project_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), |
|
name TEXT NOT NULL, |
|
description TEXT, |
|
account_id UUID NOT NULL REFERENCES basejump.accounts(id) ON DELETE CASCADE, |
|
sandbox JSONB DEFAULT '{}'::jsonb, |
|
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 |
|
); |
|
|
|
|
|
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 |
|
); |
|
|
|
|
|
CREATE TABLE messages ( |
|
message_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), |
|
thread_id UUID NOT NULL REFERENCES threads(thread_id) ON DELETE CASCADE, |
|
type TEXT NOT NULL, |
|
is_llm_message BOOLEAN NOT NULL DEFAULT TRUE, |
|
content JSONB NOT NULL, |
|
metadata JSONB DEFAULT '{}'::jsonb, |
|
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 |
|
); |
|
|
|
|
|
CREATE TABLE agent_runs ( |
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), |
|
thread_id UUID NOT NULL REFERENCES threads(thread_id), |
|
status TEXT NOT NULL DEFAULT 'running', |
|
started_at TIMESTAMP WITH TIME ZONE DEFAULT TIMEZONE('utc'::text, NOW()) NOT NULL, |
|
completed_at TIMESTAMP WITH TIME ZONE, |
|
responses JSONB NOT NULL DEFAULT '[]'::jsonb, |
|
error TEXT, |
|
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 |
|
); |
|
|
|
|
|
CREATE OR REPLACE FUNCTION update_updated_at_column() |
|
RETURNS TRIGGER AS $$ |
|
BEGIN |
|
NEW.updated_at = TIMEZONE('utc'::text, NOW()); |
|
RETURN NEW; |
|
END; |
|
$$ language 'plpgsql'; |
|
|
|
|
|
CREATE TRIGGER update_threads_updated_at |
|
BEFORE UPDATE ON threads |
|
FOR EACH ROW |
|
EXECUTE FUNCTION update_updated_at_column(); |
|
|
|
CREATE TRIGGER update_messages_updated_at |
|
BEFORE UPDATE ON messages |
|
FOR EACH ROW |
|
EXECUTE FUNCTION update_updated_at_column(); |
|
|
|
CREATE TRIGGER update_agent_runs_updated_at |
|
BEFORE UPDATE ON agent_runs |
|
FOR EACH ROW |
|
EXECUTE FUNCTION update_updated_at_column(); |
|
|
|
CREATE TRIGGER update_projects_updated_at |
|
BEFORE UPDATE ON projects |
|
FOR EACH ROW |
|
EXECUTE FUNCTION update_updated_at_column(); |
|
|
|
|
|
CREATE INDEX idx_threads_created_at ON threads(created_at); |
|
CREATE INDEX idx_threads_account_id ON threads(account_id); |
|
CREATE INDEX idx_threads_project_id ON threads(project_id); |
|
CREATE INDEX idx_agent_runs_thread_id ON agent_runs(thread_id); |
|
CREATE INDEX idx_agent_runs_status ON agent_runs(status); |
|
CREATE INDEX idx_agent_runs_created_at ON agent_runs(created_at); |
|
CREATE INDEX idx_projects_account_id ON projects(account_id); |
|
CREATE INDEX idx_projects_created_at ON projects(created_at); |
|
CREATE INDEX idx_messages_thread_id ON messages(thread_id); |
|
CREATE INDEX idx_messages_created_at ON messages(created_at); |
|
|
|
|
|
ALTER TABLE threads ENABLE ROW LEVEL SECURITY; |
|
ALTER TABLE messages ENABLE ROW LEVEL SECURITY; |
|
ALTER TABLE agent_runs ENABLE ROW LEVEL SECURITY; |
|
ALTER TABLE projects ENABLE ROW LEVEL SECURITY; |
|
|
|
|
|
CREATE POLICY project_select_policy ON projects |
|
FOR SELECT |
|
USING ( |
|
is_public = TRUE OR |
|
basejump.has_role_on_account(account_id) = true |
|
); |
|
|
|
CREATE POLICY project_insert_policy ON projects |
|
FOR INSERT |
|
WITH CHECK (basejump.has_role_on_account(account_id) = true); |
|
|
|
CREATE POLICY project_update_policy ON projects |
|
FOR UPDATE |
|
USING (basejump.has_role_on_account(account_id) = true); |
|
|
|
CREATE POLICY project_delete_policy ON projects |
|
FOR DELETE |
|
USING (basejump.has_role_on_account(account_id) = true); |
|
|
|
|
|
CREATE POLICY thread_select_policy ON threads |
|
FOR SELECT |
|
USING ( |
|
basejump.has_role_on_account(account_id) = true OR |
|
EXISTS ( |
|
SELECT 1 FROM projects |
|
WHERE projects.project_id = threads.project_id |
|
AND ( |
|
projects.is_public = TRUE OR |
|
basejump.has_role_on_account(projects.account_id) = true |
|
) |
|
) |
|
); |
|
|
|
CREATE POLICY thread_insert_policy ON threads |
|
FOR INSERT |
|
WITH CHECK ( |
|
basejump.has_role_on_account(account_id) = true OR |
|
EXISTS ( |
|
SELECT 1 FROM projects |
|
WHERE projects.project_id = threads.project_id |
|
AND basejump.has_role_on_account(projects.account_id) = true |
|
) |
|
); |
|
|
|
CREATE POLICY thread_update_policy ON threads |
|
FOR UPDATE |
|
USING ( |
|
basejump.has_role_on_account(account_id) = true OR |
|
EXISTS ( |
|
SELECT 1 FROM projects |
|
WHERE projects.project_id = threads.project_id |
|
AND basejump.has_role_on_account(projects.account_id) = true |
|
) |
|
); |
|
|
|
CREATE POLICY thread_delete_policy ON threads |
|
FOR DELETE |
|
USING ( |
|
basejump.has_role_on_account(account_id) = true OR |
|
EXISTS ( |
|
SELECT 1 FROM projects |
|
WHERE projects.project_id = threads.project_id |
|
AND basejump.has_role_on_account(projects.account_id) = true |
|
) |
|
); |
|
|
|
|
|
CREATE POLICY agent_run_select_policy ON agent_runs |
|
FOR SELECT |
|
USING ( |
|
EXISTS ( |
|
SELECT 1 FROM threads |
|
LEFT JOIN projects ON threads.project_id = projects.project_id |
|
WHERE threads.thread_id = agent_runs.thread_id |
|
AND ( |
|
projects.is_public = TRUE OR |
|
basejump.has_role_on_account(threads.account_id) = true OR |
|
basejump.has_role_on_account(projects.account_id) = true |
|
) |
|
) |
|
); |
|
|
|
CREATE POLICY agent_run_insert_policy ON agent_runs |
|
FOR INSERT |
|
WITH CHECK ( |
|
EXISTS ( |
|
SELECT 1 FROM threads |
|
LEFT JOIN projects ON threads.project_id = projects.project_id |
|
WHERE threads.thread_id = agent_runs.thread_id |
|
AND ( |
|
basejump.has_role_on_account(threads.account_id) = true OR |
|
basejump.has_role_on_account(projects.account_id) = true |
|
) |
|
) |
|
); |
|
|
|
CREATE POLICY agent_run_update_policy ON agent_runs |
|
FOR UPDATE |
|
USING ( |
|
EXISTS ( |
|
SELECT 1 FROM threads |
|
LEFT JOIN projects ON threads.project_id = projects.project_id |
|
WHERE threads.thread_id = agent_runs.thread_id |
|
AND ( |
|
basejump.has_role_on_account(threads.account_id) = true OR |
|
basejump.has_role_on_account(projects.account_id) = true |
|
) |
|
) |
|
); |
|
|
|
CREATE POLICY agent_run_delete_policy ON agent_runs |
|
FOR DELETE |
|
USING ( |
|
EXISTS ( |
|
SELECT 1 FROM threads |
|
LEFT JOIN projects ON threads.project_id = projects.project_id |
|
WHERE threads.thread_id = agent_runs.thread_id |
|
AND ( |
|
basejump.has_role_on_account(threads.account_id) = true OR |
|
basejump.has_role_on_account(projects.account_id) = true |
|
) |
|
) |
|
); |
|
|
|
|
|
CREATE POLICY message_select_policy ON messages |
|
FOR SELECT |
|
USING ( |
|
EXISTS ( |
|
SELECT 1 FROM threads |
|
LEFT JOIN projects ON threads.project_id = projects.project_id |
|
WHERE threads.thread_id = messages.thread_id |
|
AND ( |
|
projects.is_public = TRUE OR |
|
basejump.has_role_on_account(threads.account_id) = true OR |
|
basejump.has_role_on_account(projects.account_id) = true |
|
) |
|
) |
|
); |
|
|
|
CREATE POLICY message_insert_policy ON messages |
|
FOR INSERT |
|
WITH CHECK ( |
|
EXISTS ( |
|
SELECT 1 FROM threads |
|
LEFT JOIN projects ON threads.project_id = projects.project_id |
|
WHERE threads.thread_id = messages.thread_id |
|
AND ( |
|
basejump.has_role_on_account(threads.account_id) = true OR |
|
basejump.has_role_on_account(projects.account_id) = true |
|
) |
|
) |
|
); |
|
|
|
CREATE POLICY message_update_policy ON messages |
|
FOR UPDATE |
|
USING ( |
|
EXISTS ( |
|
SELECT 1 FROM threads |
|
LEFT JOIN projects ON threads.project_id = projects.project_id |
|
WHERE threads.thread_id = messages.thread_id |
|
AND ( |
|
basejump.has_role_on_account(threads.account_id) = true OR |
|
basejump.has_role_on_account(projects.account_id) = true |
|
) |
|
) |
|
); |
|
|
|
CREATE POLICY message_delete_policy ON messages |
|
FOR DELETE |
|
USING ( |
|
EXISTS ( |
|
SELECT 1 FROM threads |
|
LEFT JOIN projects ON threads.project_id = projects.project_id |
|
WHERE threads.thread_id = messages.thread_id |
|
AND ( |
|
basejump.has_role_on_account(threads.account_id) = true OR |
|
basejump.has_role_on_account(projects.account_id) = true |
|
) |
|
) |
|
); |
|
|
|
|
|
GRANT ALL PRIVILEGES ON TABLE projects TO authenticated, service_role; |
|
GRANT SELECT ON TABLE projects TO anon; |
|
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 OR REPLACE FUNCTION get_llm_formatted_messages(p_thread_id UUID) |
|
RETURNS JSONB |
|
SECURITY DEFINER |
|
LANGUAGE plpgsql |
|
AS $$ |
|
DECLARE |
|
messages_array JSONB := '[]'::JSONB; |
|
has_access BOOLEAN; |
|
current_role TEXT; |
|
latest_summary_id UUID; |
|
latest_summary_time TIMESTAMP WITH TIME ZONE; |
|
is_project_public BOOLEAN; |
|
BEGIN |
|
|
|
SELECT current_user INTO current_role; |
|
|
|
|
|
SELECT p.is_public INTO is_project_public |
|
FROM threads t |
|
LEFT JOIN projects p ON t.project_id = p.project_id |
|
WHERE t.thread_id = p_thread_id; |
|
|
|
|
|
IF current_role = 'authenticated' AND NOT is_project_public THEN |
|
|
|
SELECT EXISTS ( |
|
SELECT 1 FROM threads t |
|
LEFT JOIN projects p ON t.project_id = p.project_id |
|
WHERE t.thread_id = p_thread_id |
|
AND ( |
|
basejump.has_role_on_account(t.account_id) = true OR |
|
basejump.has_role_on_account(p.account_id) = true |
|
) |
|
) INTO has_access; |
|
|
|
IF NOT has_access THEN |
|
RAISE EXCEPTION 'Thread not found or access denied'; |
|
END IF; |
|
END IF; |
|
|
|
|
|
SELECT message_id, created_at |
|
INTO latest_summary_id, latest_summary_time |
|
FROM messages |
|
WHERE thread_id = p_thread_id |
|
AND type = 'summary' |
|
AND is_llm_message = TRUE |
|
ORDER BY created_at DESC |
|
LIMIT 1; |
|
|
|
|
|
IF latest_summary_id IS NOT NULL THEN |
|
RAISE NOTICE 'Found latest summary message: id=%, time=%', latest_summary_id, latest_summary_time; |
|
ELSE |
|
RAISE NOTICE 'No summary message found for thread %', p_thread_id; |
|
END IF; |
|
|
|
|
|
WITH parsed_messages AS ( |
|
SELECT |
|
message_id, |
|
CASE |
|
WHEN jsonb_typeof(content) = 'string' THEN content::text::jsonb |
|
ELSE content |
|
END AS parsed_content, |
|
created_at, |
|
type |
|
FROM messages |
|
WHERE thread_id = p_thread_id |
|
AND is_llm_message = TRUE |
|
AND ( |
|
|
|
|
|
latest_summary_id IS NULL |
|
OR message_id = latest_summary_id |
|
OR created_at > latest_summary_time |
|
) |
|
ORDER BY created_at |
|
) |
|
SELECT JSONB_AGG(parsed_content) |
|
INTO messages_array |
|
FROM parsed_messages; |
|
|
|
|
|
IF messages_array IS NULL THEN |
|
RETURN '[]'::JSONB; |
|
END IF; |
|
|
|
RETURN messages_array; |
|
END; |
|
$$; |
|
|
|
|
|
GRANT EXECUTE ON FUNCTION get_llm_formatted_messages TO authenticated, anon, service_role; |