Just show me the code!
As always, if you don’t care about the post I have uploaded the source code on my Github.

Today I had the necessity to document a series of C# files. In C#, you document your code adding XML on top of classes and their methods, this XML documentation proves beneficial for developers as well as for Visual Studio.

Documenting code is not a task that excites me too much, so I’ve thought, what if we use OpenAI to generate the XML comments? But, I didn’t want to copy and paste the content of every class into ChatGPT back and forth, so I’ve been wondering how long it would take to build an app capable of doing it.

I didn’t want to build anything too complex, just drag a C# file into the app, click a “Generate XML comments” button, copy the output, and done.

As I began coding the app, I thought to incorporate a few other functionalities that can be easily achieved using GPT-4. These included providing code improvement suggestions, explaining code functionality, and generating unit tests for the given C# file. And that’s precisely what I ended up building.

Nowadays, the Generative AI landscape is growing at a tremendous pace, with new announcements almost daily. For instance, OpenAI recently introduced functions, Microsoft now has multiple “Copilots.” Databases with vector capabilities are proliferating, as well as databases that offer semantic search capabilities. New versions of libraries like LangChain continue to be released on a daily basis, improved or new LLM models keep appearing regularly. And the list of new things seems to go on and on and on.

It may appear overwhelming to keep up with all these advancements, but even with just the basic GenAI fundamentals, you can build some useful tools.

This app serves as an example of it, I put it together in just a couple of hours and it is nothing more than a bunch of static prompts that are being sent to OpenAI and a few UI components for displaying the responses generated.

How the app works

The operation of this app is really simple.

  • There is an UI component that allow us to upload a C# file.
  • After successfully uploading the file in the app, you will have access to four available options:
    • Add XML comments to the provided C# file.
    • Explain the code.
    • Offer suggestions for code improvements.
    • Generate Unit Tests for the given C# file.

Each of these options follows the same set of steps:

  • A specific prompt is defined based on the chosen option.
  • The source code of the uploaded C# file is incorporated into the prompt.
  • The resulting prompt is sent to Azure OpenAI GPT-4 for processing.
  • The generated response from Azure OpenAI is displayed on the screen.

Here’s a screenshot of how the end result will look like:

app-homescreen

Building the app

Before proceeding, I would like to emphasize an important point (or at least I think so):

  • OpenAI and LLM models are an incredibly valuable tool, but it’s important not to blindly rely on its results.
    Before using any output generated by this app in a real-world scenario, review it throughly.

1. Deploying a GPT-4 model on Azure OpenAI

In order for the app to work correctly, it is necessary to deploy either a gpt-4 or a gpt-4-32k model in our Azure OpenAI instance beforehand.

The GPT-4 model will be used to generate the responses to the user interactions.

To deploy it: Go to the Azure Portal > Search for the Azure OpenAI service > Navigate to Model Deployments > Click Manage Deployments > Click Create new deployment and deploy either a gpt-4 or gpt-4-32k model.

The deployment name can be whatever you want.

deploy-gpt4-model

2. Constructing the prompts

Keep in mind that this app was developed in a couple of hours. Consequently, the prompts used to guide GPT-4 can still be refined and improved to yield better results.

The core (and most interesting) aspect of this app, lies in the prompts we utilize to guide GPT-4’s behavior. Let’s delve into these prompts and examine them closely.

Within each prompt, you will notice the placeholder {csharp_file_content}. This is where the app will insert the content of the C# file uploaded by the user.

Prompt for adding XML comments to a C# class

Given a csharp file content, you must add elaborate and high quality XML comments for any method, class, enum or interface. 
Don't add comments on anything else. Exclude anything else when trying to add comments, for example do not try to add XML comments on class properties or class constants.
If any method, class, enum or interface already contains an XML comment, then try to improve it.
You must respond only with the generated csharp code and nothing else.

csharp file content:
{csharp_file_content}

Explain code prompt

Explain in an easy and concise way what the given csharp code is doing. 
The response must be in markdown format and contain only a list of bullet points.

csharp code:
{csharp_file_content}

Suggest code improvement prompt

