BookmarkSubscribeRSS Feed

How to Containerize a SAS Viya and GenAI Application

Started ‎02-11-2025 by
Modified ‎02-11-2025 by
Views 1,543

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.

 

 

The End Result

 

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:

 

  • Converts user questions into SAS code.
  • Executes the code in a SAS Viya Compute context.
  • Summarizes the results in plain language.

 

This solution empowers users to focus on insights without needing programming skills.

 

 

Here's how to get started.

 

 

Container Ingredients

 

1. The AI Assistant

 

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.

 

 

2. Environment Variables

 

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.

 

 

3. The Python Program

 

The Python script (e.g., demo-function-calling-sas.py) is the core of the application. It orchestrates:

 

  • Dynamic SAS Viya URL Configuration: Allows users to work with any SAS Viya environment by dynamically updating the URL in the sascfg_personal.py file.
  • Azure OpenAI Integration: Uses GPT-4 to interpret user queries and generate SAS code.
  • SAS Code Execution: Executes the SAS code via the SASPy Python package and retrieves results.
  • Interactive Workflow: Runs an interactive session where users can query data, switch tables, and receive insights.

 

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:

 

  • Metadata-Driven: Fetches table metadata (e.g., columns, types) to inform GPT-4’s responses.
  • Custom Functionality: Exposes specific tools (e.g., execute_sas_code) to the LLM for executing SAS queries in SAS Viya using SASPy.
  • Error Handling: Logs errors in a user-friendly format and handles exceptions gracefully.

 

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.

 

 

4. SAS Viya

 

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.

 

 

5. Python Requirements File

 

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

 

 

6. Docker

 

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).

 

 

7. Dockerfile

 

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"]

 

 

8 .Dockerignore File

 

A .Dockerignore file excludes sensitive files and unnecessary content from your Docker image.

 

.env
__pycache__/
*.pyc
.git/
/images

 

 

Build the Container Image

 

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

 

 

Build the Container

 

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.

 

 

Try It Yourself

 

Why don’t you try the container yourself using the instructions and files in this post and let us know how it went?

 

 

Conclusion

 

A container and a few tricks allow you to transform a Python program in a fully working AI Assistant for SAS Viya.

 

 

Additional Resources

 

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.

Comments

@Bogdan_Teleuca excellent article, thanks!

 

Contributors
Version history
Last update:
‎02-11-2025 05:52 PM
Updated by:

hackathon24-white-horiz.png

The 2025 SAS Hackathon has begun!

It's finally time to hack! Remember to visit the SAS Hacker's Hub regularly for news and updates.

Latest Updates

SAS AI and Machine Learning Courses

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.

Get started