From 3188774a33a60ebe884b31ee50cd8a08c5c244d2 Mon Sep 17 00:00:00 2001 From: EricThomson Date: Wed, 4 Feb 2026 20:49:15 -0500 Subject: [PATCH 1/2] added github copilot demo initial draft --- lessons/07_AI_agents/05_github_copilot.md | 58 +++++++++ .../resources/copilot_agent_mode.jpg | Bin 0 -> 48582 bytes .../07_AI_agents/resources/mini-etl/README.md | 22 ++++ .../resources/mini-etl/data_sample.csv | 7 ++ .../resources/mini-etl/mini_etl.py | 81 ++++++++++++ .../resources/mini-etl/tests/test_mini_etl.py | 116 ++++++++++++++++++ 6 files changed, 284 insertions(+) create mode 100644 lessons/07_AI_agents/05_github_copilot.md create mode 100644 lessons/07_AI_agents/resources/copilot_agent_mode.jpg create mode 100644 lessons/07_AI_agents/resources/mini-etl/README.md create mode 100644 lessons/07_AI_agents/resources/mini-etl/data_sample.csv create mode 100644 lessons/07_AI_agents/resources/mini-etl/mini_etl.py create mode 100644 lessons/07_AI_agents/resources/mini-etl/tests/test_mini_etl.py diff --git a/lessons/07_AI_agents/05_github_copilot.md b/lessons/07_AI_agents/05_github_copilot.md new file mode 100644 index 0000000..2c2c31d --- /dev/null +++ b/lessons/07_AI_agents/05_github_copilot.md @@ -0,0 +1,58 @@ +# Paired Programming Demo with GitHub Copilot +In this practical demo, you will use GitHub Copilot to assist you in evaluating and improving a code project. + +## What is GitHub Copilot? +GitHub Copilot is a paired programming agent that can look over an entire code base (not just single files), work with VS Code via extensions in agentic mode, and help you write, fix, and improve code. It uses advanced AI models to understand the context of your code and provide relevant suggestions. It can be surprisingly effective at debugging and "understanding" code bases, making it a powerful tool for developers. This is one practical demonstration of agentic AI in action, and is a direction that you will probably see things move in the future. + +## Our project + The project we'll work with is a simple Python project that includes some functions and corresponding tests. The project, called `mini-etl` (located in `resources/`) performs basic ETL (Extract, Transform, Load) operations on CSV files. More specifically, given a CSV file with a date column and a value (sales) column, it has some simple column cleanup functions, a function to aggregate sales by date, and a "pipeline" function that ties everything together. + +The details for the project aren't that important, but what is important is that the code is *broken*, and we need some help fixing it. There are five tests that are supposed to validate the functionality of the code (in the projects `tests/` directory), but currently, all five tests are failing. Your task is to use GitHub Copilot to help you fix the code so that all tests pass. Additionally, you will ask GitHub Copilot to generate a Jupyter notebook that demonstrates the functionality of the `mini-etl` project. + +## Steps to follow + +> We recommend creating a copy of the `mini-etl` project before taking the following steps, as GitHub Copilot will be editing the code directly, and you might want to have the original code for reference later. You can always revert changes via git if you are using version control, but making a copy is often easier for demos like this. + +### Get tests passing +1. Sign up for GitHub Copilot online using your GitHub account (there is a free version that will give you a limited number of requests each month, which will be plenty for this demo). +2. In your VS Code IDE, install and enable the following extensions: `GitHub Copilot` and `GitHub Copilot Chat`. +3. Open your terminal and make sure your virtual environment is activated for Python 200, and navigate to the `mini-etl` project directory. Try running the main script: `python mini_etly.py`. It will generate errors. Also, run the test suite: `python -m pytest -q`. This package is a mess! +4. Hit `ctrl-alt-i` to open the interactive Copilot chat window (it will pop up on the right)and set it to "agent" mode. Then, at the bottom, you can select which LLM to use (e.g., Claude Haiku 4.5 is excellent). There will be a message box at the bottom that says "Describe what to build next". See the attached screenshot. + +![copilot_agent_mode](resources/copilot_agent_mode.jpg) + +5. Using your prompt-engineering skills, ask GitHub Copilot to fix the python package. Something like: + + You are in a small Python repo for creating simple ETL operations, and there are failing tests. + + Your task: make `python -m pytest -q` pass by editing `mini_etl.py` only. + Rules: + - Do NOT change anything in `tests/`. + - After each change, re-run tests and continue until all pass. + +Once you hit enter, GitHub Copilot will start analyzing your code and making suggestions. You can accept or reject the suggestions as they come in. Continue this process until all tests pass. It may require you to give it permission to edit files and other things along the way, it is quite interactive! +6. Go ahead and run the tests to confirm they all pass: `python -m pytest -q`. Also, run the main script to see that it works: `python mini_etl.py`. + +### Have it write a demo +Once the tests are passing, you can ask GitHub Copilot to generate a Jupyter notebook that demonstrates the functionality of the `mini-etl` package. In the same chat window, you can type something like: + + Now, please create a Jupyter notebook called `mini_etl_demo.ipynb` that demonstrates how to use the `mini-etl` package. The notebook should include: + - An introduction to the package + - Examples of how to use each function in the package + - A demonstration of the full ETL pipeline using sample data + +Try opening the generated notebook in Jupyter and see how well the code runs, are the explanations helpful and clear? If there are problems, does it fix them when you tell the agent what went wrong? + +Feel free to explore the repo, and continue tweaking it with GitHub Copilot's help, adding and improving functionality as you see fit! + +### Discussion +This demo is meant to showcase the potential of AI agents like GitHub Copilot in assisting with software development tasks. It moves way beyond simple code completion, and asking an LLM questions about code snippets. Here, the agent is able to understand the context of an entire code base, run tests, and make meaningful changes. It can be a powerful tool for developers, especially when working on complex projects or when trying to debug tricky issues. It can also help with generating documentation and demo materials, as we saw with the Jupyter notebook generation. + +One thing to consider is that the mini-etl project was intentionally kept very small and simple, partly so we would stay well within the limitations of the free tier of GitHub Copilot. + +Things may not be as neat and tidy when working on a huge sprawling code base. However, this demo should give you a taste of the potential of AI agents in software development. One thing to consider is just how easy it would be to simply accept all the changes that GitHub Copilot suggests without really understanding what is going on. This could lead to problems down the line if the code is not well understood by the developer. + +Especially when working with large-scale, project-wide code changes, it is extremely important to review and understand AI-generated code before acceptpting it. *Always treat it as a **first draft** that needs to be carefully reviewed and tested.* This is especially true for code in production settings where security, performance, and reliability are critical. + +There is a reason that we went through this demo nearly last in the Python data engineering sequence: at this point you have a strong enough foundation in Python, debugging, and general software development practices to be able to critically evaluate the code that GitHub Copilot generates. As AI agents become more prevalent in software development, these skills will be increasingly important. + diff --git a/lessons/07_AI_agents/resources/copilot_agent_mode.jpg b/lessons/07_AI_agents/resources/copilot_agent_mode.jpg new file mode 100644 index 0000000000000000000000000000000000000000..d3cdf48d3059fcd6221b49a94fc82b0a4fff510e GIT binary patch literal 48582 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 pd.Series: + """Clean a currency amount column. + + - strip whitespace + - remove "$" and "," characters + - convert to float + - invalid values become NaN + """ + s = s.astype("string").str.strip() + s = s.str.replace("$", "", regex=False) + + return pd.to_numeric(s, errors="coerce") + + +def parse_timestamp_series(s: pd.Series) -> pd.Series: + """Parse a timestamp column into pandas datetimes. + + - pd.to_datetime with errors="coerce" + - invalid values become NaT + """ + return pd.to_datetime(s, errors="ignore") + + +def summarize_daily(df: pd.DataFrame) -> pd.DataFrame: + """Summarize data by day. + + Expected behavior + - input df has columns "timestamp" (datetime) and "amount" (float) + - create "date" from timestamp (date only) + - group by date + - output columns: date, total_amount, num_rows + - total_amount: sum of amount (NaNs ignored) + - num_rows: number of rows in that date group (including rows with NaN amounts) + """ + out = df.copy() + out["date"] = out["timestamp"].dt.date + + grouped = out.groupby("date", as_index=False).agg( + total_amount=("amount", "sum"), + num_rows=("amount", "count"), # BUG: counts non-NaN only + ) + + return grouped.sort_values("date").reset_index(drop=True) + + +def run_pipeline(csv_path: str | Path) -> pd.DataFrame: + """ + Run all of our functions (mini pipeline) on a CSV: + First, read csv given by `csv_path`, then run our functions on the data: + + parse_time_stamp_series -> clean_amount_series -> summarize_daily + + """ + df = pd.read_csv(csv_path) + + # Normalize column names a tiny bit + df.columns = [c.strip().lower().replace(" ", "_") for c in df.columns] + + df["timestamp"] = parse_timestamp_series(df["timestamp"]) + df["amount"] = clean_amount_series(df["amount"]) + + # Drop rows where timestamp is missing (keeps the summary simple) + df = df.dropna(subset=["timestamp"]) + + return summarize_daily(df) + + +if __name__ == "__main__": + # Simple playground: edit this path if you want to try a different file. + csv_path = "data_sample.csv" + summary = run_pipeline(csv_path) + print(summary.to_string(index=False)) # don't print index column diff --git a/lessons/07_AI_agents/resources/mini-etl/tests/test_mini_etl.py b/lessons/07_AI_agents/resources/mini-etl/tests/test_mini_etl.py new file mode 100644 index 0000000..2f74323 --- /dev/null +++ b/lessons/07_AI_agents/resources/mini-etl/tests/test_mini_etl.py @@ -0,0 +1,116 @@ +import math +from pathlib import Path + +import pandas as pd +import pytest + +import mini_etl + + +def test_clean_amount_series_strips_dollar_commas_whitespace(): + # Input: [" $1,200.50 ", "9", "bad"] + # Output should become: [1200.50, 9.0, NaN] + s = pd.Series([" $1,200.50 ", "9", "bad"]) + out = mini_etl.clean_amount_series(s) + + assert out.iloc[0] == pytest.approx(1200.50) + assert out.iloc[1] == pytest.approx(9.0) + assert math.isnan(out.iloc[2]) + + +def test_parse_timestamp_series_coerces_invalid_to_nat(): + """ + parse_timestamp_series should turn timestamp strings into real datetimes. + + Two things should be true: + 1) The result should be a datetime-typed Series (not a Series of strings). + 2) Bad timestamps should not crash the function. They should become "missing". + + Pandas represents a missing datetime value as NaT ("Not a Time"). + pd.isna(...) returns True for NaT. + """ + s = pd.Series(["2024-01-01 10:00:00", "not-a-date"]) + out = mini_etl.parse_timestamp_series(s) + + # 1) The whole Series should be datetime dtype (so we can use .dt later) + assert str(out.dtype).startswith("datetime64"), f"Expected datetime dtype, got {out.dtype}" + + # 2) The invalid timestamp should become missing (NaT) + assert pd.isna(out.iloc[1]) + + +def test_summarize_daily_counts_rows_including_nan_amounts(): + # Input rows: + # 2024-01-01 10:00:00 amount=10.0 + # 2024-01-01 12:00:00 amount=NaN + # 2024-01-02 09:00:00 amount=5.0 + # + # Expected daily summary: + # 2024-01-01: total_amount=10.0, num_rows=2 (NaN ignored in sum, but row still counted) + # 2024-01-02: total_amount=5.0, num_rows=1 + df = pd.DataFrame( + { + "timestamp": pd.to_datetime( + ["2024-01-01 10:00:00", "2024-01-01 12:00:00", "2024-01-02 09:00:00"] + ), + "amount": [10.0, float("nan"), 5.0], + } + ) + + out = mini_etl.summarize_daily(df) + + row_0101 = out[out["date"] == pd.to_datetime("2024-01-01").date()].iloc[0] + assert row_0101["total_amount"] == pytest.approx(10.0) + assert row_0101["num_rows"] == 2 # counts rows, even if amount is NaN + + + +def test_run_pipeline_smoke(tmp_path): + # The pipeline should run end-to-end and return columns: + # ["date", "total_amount", "num_rows"] + here = Path(__file__).resolve().parent.parent + sample = here / "data_sample.csv" + dst = tmp_path / "data_sample.csv" + dst.write_text(sample.read_text(encoding="utf-8"), encoding="utf-8") + + out = mini_etl.run_pipeline(dst) + + # Expected columns + assert list(out.columns) == ["date", "total_amount", "num_rows"] + + # Expected dates (sorted) + assert list(out["date"]) == [ + pd.to_datetime("2024-01-01").date(), + pd.to_datetime("2024-01-02").date(), + ] + + # Expected number of summary rows + assert len(out) == 2 + + +def test_run_pipeline_expected_totals(tmp_path): + # Expected daily summary for the included sample CSV: + # + # 2024-01-01: 1200.50 + 9.00 - 5.00 = 1204.50 (3 rows) + # 2024-01-02: 3.00 + 2.00 = 5.00 (2 rows) + # + # Note: the row with "not-a-date" should be dropped by the pipeline. + here = Path(__file__).resolve().parent.parent + sample = here / "data_sample.csv" + dst = tmp_path / "data_sample.csv" + dst.write_text(sample.read_text(encoding="utf-8"), encoding="utf-8") + + out = mini_etl.run_pipeline(dst) + + expected = pd.DataFrame( + { + "date": [ + pd.to_datetime("2024-01-01").date(), + pd.to_datetime("2024-01-02").date(), + ], + "total_amount": [1204.50, 5.00], + "num_rows": [3, 2], + } + ) + + pd.testing.assert_frame_equal(out, expected) From d6f6d2622fb378f505bb4e8bb8922a46dd5e089b Mon Sep 17 00:00:00 2001 From: EricThomson Date: Wed, 4 Feb 2026 21:04:36 -0500 Subject: [PATCH 2/2] revisions to copilot demo --- lessons/07_AI_agents/05_github_copilot.md | 41 +++++++++++++---------- 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/lessons/07_AI_agents/05_github_copilot.md b/lessons/07_AI_agents/05_github_copilot.md index 2c2c31d..9711b3c 100644 --- a/lessons/07_AI_agents/05_github_copilot.md +++ b/lessons/07_AI_agents/05_github_copilot.md @@ -2,18 +2,20 @@ In this practical demo, you will use GitHub Copilot to assist you in evaluating and improving a code project. ## What is GitHub Copilot? -GitHub Copilot is a paired programming agent that can look over an entire code base (not just single files), work with VS Code via extensions in agentic mode, and help you write, fix, and improve code. It uses advanced AI models to understand the context of your code and provide relevant suggestions. It can be surprisingly effective at debugging and "understanding" code bases, making it a powerful tool for developers. This is one practical demonstration of agentic AI in action, and is a direction that you will probably see things move in the future. +GitHub Copilot is a paired programming agent that can look over an entire code base (not just single files), and help you write, fix, and improve code. It can work with VS Code via extensions and can be surprisingly effective at debugging and "understanding" a code base, making it a powerful tool for developers. This is one practical demonstration of agentic AI in action, and is a direction that you will probably see things move in the future. + +There are other similar tools out there, but GitHub Copilot integrates really nicely with VS Code, is easy to set up and use, and has a free tier that works very well for demonstration purposes. ## Our project - The project we'll work with is a simple Python project that includes some functions and corresponding tests. The project, called `mini-etl` (located in `resources/`) performs basic ETL (Extract, Transform, Load) operations on CSV files. More specifically, given a CSV file with a date column and a value (sales) column, it has some simple column cleanup functions, a function to aggregate sales by date, and a "pipeline" function that ties everything together. +For our demo we'll work with a simple package that includes just a few functions and corresponding tests. The project, called `mini-etl` (located in `resources/`) performs basic ETL (Extract, Transform, Load) operations on CSV files. More specifically, given a CSV file with a date and a value (sales) column, it has some simple column cleanup functions, a function to aggregate sales by date, and a "pipeline" function that ties everything together. -The details for the project aren't that important, but what is important is that the code is *broken*, and we need some help fixing it. There are five tests that are supposed to validate the functionality of the code (in the projects `tests/` directory), but currently, all five tests are failing. Your task is to use GitHub Copilot to help you fix the code so that all tests pass. Additionally, you will ask GitHub Copilot to generate a Jupyter notebook that demonstrates the functionality of the `mini-etl` project. +The details for the project aren't that important, but what is important is that the code is *broken*, and we need some help fixing it. -## Steps to follow +There are five tests that are supposed to validate the functionality of the code (in the projects `tests/` directory), but currently, all five tests are failing. Your task is to use GitHub Copilot to help you fix the code so that all tests pass. Additionally, you will ask GitHub Copilot to generate a Jupyter notebook that demonstrates the functionality of the `mini-etl` project. > We recommend creating a copy of the `mini-etl` project before taking the following steps, as GitHub Copilot will be editing the code directly, and you might want to have the original code for reference later. You can always revert changes via git if you are using version control, but making a copy is often easier for demos like this. -### Get tests passing +### First steps: setup and get tests passing 1. Sign up for GitHub Copilot online using your GitHub account (there is a free version that will give you a limited number of requests each month, which will be plenty for this demo). 2. In your VS Code IDE, install and enable the following extensions: `GitHub Copilot` and `GitHub Copilot Chat`. 3. Open your terminal and make sure your virtual environment is activated for Python 200, and navigate to the `mini-etl` project directory. Try running the main script: `python mini_etly.py`. It will generate errors. Also, run the test suite: `python -m pytest -q`. This package is a mess! @@ -23,36 +25,41 @@ The details for the project aren't that important, but what is important is that 5. Using your prompt-engineering skills, ask GitHub Copilot to fix the python package. Something like: +``` You are in a small Python repo for creating simple ETL operations, and there are failing tests. Your task: make `python -m pytest -q` pass by editing `mini_etl.py` only. Rules: - Do NOT change anything in `tests/`. - After each change, re-run tests and continue until all pass. +``` -Once you hit enter, GitHub Copilot will start analyzing your code and making suggestions. You can accept or reject the suggestions as they come in. Continue this process until all tests pass. It may require you to give it permission to edit files and other things along the way, it is quite interactive! -6. Go ahead and run the tests to confirm they all pass: `python -m pytest -q`. Also, run the main script to see that it works: `python mini_etl.py`. +Once you hit enter, GitHub Copilot will start analyzing your code and making suggestions. You can accept or reject the suggestions as they come in. Continue this process until all tests pass. Before it starts working, it may require you to give it permission to edit files and other things along the way: it is quite interactive! +6. It may take a while to finish. Once it says it has finished, go ahead and run the tests to confirm they all pass: `python -m pytest -q`. Also, run the main script to see that it works: `python mini_etl.py`. -### Have it write a demo +### Next, have it write a demo Once the tests are passing, you can ask GitHub Copilot to generate a Jupyter notebook that demonstrates the functionality of the `mini-etl` package. In the same chat window, you can type something like: +``` Now, please create a Jupyter notebook called `mini_etl_demo.ipynb` that demonstrates how to use the `mini-etl` package. The notebook should include: - - An introduction to the package + - A gentle introduction to the package that explains its purpose. - Examples of how to use each function in the package - A demonstration of the full ETL pipeline using sample data +``` +Try opening the generated notebook in Jupyter and see if the code cells run. Are the explanations helpful and clear? If there are problems, does it fix them when you tell the agent what went wrong? -Try opening the generated notebook in Jupyter and see how well the code runs, are the explanations helpful and clear? If there are problems, does it fix them when you tell the agent what went wrong? +Feel free to explore the package, and continue tweaking it with GitHub Copilot's help, adding and improving functionality as you see fit! -Feel free to explore the repo, and continue tweaking it with GitHub Copilot's help, adding and improving functionality as you see fit! +## Discussion +This demo is meant to showcase the potential of AI agents like GitHub Copilot in assisting with software development tasks. It moves way beyond one-line code compation, and asking an LLM questions about code snippets. Rather, the agent is given an entire *project* as context, and is able to run tests, rewrite code, to make meaningful improvements to a project. -### Discussion -This demo is meant to showcase the potential of AI agents like GitHub Copilot in assisting with software development tasks. It moves way beyond simple code completion, and asking an LLM questions about code snippets. Here, the agent is able to understand the context of an entire code base, run tests, and make meaningful changes. It can be a powerful tool for developers, especially when working on complex projects or when trying to debug tricky issues. It can also help with generating documentation and demo materials, as we saw with the Jupyter notebook generation. +In addition to helping debug or develop new tools, it can also help with generating documentation and demo materials, as we saw with the Jupyter notebook generation. -One thing to consider is that the mini-etl project was intentionally kept very small and simple, partly so we would stay well within the limitations of the free tier of GitHub Copilot. +One thing to consider is that the mini-etl project was intentionally kept *very* small and simple, partly so we would stay well within the limitations of the free tier of GitHub Copilot. -Things may not be as neat and tidy when working on a huge sprawling code base. However, this demo should give you a taste of the potential of AI agents in software development. One thing to consider is just how easy it would be to simply accept all the changes that GitHub Copilot suggests without really understanding what is going on. This could lead to problems down the line if the code is not well understood by the developer. +Things may not be as neat and tidy when working on a huge sprawling code base with multiple subcomponents. Also, consider just how easy it would be to simply accept all the changes that GitHub Copilot suggests without really understanding what is going on. This could lead to problems down the line if the code is not well understood by the developer. What if some of the changes lead to problems downstream and you didn't understand the changes? -Especially when working with large-scale, project-wide code changes, it is extremely important to review and understand AI-generated code before acceptpting it. *Always treat it as a **first draft** that needs to be carefully reviewed and tested.* This is especially true for code in production settings where security, performance, and reliability are critical. +Especially when working with large-scale, project-wide code changes, it is extremely important to review and understand AI-generated code before accepting it. *Always treat it as a **first draft** from a junior associate that needs to be carefully reviewed and tested.* This is doubly true for code in production settings where security, performance, and reliability are critical. -There is a reason that we went through this demo nearly last in the Python data engineering sequence: at this point you have a strong enough foundation in Python, debugging, and general software development practices to be able to critically evaluate the code that GitHub Copilot generates. As AI agents become more prevalent in software development, these skills will be increasingly important. +There is a reason that we went through this demo nearly last in the Python data engineering sequence: at this point you have a strong foundation in Python, debugging, and general software development practices: you can critically evaluate the code that tools like GitHub Copilot generates. As AI agents become more prevalent in software development, the abilithy to critically evaluate AI-generated code will be increasingly important.