Given a csharp file content, try to suggest no more than 2 or 3 quality code improvements.
Don't mention anything about code comments.
Do not show any source code. Just respond with a list of 2 or 3 suggestions.
If you don't have any suggestion or you don't have enough information, then respond saying only: no suggestions found."

csharp code:
{csharp_file_content}

Prompt for creating unit tests of a given C# class

Given a csharp file content, try to create some Unit Tests.
Only create Unit Tests if it those might be valuable for the application.
The tests should be using xUnit and Moq.
Follow a naming standard of Given_x_When_y_Then_z.
If you decide that creating the unit tests make sense, then respond only with the generated csharp code and nothing else.
If you decide that creating the unit tests doesn't make sense, then respond saying only: No unit tests available for this file.

csharp file content:
{csharp_file_content}

3. Sending the prompts to GPT-4 and retrieving the response

In the previous section, we constructed the prompts for GPT-4.

Now, in this section, we will develop a few Python functions responsible for sending those prompts to Azure OpenAI and receiving the corresponding responses. To accomplish this, we will utilize the ChatCompletion.create method from the openai Python package.

GPT-4 expects messages to be formatted as a conversation. The messages parameter should be an array of dictionaries that represent a conversation structured by roles.

The system role also known as the system message must be included at the beginning of the array. This message provides the initial instructions to the model.

After the system role, you can include a series of messages between the user and the assistant, in this case we are adding the prompt that we have built in the previous section.

import openai
import os

xml_comments_prompt = '''
Given a csharp file content, you must add elaborate and high quality XML comments for any method, class, enum or interface. 
Don't add comments on anything else. Exclude anything else when trying to add comments, for example do not try to add XML comments on class properties or class constants.
If any method, class, enum or interface already contains an XML comment, then try to improve it.
You must respond only with the generated csharp code and nothing else.

csharp file content:
{csharp_file_content}
'''

explain_prompt = '''
Explain in an easy and concise way what the given csharp code is doing. 
The response must be in markdown format and contain only a list of bullet points.

csharp code:
{csharp_file_content}
'''

suggestions_prompt = '''
Given a csharp file content, try to suggest no more than 2 or 3 quality code improvements.
Don't mention anything about code comments.
Do not show any source code. Just respond with a list of 2 or 3 suggestions.
If you don't have any suggestion or you don't have enough information, then respond saying only: no suggestions found."

csharp code:
{csharp_file_content}
'''

unit_tests_prompt = '''
Given a csharp file content, try to create some Unit Tests.
Only create Unit Tests if it those might be valuable for the application.
The tests should be using xUnit and Moq.
Follow a naming standard of Given_x_When_y_Then_z.
If you decide that creating the unit tests make sense, then respond only with the generated csharp code and nothing else.
If you decide that creating the unit tests doesn't make sense, then respond saying only: No unit tests available for this file.

csharp file content:
{csharp_file_content}
'''

def generate_xml_comments(code):
    
    prompt = xml_comments_prompt.format(csharp_file_content=code)
    message = _get_messages(prompt)

    response = openai.ChatCompletion.create(
        engine=_get_llm_model(),
        messages = message,
        temperature=0.5,
        max_tokens=8000,
    )

    return response.choices[0].message.content.strip()

def explain_code(code):

    prompt = explain_prompt.format(csharp_file_content=code)
    message = _get_messages(prompt)

    response = openai.ChatCompletion.create(
        engine=_get_llm_model(),
        messages = message,
        temperature=0.5,
        max_tokens=8000,
    )

    return response.choices[0].message.content.strip()

def suggest_code_improvements(code):

    prompt = suggestions_prompt.format(csharp_file_content=code)
    message = _get_messages(prompt)

    response = openai.ChatCompletion.create(
        engine=_get_llm_model(),
        messages = message,
        temperature=0,
        max_tokens=8000,
    )

    return response.choices[0].message.content.strip()

def generate_unit_tests(code):
    
    prompt = unit_tests_prompt.format(csharp_file_content=code)
    message = _get_messages(prompt)

    response = openai.ChatCompletion.create(
        engine=_get_llm_model(),
        messages = message,
        temperature=0.3,
        max_tokens=8000,
    )

    return response.choices[0].message.content.strip()

def _get_llm_model():
    return os.getenv('AZURE_OPENAI_GPT4_MODEL_NAME')

