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.
Are you ready for the spotlight? We're accepting content ideas for SAS Innovate 2025 to be held May 6-9 in Orlando, FL. The call is open until September 25. Read more here about why you should contribute and what is in it for you!
Data Literacy is for all, even absolute beginners. Jump on board with this free e-learning and boost your career prospects.