The SAS Scripting Wrapper for Analytics Transfer (SWAT) custom agent is designed to be an interactive AI assistant. It streamlines SAS code generation and execution for users, regardless of their expertise in SAS, Python, or CAS actions. In this post, we'll look into the agent's code, outline the prerequisites, and guide you through replicating the agent in your own environment.
Check out the video in SWAT Code Generation and Execution in SAS Viya with Azure OpenAI and LangChain for an insider's look at how this agent functions. I'll walk you through its operations, potential uses, and some of the intriguing behaviors that the custom agent displays.
Select any image to see a larger version.
Mobile users: To view the images, select the "Full" version at the bottom of the page.
Now it's time to reveal the code and walk you through the details.
The core Python script, named sasgenexswat.py, establishes the SWAT LangChain custom agent that interfaces with SAS Viya
# LangChain custom SAS Viya agent with memory and only two tools
# Resource: https://python.langchain.com/docs/modules/agents/how_to/custom_agent
import os
import json
import urllib3
urllib3.disable_warnings
# Load the Large Language Model - Azure OpenAI
from langchain_openai import AzureChatOpenAI
from openai import AzureOpenAI
# Get Azure OpenAI environment variables from .env
from dotenv import load_dotenv
load_dotenv()
azure_oai_endpoint = os.getenv("AZURE_OAI_ENDPOINT")
azure_oai_key = os.getenv("AZURE_OAI_KEY")
azure_oai_deployment = os.getenv("AZURE_OAI_DEPLOYMENT")
azure_oai_model = os.getenv("AZURE_OAI_DEPLOYMENT")
os.environ["OPENAI_API_TYPE"] = "azure"
os.environ["AZURE_OPENAI_API_KEY"] = azure_oai_key
os.environ["AZURE_OPENAI_ENDPOINT"] = azure_oai_endpoint
os.environ["OPENAI_API_VERSION"] = "2024-05-01-preview"
## We'll use GPT-4 as deployed model
llm = AzureChatOpenAI(deployment_name=azure_oai_model, temperature=0, max_tokens=2000)
print("We'll be using the following LLM:\n", llm, "\n")
# LLM Client
from openai import AzureOpenAI
client = AzureOpenAI(
azure_endpoint = azure_oai_endpoint,
api_key = azure_oai_key,
api_version= "2024-05-01-preview"
)
# Arguments
import sys
# Check if at least one argument is provided
if len(sys.argv) > 1:
# sys.argv[1] is the first parameter passed to the script
sas_viya_url = sys.argv[1]
print(f"""Passing prompts to Azure OpenAI.
\nAzure OpenAI will generate the SWAT code and execute it on: {sas_viya_url}.
\nAssuming you obtained an access token first by running get_token_2023.py.""")
print('\nRetrieving the saved token from api/access_token.txt')
with open("api/access_token.txt", "r", encoding="UTF-8") as f:
token = f.read()
else:
print("No SAS Viya URL provided.")
import swat # SWAT session
# Define custom tools - SWAT GenEx
from langchain.pydantic_v1 import BaseModel, Field
from langchain.tools import BaseTool, StructuredTool, tool
import re # code parsing
from io import StringIO # exec output
@tool
def get_word_length(word: str) -> int:
"""Returns the length of a word."""
return len(word)
@tool
def generate_swat_code(prompt: str) -> str:
"""Returns the generated SWAT Python code, based on the user prompt.
The code should be aimed at printing the execution output, as it would be used further.
Args:
prompt: The prompt which will be sent to Azure OpenAI.
"""
response = client.chat.completions.create(
model=azure_oai_model,
messages=[
{"role": "system", "content": "You are a SAS Python SWAT package assistant that helps people write SWAT code. Return only the Python SWAT code, ready to be executed. This is the code in between ```python and ```. Assume there is already a connection named 'conn' defined."},
# ... other system and user messages ...
{"role": "user", "content": prompt},
]
)
sas_response = response.choices[0].message.content + '\n'
# Strip code of characters
pattern = re.compile(r"py\n(.*?)", re.DOTALL)
match = pattern.search(sas_response)
if match:
swat_code = match.group(1).strip()
print('\n',swat_code, '\n')
else:
swat_code = sas_response
# Clean code block
code_block = swat_code
swat_code = code_block.replace("```python\n", "").replace("\n```", "")
# Save the SWAT code to a file
filename = "generated_swat_code.py"
# Open the file first
try:
with open(filename, 'r', encoding='utf-8') as file:
current_content = file.read()
except FileNotFoundError:
current_content = ""
# Prepend the new SWAT code to the existing content
new_content = '# Prompt -> ' + prompt + '\n' '# Generated code -> ' + '\n' + swat_code + '\n' + current_content
with open(filename, 'w', encoding='utf-8') as file:
file.write(new_content)
print(f"Generated SWAT code has been saved to {filename}")
return swat_code
@tool
def execute_swat_code(swat_code: str) -> str:
"""Useful when you need to execute generated Python SWAT code.
Input to this tool is correct Python SWAT code, ready to be executed in a SAS Viya environment CAS session.
Assume an already established SWAT connection, called 'conn'.
Identify the results, summarize and provide a short explanation.
If an error is returned, you may need to rewrite the SWAT code, using the generate_SWAT_code tool.
Provide a Final Result: Summarize and provide a short explanation of what was done and what the outcome was.
If you encounter an issue, detail what the issue is.
Args:
swat_code: The SWAT Python code which will be submitted to a CAS session in SAS Viya.
"""
# Python code as a string stored in a variable
print (f'\nI will submit the following code to {sas_viya_url}: \n',swat_code,'\n...End of code...\n')
try:
# Create SWAT connection using the url and a token obtained earlier
port='443'
conn_string= sas_viya_url + ':' + port + '/cas-shared-default-http'
conn=swat.CAS(conn_string, password=token)
# Create a dictionary to hold the execution context
namespace = {'conn': conn} # Add 'conn' to the namespace
old_stdout = sys.stdout
old_stderr = sys.stderr
redirected_output = sys.stdout = StringIO()
redirected_error = sys.stderr = StringIO()
try:
# execute the SWAT code
exec(swat_code, namespace)
except Exception:
# Optionally, handle the exception in some way here
pass
# Reset stdout and stderr to their original values
sys.stdout = old_stdout
sys.stderr = old_stderr
# Get the captured output and errors
output = redirected_output.getvalue()
errors = redirected_error.getvalue()
# Print the captured output and errors
print("Captured Output:")
print(output)
print("Captured Errors:")
print(errors)
# Terminate SWAT connection
conn.terminate()
return output + errors
except Exception as e:
print(f'An error occurred: {e}')
errors = e
return errors
# Describe the tools. The description is important for the LangChain chain.
tools = [get_word_length, generate_swat_code, execute_swat_code]
# LangChain Agent with Memory
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
MEMORY_KEY = "chat_history"
prompt = ChatPromptTemplate.from_messages(
[
(
"system",
"You are a world class AI assistant who helps people generate and execute SAS code. Interpret the results if asked.",
),
MessagesPlaceholder(variable_name=MEMORY_KEY),
("user", "{input}"),
MessagesPlaceholder(variable_name="agent_scratchpad"),
]
)
llm_with_tools = llm.bind_tools(tools)
from langchain_core.messages import AIMessage, HumanMessage
from langchain.agents.format_scratchpad.openai_tools import (
format_to_openai_tool_messages,
)
from langchain.agents.output_parsers.openai_tools import OpenAIToolsAgentOutputParser
chat_history = []
agent = (
{
"input": lambda x: x["input"],
"agent_scratchpad": lambda x: format_to_openai_tool_messages(
x["intermediate_steps"]
),
"chat_history": lambda x: x["chat_history"],
}
| prompt
| llm_with_tools
| OpenAIToolsAgentOutputParser()
)
from langchain.agents import AgentExecutor
agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True)
# Main function
def chat_with_open_ai():
# ... as before
while True:
print("SAS and Azure OpenAI are listening. Write 'Stop' or press Ctrl-Z to end the conversation.")
try:
# SWAT start session
#conn = start_or_retrieve_swat_session(sas_viya_url)
# Get text from the keyboard
q = input("Enter your question: \n")
if q == "Stop":
print("Conversation ended.")
#term = conn.terminate()
#print('SWAT session ended successfully.', term)
break
else:
print("\nI will ask: ", q)
result = agent_executor.invoke({"input": q, "chat_history": chat_history})
chat_history.extend(
[
HumanMessage(content=q),
AIMessage(content=result["output"]),
]
)
except EOFError:
break
# main
try:
chat_with_open_ai()
except Exception as err:
print("Encountered exception. {}".format(err))
The provided Python code integrates several components to create a custom agent that leverages Azure's OpenAI Large Language Model (LLM), specifically GPT-4, to generate and execute SAS code via the SAS Python SWAT (SAS Scripting Wrapper for Analytics Transfer) package. This agent is designed to facilitate interactions with SAS Viya, a cloud-enabled, in-memory analytics engine.
Here's a step-by-step breakdown of the code's functionality:
@tool
def generate_swat_code(prompt: str) -> str:
"""Returns the generated SWAT Python code, based on the user prompt.
The code should be aimed at printing the execution output, as it would be used further.
Args:
prompt: The prompt which will be sent to Azure OpenAI.
"""
response = client.chat.completions.create(
model=azure_oai_model,
messages=[
{"role": "system", "content": """You are a SAS Python SWAT package assistant that helps people write SWAT code.
Return only the Python SWAT code, ready to be executed. This is the code in between ```python and ```.
Assume there is already a connection named 'conn' defined."},
# ... other system and user messages ...
{"role": "user", "content": prompt},
]
)
@tool
def execute_swat_code(swat_code: str) -> str:
"""Useful when you need to execute generated Python SWAT code.
Input to this tool is correct Python SWAT code, ready to be executed in a SAS Viya environment CAS session.
Assume an already established SWAT connection, called 'conn'.
Identify the results, summarize and provide a short explanation.
If an error is returned, you may need to rewrite the SWAT code, using the generate_SWAT_code tool.
Provide a Final Result: Summarize and provide a short explanation of what was done and what the outcome was.
If you encounter an issue, detail what the issue is.
Args:
swat_code: The SWAT Python code which will be submitted to a CAS session in SAS Viya.
"""
try:
# Create SWAT connection using the url and a token obtained earlier
port='443'
conn_string= sas_viya_url + ':' + port + '/cas-shared-default-http'
conn=swat.CAS(conn_string, password=token)
# Create a dictionary to hold the execution context
namespace = {'conn': conn} # Add 'conn' to the namespace
old_stdout = sys.stdout
old_stderr = sys.stderr
redirected_output = sys.stdout = StringIO()
redirected_error = sys.stderr = StringIO()
try:
# execute the SWAT code
exec(swat_code, namespace)
except Exception:
# Optionally, handle the exception in some way here
pass
#
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
MEMORY_KEY = "chat_history"
prompt = ChatPromptTemplate.from_messages(
[
(
"system",
"You are a world class AI assistant who helps people generate and execute SAS code. Interpret the results if asked.",
),
MessagesPlaceholder(variable_name=MEMORY_KEY),
("user", "{input}"),
MessagesPlaceholder(variable_name="agent_scratchpad"),
]
)
llm_with_tools = llm.bind_tools(tools)
Now let’s look at what you need to build this custom agent.
To build this custom agent, you'll require a SAS Viya deployment. From your SAS Viya environment, you'll need to gather the following details:
Additionally, ensure that CAS (Cloud Analytic Services) access is enabled for external connections to SAS Viya.
If your SAS Viya is hosted on Azure, it's essential to configure an inbound security rule. This rule must include the IP address of the computer running Visual Studio Code and specify port 443. The target for this rule should be the public IP address of your SAS Viya's load balancer.
To get started, you'll need to create a resource and deploy a model. If you're unfamiliar with the process, the Azure OpenAI QuickStart provides step-by-step guidance. To interact with Azure OpenAI, you require an endpoint, a key, and a deployment.
For this project, I selected the 1106-Preview GPT-4 model version, which was trained on data from December 2023. I've found that this version excels at generating SWAT code. Remember, choosing the right model is as crucial as the code that powers your application
We will use Visual Studio Code with the Python extension installed to run the custom agent. If you’re a regular user of Visual Studio Code, great.
Create a requirements.txt file that includes these Python packages:
python-dotenv
pandas
urllib3
swat
openai
langchain
langchain_openai
langchain-core
langchain-community
In the directory containing your main Python script, create a .env file to store your Azure OpenAI configuration parameters. The file should include the following lines:
AZURE_OAI_ENDPOINT='https://myuser.openai.azure.com/'
AZURE_OAI_KEY='key_here' #Azure Open AI key here
AZURE_OAI_DEPLOYMENT='gpt-4'
The sasgenexswat.py program is configured to read these environment variables from your .env file.
Firstly, you'll need to obtain the SAS Viya certificates, which are in the .PEM file format, and download them to your local machine.
For detailed instructions, please refer to the provided documentation.
The sasgenexswat.py program is designed to read an access token from a file called api/access_token.txt.
For SWAT to establish a connection within the custom agent, you'll need an OAuth token. You have two options for obtaining this token:
Once you have your certificates, open Visual Studio Code, launch a Bash terminal, and proceed with the setup.
SASVIYAURL=my_sas_viya_url
SASUSER=fill_in_here # SAS user
SASPASS=pass_here # SAS user password
CLIENT=fill_in_here # client id
CLIENTSECRET=fill_in_here # client secret
PEMPATH='fill_here'
# for example PEMPATH='C:\Users\myuser\Downloads\trustedcerts.pem'
export CAS_CLIENT_SSL_CA_LIST=${PEMPATH}
Run a program called get_token_2023.py. You'll need to provide several environment variables.
python get_token_2023.py $SASVIYAURL $CLIENT $CLIENTSECRET $SASUSER $SASPASS $PEMPATH
To install the Python packages listed in the requirements.txt file, execute the following command:
pip install -U -r requirements.txt
For additional context, if you're interested in the specific versions of LangChain, OpenAI, and SWAT packages used in the demo, you can retrieve them using these commands:
pip freeze | grep lang
pip freeze | grep openai
pip freeze | grep swat
python --version
At the time of writing this post, the versions of the Python packages I am using are as follows:
langchain==0.2.14
langchain-community==0.2.12
langchain-core==0.2.33
langchain-openai==0.1.22
langchain-text-splitters==0.2.2
langsmith==0.1.93
langchain-openai==0.1.22
openai==1.41.0
swat==1.14.0
Python 3.11.9
To execute the program, you'll need to provide your SAS Viya URL, which you've previously set as an environmental variable. Run the program by typing the following command:
python sasgenexswat.py $SASVIYAURL
Check out the video in SWAT Code Generation and Execution in SAS Viya with Azure OpenAI and LangChain for prompts and responses.
Start asking questions such as:
'https://support.sas.com/documentation/onlinedoc/viya/exampledatasets/cars.csv'
The SWAT code Generator-Executor (GenEx) custom agent approach is innovative, as it uses a large language model, GPT-4, to generate Python SWAT code based on user prompts. The custom agent is relying on just two main components: a SWAT code generator and a SWAT code executor. These tools work in tandem to run the generated SAS code within your chosen SAS Viya environment, managing both the output and any potential errors through the agent, which then crafts a final response.
SWAT code execution is the crucial step that makes large language model (LLM) code generation practical for SAS Viya users; it determines whether the code functions correctly in their environment.
Should an error arise, the agent is programmed to interpret the issue and attempt to regenerate the code, aiming for a successful execution. This can be a double-edged sword; while it's a source of strength, it can also be a weakness, as the custom agent may occasionally become stuck in a loop.
It's remarkable how just two hundred fifty lines of code can integrate enterprise analytics and data management platforms such as SAS Viya with AI platforms like Azure OpenAI. The linchpin that unites these diverse platforms remains open source.
I hope you found this article insightful. I encourage you to give it a try! Please feel free to reach out with feedback or suggestions for enhancing the agent or taking its capabilities to the next level.
Special thanks to Grace Liao from SAS Research & Development for inspiring me to try the approach.
Thanks to Peter Styliadis for his great SWAT Series.
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.
April 27 – 30 | Gaylord Texan | Grapevine, Texas
Walk in ready to learn. Walk out ready to deliver. This is the data and AI conference you can't afford to miss.
Register now and lock in 2025 pricing—just $495!
Still thinking about your presentation idea? The submission deadline has been extended to Friday, Nov. 14, at 11:59 p.m. ET.
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.