def _get_messages(prompt):
    message = [
        {"role": "system", "content":  "You are an app assistant trying to improve the source code of a csharp file." },
        {"role": "user", "content": prompt}
    ]
    return message

4. Building the User Interface using Streamlit

The user interface we’ll create will be very straightforward, consisting of the following components:

  • An upload widget that allows users to drag and drop or select a C# file from their computer.
  • A code block to display the content of the uploaded file.
  • Four buttons: one for adding XML comments to the uploaded C# file, another for explaining the code, one for receiving suggestions on how to improve the code, and the last one for generating unit tests.

To build the user interface I’m using Streamlit. I decided to use Streamlit because I can build a simple and functional UI with just a few lines of Python.

The next image showcases how the user interface will look once it is fully built.

app-homescreen

Now, let’s take a look at the source code and then I’ll try to explain the most relevant parts.

import streamlit as st
import openai
import os
import codecs
from dotenv import load_dotenv
from llm import explain_code, suggest_code_improvements, generate_xml_comments, generate_unit_tests

load_dotenv()

if os.getenv('AZURE_OPENAI_APIKEY') is None:
    st.error("AZURE_OPENAI_APIKEY not set. Please set this environment variable and restart the app.")
if os.getenv('AZURE_OPENAI_BASE_URI') is None:
    st.error("AZURE_OPENAI_BASE_URI not set. Please set this environment variable and restart the app.")
if os.getenv('AZURE_OPENAI_GPT4_MODEL_NAME') is None:
    st.error("AZURE_OPENAI_GPT4_MODEL_NAME not set. Please set this environment variable and restart the app.")

openai.api_type = "azure"
openai.api_base = os.getenv('AZURE_OPENAI_BASE_URI')
openai.api_version = "2023-05-15"
openai.api_key = os.getenv('AZURE_OPENAI_APIKEY')

def clear_state():
    for key in st.session_state.keys():
        del st.session_state[key]

def read_csharp_file(file):
    content = file.read()
    decoded_content = codecs.decode(content, 'utf-8')
    return decoded_content

st.title("CSharp GPT-4 file enhancer")

uploaded_file = st.file_uploader(label="Add a csharp file", type=["cs"], accept_multiple_files=False, on_change=clear_state)
if uploaded_file is not None:
    
    csharp_code = read_csharp_file(uploaded_file)
        
    with st.expander("Source code"):
        st.code(csharp_code, language='csharp')

    if st.button("Add XML comments"):
        with st.spinner("Generating XML comments..."):
            if 'xml_comments_csharp_code' in st.session_state.keys():
                with st.expander("Source code with XML comments"):
                    st.code(st.session_state['xml_comments_csharp_code'], language='csharp')   
            else:
                with st.expander("Source code with XML comments"):
                    xml_comments_csharp_code = generate_xml_comments(csharp_code)
                    xml_comments_csharp_code = xml_comments_csharp_code.strip("```").lstrip("csharp").strip()
                    st.code(xml_comments_csharp_code, language='csharp')          
                    st.session_state['xml_comments_csharp_code'] = xml_comments_csharp_code

    if st.button("Explain code"):
        with st.spinner("Explaining code..."):
            if 'csharp_code_explained' in st.session_state.keys():
                st.markdown(st.session_state['csharp_code_explained'])
            else:
                csharp_code_explained = explain_code(csharp_code)
                st.markdown(csharp_code_explained)
                st.session_state['csharp_code_explained'] = csharp_code_explained

    if st.button("Suggest code improvements"):
        with st.spinner("Searching for improvements..."):
            if 'csharp_code_improvements' in st.session_state.keys():
                st.markdown(st.session_state['csharp_code_improvements'])
            else:
                csharp_code_improvements = suggest_code_improvements(csharp_code)
                st.markdown(csharp_code_improvements)
                st.session_state['csharp_code_improvements'] = csharp_code_improvements

    if st.button("Generate unit tests"):
         with st.spinner("Trying to generate Unit Tests..."):
            if 'unit_tests_csharp_code' in st.session_state.keys():
                with st.expander("Unit Tests source code"):
                    st.code(st.session_state['unit_tests_csharp_code'], language='csharp')   
            else:
                with st.expander("Unit Tests source code"):
                    unit_tests_csharp_code = generate_unit_tests(csharp_code)
                    unit_tests_csharp_code = unit_tests_csharp_code.strip("```").lstrip("csharp").strip()
                    st.code(unit_tests_csharp_code, language='csharp')          
                    st.session_state['unit_tests_csharp_code'] = unit_tests_csharp_code

