Files
2026-07-13 13:30:30 +08:00

189 lines
9.4 KiB
PL/PgSQL

/*
####################################################################################
# Copyright 2024 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
####################################################################################
*/
-- DESCRIPTION: Example PostgreSQL function to dynamically build a RAG-enriched
-- prompt and invoke an LLM.
-- DISCLAIMER: This function is provided for demonstration purposes only and
-- should not be used in production without sufficient testing.
-- EXAMPLE USAGE: SELECT * FROM llm(prompt => 'This is a simple prompt.');
DROP FUNCTION IF EXISTS llm;
CREATE FUNCTION llm(
set_debug BOOLEAN DEFAULT false,
enable_history BOOLEAN DEFAULT false,
enable_stock_lookup BOOLEAN DEFAULT false,
uid INT DEFAULT 2147483647,
model TEXT DEFAULT 'gemini',
user_role TEXT DEFAULT 'I am a generic user',
llm_role TEXT DEFAULT ' You are a helpful AI Assistant',
mission TEXT DEFAULT null,
additional_context TEXT DEFAULT null,
output_format TEXT DEFAULT null,
examples TEXT DEFAULT null,
prompt TEXT DEFAULT 'Tell me I need to pass in a prompt parameter.',
output_instructions TEXT DEFAULT null,
response_restrictions TEXT DEFAULT 'You have no response restrictions for this prompt.',
disclaimer TEXT DEFAULT null,
max_output_tokens INT DEFAULT 512,
temperature DECIMAL DEFAULT 0.0,
top_p DECIMAL DEFAULT 0.95,
top_k DECIMAL DEFAULT 40
)
RETURNS TABLE (
llm_prompt TEXT,
llm_prompt_len INT,
llm_response TEXT,
llm_response_len INT,
extractive_prompt TEXT,
extractive_response TEXT,
recommended_tickers TEXT
)
LANGUAGE plpgsql
AS $$
DECLARE
llm_prompt TEXT := '';
llm_prompt_len INT := null;
llm_response TEXT := null;
interaction_history_count INT := 0;
extractive_prompt TEXT := '';
extractive_response TEXT := '';
recommended_tickers TEXT := '';
BEGIN
-- Define user and AI roles
IF llm_role IS NOT null THEN SELECT CONCAT(llm_prompt, 'AI ROLE: ', llm_role, E'.\n') INTO llm_prompt; END IF;
IF mission IS NOT null THEN SELECT CONCAT(llm_prompt, 'AI MISSION: ', mission, E'.\n') INTO llm_prompt; END IF;
IF user_role IS NOT null THEN SELECT CONCAT(llm_prompt, 'USER ROLE: ', user_role, E' \n\n') INTO llm_prompt; END IF;
-- Define the task/prompt
IF prompt IS NOT null THEN SELECT CONCAT(llm_prompt, E'INSTRUCTIONS: \n- Respond to the PROMPT using FEWER than ', CAST (ROUND(max_output_tokens * 3) AS TEXT), E' characters, including white space. The PROMPT begins with "<PROMPT>" and ends with "</PROMPT>". \n- Use available CONTEXT to improve your response, and tell the user specifically which CONTEXT you used in plain language (do not use programming markup or tags). The context begins with "<CONTEXT>" and ends with "</CONTEXT>". \n- Strictly comply with all response restrictions. Response restrictions start with <RESPONSE_RESTRICTIONS> and end with </RESPONSE_RESTRICTIONS>. \n\n<PROMPT>\n ', prompt, E'\n</PROMPT>\n\n') INTO llm_prompt; END IF;
-- Enforce response restrictions
IF response_restrictions IS NOT null THEN SELECT CONCAT(llm_prompt, E'<RESPONSE_RESTRICTIONS>\n\n ', response_restrictions, E' \n\n</RESPONSE_RESTRICTIONS>\n\n') INTO llm_prompt; END IF;
-- Open the context tag
SELECT CONCAT(llm_prompt, E'<CONTEXT>\n\n') INTO llm_prompt;
-- Add conversation history
IF enable_history is true THEN
-- Check if this is the first interaction from this user
SELECT COUNT(*) FROM conversation_history WHERE user_id = uid INTO interaction_history_count;
-- Add last interaction to prompt
SELECT CONCAT(llm_prompt, E'<LATEST_INTERACTION>\n==========\n',
CASE
WHEN interaction_history_count = 0 THEN E' First interaction\n'
ELSE (SELECT CONCAT(E'**TIME**\n', datetime, E'\n\n**USER**\n', user_prompt, E'\n\n**AI**\n', ai_response, E'\n') FROM conversation_history WHERE user_id = uid ORDER BY datetime DESC LIMIT 1)
END,
E'\n==========\n</LATEST_INTERACTION>\n\n') INTO llm_prompt;
-- Add other relevant interaction history to prompt
IF interaction_history_count > 1 THEN
WITH ch AS (
SELECT * FROM conversation_history
WHERE user_id = uid
AND id < (SELECT id FROM conversation_history WHERE user_id = uid ORDER BY datetime DESC LIMIT 1)
ORDER BY user_prompt_embedding <=> google_ml.embedding('text-embedding-005', prompt)::vector
LIMIT 3
)
SELECT CONCAT(llm_prompt, E'<OTHER_INTERACTION_HISTORY>\n==========\n', STRING_AGG(CONCAT(E'**TIME**\n', datetime, E'\n\n**USER**\n ', user_prompt, E'\n\n**AI**\n', ai_response), E'\n==========\n<\OTHER_INTERACTION_HISTORY>\n\n'))
INTO llm_prompt FROM ch;
END IF;
END IF;
-- Add additional_context and examples
IF additional_context IS NOT null THEN SELECT CONCAT(llm_prompt, E'<ADDITIONAL_CONTEXT> Use the following CONTEXT to respond to the PROMPT, and tell me specifically which pieces of CONTEXT you used to improve your response:\n\n ', additional_context, E' </ADDITIONAL_CONTEXT>\n\n') INTO llm_prompt; END IF;
IF examples IS NOT null THEN SELECT CONCAT(llm_prompt, E'<EXAMPLES> Use the following EXAMPLES to improve your OUTPUT.\n==========\n', examples, E' \n==========\n</EXAMPLES>\n\n') INTO llm_prompt; END IF;
-- Add output instructions, format, and length constraints
IF output_instructions IS NOT null THEN SELECT CONCAT(llm_prompt, E'<OUTPUT_INSTRUCTIONS> \nRe-write your OUTPUT using the following instructions:\n', output_instructions, E' \n</OUTPUT_INSTRUCTIONS>\n\n') INTO llm_prompt; END IF;
IF output_format IS NOT null THEN SELECT CONCAT(llm_prompt, '<OUTPUT_FORMAT> Complete the TASK using the following OUTPUT FORMAT: ', output_format, E' </OUTPUT_FORMAT>\n\n') INTO llm_prompt; END IF;
-- Do stock lookup if enabled
IF enable_stock_lookup is true THEN
SELECT CONCAT(E'List 3 words that best describe the type of investment this person is looking for based on their QUESTION, RISK_PROFILE, and BIO. \n\nQUESTION:\n', prompt, E'\n\nRISK_PROFILE: \n', risk_profile, E'\n\nBIO: \n', bio) INTO extractive_prompt FROM user_profiles WHERE id = uid;
SELECT google_ml.predict_row(model, json_build_object(
'contents', json_build_array(
json_build_object(
'role', 'user',
'parts', json_build_array(
json_build_object(
'text', extractive_prompt
)
)
)
),
'generationConfig', json_build_object(
'temperature', temperature,
'topP', top_p,
'topK', top_k,
'maxOutputTokens', max_output_tokens
))) -> 'candidates' -> 0 -> 'content' -> 'parts' -> 0 ->> 'text' INTO extractive_response;
WITH inv AS (
SELECT ticker, analysis
FROM investments
WHERE rating = 'BUY'
ORDER BY analysis_embedding <=> google_ml.embedding('text-embedding-005',extractive_response)::vector
LIMIT 3
)
SELECT
CONCAT(llm_prompt, E'<SUGGESTED_STOCKS> Recommend these specific stock tickers to me, and tell me why they are a good fit for me based on my BIO and personal details: ', STRING_AGG(ticker, ', '), E'\n\nTicker Details: ', STRING_AGG(CONCAT(E'\n========\n**STOCK TICKER**: ', ticker, E'\n\n', analysis), E'\n'), E'\n========\n</SUGGESTED_STOCKS>\n\n'),
CONCAT('Tickers: ', STRING_AGG(ticker, ', '))
FROM inv
INTO llm_prompt, recommended_tickers;
END IF;
-- Close the context tag
SELECT CONCAT(llm_prompt, E'</CONTEXT>\n\n') INTO llm_prompt;
-- Send enriched prompt to LLM
IF set_debug is false THEN
SELECT google_ml.predict_row(model, json_build_object(
'contents', json_build_array(
json_build_object(
'role', 'user',
'parts', json_build_array(
json_build_object(
'text', llm_prompt
)
)
)
),
'generationConfig', json_build_object(
'temperature', temperature,
'topP', top_p,
'topK', top_k,
'maxOutputTokens', max_output_tokens
))) -> 'candidates' -> 0 -> 'content' -> 'parts' -> 0 ->> 'text' INTO llm_response;
END IF;
-- Record conversation history
IF enable_history is true THEN
INSERT INTO conversation_history (user_id, user_prompt, ai_response)
VALUES (uid, prompt, llm_response);
END IF;
-- Add disclaimer
IF disclaimer IS NOT null THEN SELECT CONCAT(llm_response, E'\n\n', disclaimer) INTO llm_response; END IF;
-- Return the response
RETURN QUERY SELECT llm_prompt, LENGTH(llm_prompt), llm_response, LENGTH(llm_response), extractive_prompt, extractive_response, recommended_tickers;
END;
$$;