Imagine you’ve built a Python program that uses Generative AI (GenAI) to query data using natural language from any table in any SAS library in a SAS Viya environment. Now, you want to make it easier for users to consume this powerful application. The best approach? Containerization! This guide will walk you through containerizing the application so it’s portable, scalable, and easy to deploy.
By the end of this process, you’ll have an interactive AI-powered assistant that enables users to query and analyze SAS Viya data using natural language. Powered by Azure OpenAI’s GPT-4 and SAS Viya, the application:
This solution empowers users to focus on insights without needing programming skills.
Here's how to get started.
The application relies on a Large Language Model (LLM). For this guide, we use GPT-4 deployed on Azure OpenAI. The Python program communicates with the LLM through the OpenAI Python client library.
To securely manage Azure OpenAI credentials, use environment variables. Create a .env file in the same directory as your Dockerfile with the following content:
OPENAI_URI=Your_Azure_OpenAI_Endpoint
OPENAI_KEY=Your_Azure_OpenAI_API_Key
OPENAI_VERSION=Your_Azure_OpenAI_API_Version
OPENAI_GPT_DEPLOYMENT=Your_OpenAI_Model_Deployment_Name
Replace the placeholders with your actual Azure OpenAI details. This ensures sensitive information stays secure and outside your codebase. For Docker on Linux do not wrap the values in quotes.
The Python script (e.g., demo-function-calling-sas.py) is the core of the application. It orchestrates:
import os import json from typing import Any, Callable, Dict from openai import AzureOpenAI from dotenv import load_dotenv from termcolor import colored import saspy import re # Load environment variables load_dotenv(".env") # Azure OpenAI configuration api_endpoint = os.getenv("OPENAI_URI") api_key = os.getenv("OPENAI_KEY") api_version = os.getenv("OPENAI_VERSION") api_deployment_name = os.getenv("OPENAI_GPT_DEPLOYMENT") # Create an AzureOpenAI client client = AzureOpenAI( api_key=api_key, api_version=api_version, azure_endpoint=api_endpoint) def sas_viya_url(): # Ask the user for the new URL new_url = input("Enter your SAS Viya URL, for example: https://beret-p04222-rg.gelenable.sas.com: ") # Read the entire content of the file with open('sascfg_personal.py', 'r') as f: content = f.read() # Define a regex pattern to find the 'url' key inside 'httpsviya' dictionary pattern = r"(httpsviya\s*=\s*\{\s*(?:[^{}]*\n)*?\s*'url'\s*:\s*)'[^']*'" # Replacement string with the new URL replacement = r"\1'{}'".format(new_url) # Perform the substitution new_content = re.sub(pattern, replacement, content, flags=re.MULTILINE) # Write the updated content back to the file with open('sascfg_personal.py', 'w') as f: f.write(new_content) print("The 'url' value has been updated successfully in sascfg_personal.py.") def log_message(message: str): """Logs error messages in red bold text.""" print(colored(message, 'red', attrs=['bold'])) def print_in_color(key, value): """Prints key-value pairs in color for better readability.""" if isinstance(value, str): formatted_value = value.replace("\\n", "\n").strip() else: formatted_value = str(value) # Convert to string if not already print(colored(key, 'green', attrs=['bold']), colored(formatted_value, 'yellow', attrs=['bold'])) def execute_sas_code(sas, sas_code: str) -> str: """Executes the given SAS code using the provided SAS session.""" sas_result = sas.submit(sas_code, results='TEXT') result_txt = sas_result['LST'] return result_txt def get_column_info(sas, library: str, table: str) -> str: """Retrieves column metadata from a SAS table.""" sas_code = f""" proc contents data={library}.{table} out=column_metadata(keep=name type length format informat label) noprint; run; proc print data=column_metadata; run; """ return execute_sas_code(sas, sas_code) # SAS Tools tools_list = [ { "type": "function", "function": { "name": "execute_sas_code", "description": "This function is used to answer user questions about SAS Viya data by executing PROC SQL queries, DATA STEP or any other SAS PROC against the data source.", "parameters": { "type": "object", "properties": { "sas_code": { "type": "string", "description": f""" The input should be a well-formed SAS code to extract information based on the user's question. The code execution result will be returned as plain text, not in JSON format. """, } }, "required": ["sas_code"], "additionalProperties": False, }, }, }, ] def call_functions(tool_calls, function_map) -> None: """ Processes and executes function calls requested by the assistant. This function is responsible for calling the appropriate Python functions based on the function calls detected in the assistant's response. It extracts the function name and arguments from each function call, looks up the corresponding function in `function_map`, executes it with the provided arguments, and displays the result. Args: tool_calls (list): A list of function call objects extracted from the assistant's response. Each function call contains the function name and its arguments. Raises: ValueError: If a function name from the tool call does not exist in `function_map`. """ for tool_call in tool_calls: func_name = tool_call.function.name arguments = json.loads(tool_call.function.arguments) print_in_color("Executing function tool call", "") print_in_color("Function Name:", func_name) print_in_color("Arguments:", arguments) function = function_map.get(func_name) if not function: raise ValueError(f"Unknown function: {func_name}") result_df = function(arguments) print(result_df) def process_message(sas, question: str, library: str, table: str, sas_table_info: str): """Processes the user's question and interacts with the OpenAI API. Calls functions as needed.""" # Construct system message system_message = ( "You are a data analysis assistant for data in SAS tables and libraries. " "Please be polite, professional, helpful, and friendly. " "Use the `execute_sas_code` function to execute SAS data queries, PROC SQL, DATA STEP or any other SAS PROC." "Default to aggregated data unless a detailed breakdown is requested. " "The function returns TXT-formatted results, nicely tabulated for use display. " f"Refer to the {library}.{table} metadata: {sas_table_info}. ", f"If a question is not related to {library}.{table} data or you cannot answer the question, " "then simply say, 'I can't answer that question. Please contact IT for more assistance.' " "If the user asks for help or says 'help', provide a list of sample questions that you can answer." ) #print(system_message) messages = [{"role": "system", "content": str(system_message)}, {"role": "user", "content": question}] # First API call: Ask the model to use the tool try: response = client.chat.completions.create( model=api_deployment_name, messages=messages, tools=tools_list, temperature=0.2, max_tokens=1512, ) # Process the model's response response_message = response.choices[0].message tool_calls = getattr(response_message, "tool_calls", []) # addition messages.append(response_message) #print("Model's response: ", response_message) # Map the tool to the function defined above function_map: Dict[str, Callable[[Any], str]] = { "execute_sas_code": lambda args: execute_sas_code(sas, args["sas_code"]), } if tool_calls: call_functions(tool_calls, function_map) # new addition for tool_call in response_message.tool_calls: if tool_call.function.name == "execute_sas_code": function_args = json.loads(tool_call.function.arguments) #print(f"Function arguments: {function_args}") sas_response = execute_sas_code(sas, sas_code=function_args.get("sas_code") ) messages.append({ "tool_call_id": tool_call.id, "role": "tool", "name": "get_current_time", "content": sas_response, }) else: return response_message.content # Second API call: Get the final response from the model final_response = client.chat.completions.create( model=api_deployment_name, messages=messages, temperature=0.2, max_tokens=888 ) return final_response.choices[0].message.content except Exception as e: log_message(f"An error occurred: {e}") # Main function def main(): """Main function to start the assistant.""" # Choose the SAS Viya URL for SASPY config sas_viya_url() # Start SAS session print ('I am starting a SAS session. Thanks for your patience...') sas = saspy.SASsession(cfgfile='sascfg_personal.py', cfgname='httpsviya') library = 'sampsio' table = 'dmlcens' print(f"Library and table selected: {library} and {table}") # Fetch table metadata once at the beginning sas_table_info = get_column_info(sas, library=library, table=table) print('I am going to use this table metadata: ', sas_table_info) while True: print("SAS Viya and Azure OpenAI are listening. Write 'help' to get suggestions, 'change' to switch tables, 'stop' or press Ctrl-Z to end.") try: # Start session q = input("Enter your question: \n") if q.strip().lower() == "change": library = input("Enter the library name: ") table = input("Enter the table name: ") print(f"Library and table selected: {library} and {table}") # Fetch new table metadata after changing the library/table sas_table_info = get_column_info(sas, library=library, table=table) print('I am going to use this table metadata: ', sas_table_info) elif q.strip().lower() == "stop": print("Conversation ended.") sas.endsas() break else: assistant_result = process_message(sas, q, library, table, sas_table_info) print(assistant_result) except EOFError: break except Exception as e: # Handle exceptions print(f"An error occurred: {e}") if __name__ == "__main__": main()
Key Features:
Tip: A SAS Studio compute context session called sas is started just once, at the beginning of the main function. The session is reused in the def execute_sas_code(sas, sas_code: str) -> str: speeding up the application.
The file sascfg_personal.py is needed to start a SAS Viya SAS Studio compute context session, using the SASPy Python package. The generated SAS code is executed in this session.
SAS_config_names=['httpsviya']
SAS_config_options = {'lock_down': False,
'verbose' : True,
'prompt' : True
}
SAS_output_options = {'output' : 'html5'} # not required unless changing any of the default
httpsviya = {'url' : 'your-sas-viya-url.com',
'context' : 'SAS Studio compute context',
'options' : ["fullstimer", "memsize=4G"],
'verify' : False
}
Tip: The Python program demo-function-calling-sas.py asks the user the SAS Viya environment URL and writes it in the sascfg_personal.py in the httpsviya object. The trick allows you to work with any SAS Viya environment. SASPY will then generate a link that you can use to log in SAS Viya and generate an authentication code.
The following Python packages are required. You can install them using pip and the provided requirements.txt file.
openai>=1.37.1, <2.0.0 python-dotenv>=1.0.1, <2.0.0
termcolor
saspy
To build a Docker container you will need a machine with Docker installed. For this example, we’ll assume Docker is installed on a Linux machine (CentOS).
The Dockerfile specifies how to build your Docker image, copying the necessary files, installing the requirements and running the Python program.
# Use a lightweight Python image
FROM python:3.9-slim
# Set the working directory in the container
WORKDIR /app
# Copy requirements.txt into the container
COPY requirements.txt .
# Install the Python dependencies
RUN pip install --no-cache-dir -r requirements.txt
# Copy the rest of the application code
COPY . .
# Set the entry point to your application
CMD ["python", "demo-function-calling-sas.py"]
A .Dockerignore file excludes sensitive files and unnecessary content from your Docker image.
.env
__pycache__/
*.pyc
.git/
/images
Then, use the Docker CLI to build your image from the Dockerfile:
docker build -t data-query-sas .
# Confirm the build
docker image ls | grep data-query-sas
Use the Docker CLI to create and run a container from the image you built:
docker run -it --rm --env-file .env data-query-sas python demo-function-calling-sas.py
Note that the container will use the environment variables stored in the .env file.
Why don’t you try the container yourself using the instructions and files in this post and let us know how it went?
A container and a few tricks allow you to transform a Python program in a fully working AI Assistant for SAS Viya.
The approach is similar with the Jupyter Notebook described in Query Data Using Natural Language in SAS Viya with Azure OpenAI. The Python program has been tweaked to reuse a SAS Compute session, handle authorization code authentication with any SAS Viya environment and allow changing the table to be analyzed. The most important difference is that the program has been packaged as a Docker container.
Thank you for your time reading this post. If you liked the post, give it a thumbs up! Please comment and tell us what you think about having conversations with your data. If you wish to get more information, please write me an email.
Find more articles from SAS Global Enablement and Learning here.
@Bogdan_Teleuca excellent article, thanks!
It's finally time to hack! Remember to visit the SAS Hacker's Hub regularly for news and updates.
The rapid growth of AI technologies is driving an AI skills gap and demand for AI talent. Ready to grow your AI literacy? SAS offers free ways to get started for beginners, business leaders, and analytics professionals of all skill levels. Your future self will thank you.