There isn’t much to comment on here since the code is quite straightforward. Nevertheless, let’s delve into the two or three things that are worth mentioning.

  • The first step is configure the openai python package to work with our specific instance of Azure OpenAI.

The Azure OpenAI API key and uri are being set utilizing environment variables.

if os.getenv('AZURE_OPENAI_APIKEY') is None:
    st.error("AZURE_OPENAI_APIKEY not set. Please set this environment variable and restart the app.")
if os.getenv('AZURE_OPENAI_BASE_URI') is None:
    st.error("AZURE_OPENAI_BASE_URI not set. Please set this environment variable and restart the app.")
if os.getenv('AZURE_OPENAI_GPT4_MODEL_NAME') is None:
    st.error("AZURE_OPENAI_GPT4_MODEL_NAME not set. Please set this environment variable and restart the app.")

openai.api_type = "azure"
openai.api_base = os.getenv('AZURE_OPENAI_BASE_URI')
openai.api_version = "2023-05-15"
openai.api_key = os.getenv('AZURE_OPENAI_APIKEY')
  • To simplify the file uploading process in our application, we utilize the file_uploader component from Streamlit. This component allows users to upload files with just one line of code.

Each time a user uploads a new file, the clear_state function is triggered. Our application utilizes the Streamlit session’s state to cache responses from Azure OpenAI, resulting in a reduced number of calls made to the API.

Why caching the responses from Azure OpenAI?
The caching mechanism serves a purpose: let’s say you upload a C# file and request GPT-4 to generate some unit tests. It wouldn’t make sense to call GPT-4 again if you try to generate unit tests for the same file a second time, it would be an unnecessary token expenditure.

def clear_state():
    for key in st.session_state.keys():
        del st.session_state[key]

...

uploaded_file = st.file_uploader(label="Add a csharp file", type=["cs"], accept_multiple_files=False, on_change=clear_state)
  • The final step is to utilize the button component from Streamlit to render buttons for each functionality.

It’s important to note that whenever a button is pressed, we call the corresponding function we built in the previous section. For example, when the Add XML comments button is pressed, the function generate_xml_comments(csharp_code) is invoked to generate XML comments for the provided C# file.

Additionally, we leverage the Streamlit session’s state to cache responses from Azure OpenAI. This caching mechanism serves the same purpose as I explained in the previous point, allowing us to reduce the number of unnecesary calls made to the API.


  if st.button("Add XML comments"):
        with st.spinner("Generating XML comments..."):
            if 'xml_comments_csharp_code' in st.session_state.keys():
                with st.expander("Source code with XML comments"):
                    st.code(st.session_state['xml_comments_csharp_code'], language='csharp')   
            else:
                with st.expander("Source code with XML comments"):
                    xml_comments_csharp_code = generate_xml_comments(csharp_code)
                    xml_comments_csharp_code = xml_comments_csharp_code.strip("```").lstrip("csharp").strip()
                    st.code(xml_comments_csharp_code, language='csharp')          
                    st.session_state['xml_comments_csharp_code'] = xml_comments_csharp_code
...

Testing the app

Now, let’s test if the app works correctly.

  • When you upload a C# file, the source code can be viewed in a code block.

app-upload

  • When you run the “Add XML comments” functionality, the result is displayed in another code block.

app-xml-comments-result

  • Here’s the explanation given by GPT-4 when you run the “Explain code” functionality.

app-explain-code

  • Here’s an example of the suggestions given by GPT-4 when you run the “Suggest code improvements” functionality.

app-suggestions

  • When you run the “Generate unit tests” functionality, the result is displayed on another code block.

app-unit-test

  • If you try to generate unit tests from a file which makes no sense (e.g. Program.cs), the response will be that no tests are available.

app-no-unit-test