<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Brett Fitzgerald</title><link>https://brettgfitzgerald.com/</link><description>Recent content on Brett Fitzgerald</description><generator>Hugo -- gohugo.io</generator><language>en-us</language><atom:link href="https://brettgfitzgerald.com/index.xml" rel="self" type="application/rss+xml"/><item><title>Building a Medallion Data Pipeline in Azure with Data Factory, Databricks, and Power BI</title><link>https://brettgfitzgerald.com/posts/first-steps-in-azure/</link><pubDate>Tue, 14 Apr 2026 16:10:56 -0400</pubDate><guid>https://brettgfitzgerald.com/posts/first-steps-in-azure/</guid><description>&lt;h2 id="why"&gt;Why?&lt;/h2&gt;
&lt;p&gt;I have done very little work in Microsoft Azure. I&amp;rsquo;m very familiar with Google Cloud Platform and the data analytics and AI tools available there, and I&amp;rsquo;m somewhat familiar with similar AWS offerings. But there&amp;rsquo;s a technical gap in my Microsoft side. I wanted a quick project to gain some familiarity with the platform. The goal will be to do some simple data transformations and analysis on publicly available data, specifically.&lt;/p&gt;
&lt;p&gt;To that end, I used two 2023 MEPS files: one that captures person-level demographics, coverage, and expenditures, and another that captures office-based visits at the event level. From there, I shaped the raw data into a simple bronze/silver/gold workflow and used it to analyze cost drivers, utilization trends, and high-cost member concentration. This was all done in a &lt;a href="https://github.com/controversy187/healthcare_data"&gt;local Jupyter notebook&lt;/a&gt;. Now I&amp;rsquo;m ready to recreate this in Azure.&lt;/p&gt;
&lt;h2 id="the-plan"&gt;The Plan&lt;/h2&gt;
&lt;p&gt;Here&amp;rsquo;s the plan. I have the two Excel files, and I&amp;rsquo;ll need to place them in a storage account in Azure. From there, I&amp;rsquo;ll use a Data Factory to process them into Parquet files. One awesome thing about Azure is that I can use Parquet files directly as external tables in Unity Catalog, so I&amp;rsquo;ll register those, then process them into silver and gold Delta tables. From there, I can connect Power BI to the Gold datasets to visualize my business insights.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Architectural Diagram" loading="lazy" src="https://brettgfitzgerald.com/posts/first-steps-in-azure/00-architecture-diagram.png"&gt;&lt;/p&gt;
&lt;h2 id="landing-raw-data-in-azure"&gt;Landing raw data in Azure&lt;/h2&gt;
&lt;p&gt;First off, after creating an Azure account and logging in, I create a Resource Group. This seems like it&amp;rsquo;s similar to a Project in GCP. A logical grouping of virtual machines, databases, services, etc. A group of resources. So the name Resource Group makes sense. Seems to be a pretty straightforward process: Find resource groups on the left sidebar menu, click &amp;ldquo;Create&amp;rdquo;, and type in a Name. It defaulted to the only subscription available, which I assume matters for billing purposes at some point. Select a Region and hit Review + Create.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Resource Group" loading="lazy" src="https://brettgfitzgerald.com/posts/first-steps-in-azure/01-resource-group.jpg"&gt;&lt;/p&gt;
&lt;p&gt;I don&amp;rsquo;t know what data is sensitive in Azure, so I&amp;rsquo;ll block out what makes sense to me :)&lt;/p&gt;
&lt;p&gt;Next up, I need a storage account. This will act like a data lake for my demo. So, I click Home, then choose &amp;ldquo;Storage accounts&amp;rdquo; from my left menu. Hit create, it pre-populates the fields it can (subscription and my newly created resource group), and then I need to come up with a globally unique storage account name. This seems strange to me. Since it&amp;rsquo;s part of a resource group, it seems that a storage account would naturally be namespaced to the resource group. I&amp;rsquo;m sure it will make sense later. For now, I&amp;rsquo;m not going to share my storage account name, since I don&amp;rsquo;t know if they&amp;rsquo;re secret. I change my redundancy to Local, since I&amp;rsquo;m just messing around and can be cheap. One option I change is up in the advanced tab, I enable hierarchical namespace. That&amp;rsquo;s what makes this available as an Azure Data Lake Storage Gen2 account, which is what I think I want. Review + Create, and I&amp;rsquo;ve got a storage account!&lt;/p&gt;
&lt;p&gt;&lt;img alt="Storage Account" loading="lazy" src="https://brettgfitzgerald.com/posts/first-steps-in-azure/02-storage-account.jpg"&gt;&lt;/p&gt;
&lt;p&gt;Now that I have a storage account, I need a container. This will be the actual &amp;ldquo;data lake&amp;rdquo;. Following &amp;ldquo;Data Storage&amp;rdquo; to &amp;ldquo;Containers&amp;rdquo; then clicking &amp;ldquo;Add a container&amp;rdquo; gets me where I need to be. I apparently already have a container called $logs, which makes me happy because who doesn&amp;rsquo;t love solid logging! Adding a container is simple, just give a name. Mine&amp;rsquo;s called &amp;ldquo;lake&amp;rdquo;. Once it&amp;rsquo;s created, create &lt;code&gt;raw/meps/2023/&lt;/code&gt;. I saw a message that says that this directory won&amp;rsquo;t actually be created until there are items in it. So I&amp;rsquo;ll upload my two raw data files from MEPs, which I named &lt;code&gt;h248g.xlsx&lt;/code&gt; and &lt;code&gt;h251.xlsx&lt;/code&gt;. Those are the MEPS Office-Based Medical Provider Visits file (2023) and MEPS Full-Year Consolidated file (2023), respectively.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Files uploaded" loading="lazy" src="https://brettgfitzgerald.com/posts/first-steps-in-azure/03-file-upload.jpg"&gt;&lt;/p&gt;
&lt;h2 id="creating-compute-orchestration-and-excel-ingestion"&gt;Creating compute, orchestration, and Excel ingestion&lt;/h2&gt;
&lt;p&gt;Great! I&amp;rsquo;ve got my raw data in its original format, in my Azure data container. Time for my first data transformation in Azure! For this, I&amp;rsquo;ll use Azure Data Factory. I search for &amp;ldquo;Data Factory&amp;rdquo; in the search bar and select &amp;ldquo;Data Factories&amp;rdquo;. I guess Azure likes plurals here. I hit &amp;ldquo;create&amp;rdquo;, select my subscription and resource group, and give it a name. Turns out that this has to be globally unique, so be mindful of that. Review + Create button, and I&amp;rsquo;ve got a Data Factory! Interestingly, once the factory is created, it takes me back to my resource group, and it shows both my factory and my storage account. So my resource group really does seem to be a just a collection of all the resources I&amp;rsquo;ve created.&lt;/p&gt;
&lt;p&gt;Before that, I need to create an Azure Databricks Workspace. Searching the top bar for Databricks gets me there pretty easily. Easy clicks, basically selecting the defaults and giving the workspace a name. It took a couple of minutes and was successful. Now I&amp;rsquo;ll go back to the Data Factory I built earlier and launch the studio. From here, I can create a linked service for Azure Data Lake Storage Gen2. This was enabled a few steps ago when we created the storage account and checked the box for hierarchical namespace. When I try to test the connection, though, I get an error that this endpoint doesn&amp;rsquo;t support BlobStorageEvents or SoftDelete. So I navigate back to my storage account, turn off blob soft delete and container soft delete, and hit Save.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Disable Soft Delete" loading="lazy" src="https://brettgfitzgerald.com/posts/first-steps-in-azure/04-disable-soft-delete.jpg"&gt;&lt;/p&gt;
&lt;p&gt;Going back and trying to create the Azure Data Factory linked service to the Azure Data Lake Storage Gen2 worked this time! That gives my factory access to my storage account. That means I can read my raw Excel files, and I have the ability to write out my bronze data set. It&amp;rsquo;s not doing anything like that right now, but it&amp;rsquo;s defining that connection.&lt;/p&gt;
&lt;p&gt;Next, I&amp;rsquo;m going to create a linked service in my factory to Azure Databricks, which will, in turn, trigger Databricks notebooks after the Excel ingestion step. So, on my linked services page in my Data Factory, I click &amp;ldquo;+ New&amp;rdquo; at the top. I select Azure Databricks, but I hit another blocker. Apparently, I need an Access Token for my Databricks workspace.&lt;/p&gt;
&lt;p&gt;So I navigate to the Databricks I created earlier, I find Access Tokens in the developer settings. I set a max lifetime of 120, and then select All APIs for the API scope, because I don&amp;rsquo;t yet know exactly what APIs I&amp;rsquo;ll be needing. Then I copy my API token and save it in a safe place, since I&amp;rsquo;ll only be able to see it at this time.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Access Token" loading="lazy" src="https://brettgfitzgerald.com/posts/first-steps-in-azure/05-access-token.jpg"&gt;&lt;/p&gt;
&lt;p&gt;Back on my Azure Databricks Linked Service page in my data factory, I paste in my access token. Then I can select my Cluster Version, Cluster node type, and I went with Python 3 because that&amp;rsquo;s what I&amp;rsquo;m more familiar with these days. Hitting Test Connection resulted in a success, so I hit Create!&lt;/p&gt;
&lt;p&gt;Now, back to my Data Factory page, I create a new Excel dataset, since that is what my source files are. I had to generate a smaller version of the excel file with 250 rows chosen at random. The original file was too large for the connector to parse to determine the schema, so the smaller sample file will allow it to understand the structure of the data.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Create Dataset" loading="lazy" src="https://brettgfitzgerald.com/posts/first-steps-in-azure/06-create-dataset.jpg"&gt;&lt;/p&gt;
&lt;p&gt;I repeat the process for the visits dataset, and then publish them both. I validate the schema for the visits dataset. The person dataset has too many columns to display. Previewing the data looks pretty good, too!&lt;/p&gt;
&lt;p&gt;Now it&amp;rsquo;s time to create the bronze sink datasets in Azure Data Factory. We&amp;rsquo;re going to go with Parquet instead of CSV for this because it&amp;rsquo;s a little more data-friendly for both analytics and preserving data types. I create them the same way that I loaded the .xlsx datasets, but I&amp;rsquo;m not defining a schema for them, since the files don&amp;rsquo;t exist yet. Now we have our source files and bronze data sinks defined.&lt;/p&gt;
&lt;p&gt;We&amp;rsquo;re getting very close now, so it&amp;rsquo;s time to build the Data Factory ingestion pipeline. In our Data Factory, we go to Add New Resource, Pipeline, Pipeline. I&amp;rsquo;m calling it &lt;code&gt;pl_meps_excel_to_bronze&lt;/code&gt;. I add a Copy Data block and set the source to &lt;code&gt;ds_excel_meps_person_2023&lt;/code&gt; and the sink to &lt;code&gt;ds_bronze_meps_person_parquet&lt;/code&gt;, since that&amp;rsquo;s what I named my source and sink. For the copy behavior, I chose Preserve Hierarchy. I repeat these steps to create a second Copy Data block. They can run in parallel, so I don&amp;rsquo;t need to orchestrate them any differently from what they are. I hit the validate and debug buttons to make sure things are working properly. The person copy took significantly longer than the visits copy, but in the end they both came back successfully.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Copy Data Test" loading="lazy" src="https://brettgfitzgerald.com/posts/first-steps-in-azure/07-build-pipeline.jpg"&gt;&lt;/p&gt;
&lt;p&gt;Once those tests work, I triggered them for real. It took my Vists copy a little over a minute to run, and my Person copy clocked in around seven and a half minutes. Going back to my dataset in my Data Factory, I chose my person parquet dataset, selected Preview, and voilà! I see my Bronze data!&lt;/p&gt;
&lt;p&gt;&lt;img alt="Parquet Preview" loading="lazy" src="https://brettgfitzgerald.com/posts/first-steps-in-azure/08-parquet-preview.jpg"&gt;&lt;/p&gt;
&lt;h2 id="build-silver-data-layers"&gt;Build Silver data layers&lt;/h2&gt;
&lt;p&gt;To actually start manipulating the data, we need to use Databricks. After opening the Databricks workspace, I create a compute resource. It&amp;rsquo;s pretty straightforward, and I didn&amp;rsquo;t change any of the options. That should give me resources to run notebooks on. From there, I hit the Workspace link on the left, create a directory called &lt;code&gt;healthcare_demo&lt;/code&gt;, and then create notebooks for each step of my translation.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Notebook Setup" loading="lazy" src="https://brettgfitzgerald.com/posts/first-steps-in-azure/09-notebook-setup.jpg"&gt;&lt;/p&gt;
&lt;p&gt;Now that I&amp;rsquo;m starting to actually build things, I need to make sure that they stay organized. For that, I&amp;rsquo;m turning to Unity Catalog. I create three schemas, &lt;code&gt;bronze&lt;/code&gt;, &lt;code&gt;silver&lt;/code&gt;, and &lt;code&gt;gold&lt;/code&gt;. Once that is complete, I write my first code in my Bronze notebook to read in the parquet files that my data facto
ry created. Before I can execute it, though, I need to set up my access.&lt;/p&gt;
&lt;p&gt;A search for &amp;ldquo;Access Connector for Azure Databricks&amp;rdquo; at the top of my account gets me where I need to go. After creating a new connector for my resource group, I open my storage account and go to IAM. From there, add my newly created access connector as a member. Now, back in the Catalog, I hit the gear icon and choose &amp;ldquo;Credentials&amp;rdquo;. Clicking Create Credential brings me to a screen where I can give it a name and paste in the Resource ID from the Access Connector we just created.&lt;/p&gt;
&lt;p&gt;Finally, I need to create the external location, so back in my Catalog, I hit my gear icon and go to External Locations and Create External Location. I give it the most amazing name I can think of, use the location &lt;code&gt;abfss://lake@sthealthcaredemo12345.dfs.core.windows.net/bronze/meps/2023/&lt;/code&gt; (yours might not match mine), and choose the storage credential I just created. When I tried this, I got an error that Hierarchical Namespace was not enabled, so I went back to my Storage Account, and, sure enough, Hierarchical Namespace was disabled. I clicked it and it brought me to a walkthrough to enable the feature.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Hierarchical Namespace" loading="lazy" src="https://brettgfitzgerald.com/posts/first-steps-in-azure/10-hierarchical-namespace.jpg"&gt;&lt;/p&gt;
&lt;p&gt;The upgrade went smooth. It warned me that it could take several hours, so I went and did other things for a while. Interestingly, when I came back to it a half hour later, there was an ajax error on the page, but navigating back to the configuration page showed that Hierarchical Namespace is now enabled. So, back, to creating the external location, and&amp;hellip; success? Kind of? I got a message about File Events Permissions not being verified. I&amp;rsquo;m not quite sure that means, so mental bookmark, and I click &amp;ldquo;Force Create&amp;rdquo;. Great Success!&lt;/p&gt;
&lt;p&gt;Now, since I already have my bronze parquet files and I now have an external location, I want to register those as external tables in Unity Catalog. After going to the SQL Editor, I just run the following SQL calls:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;CREATE SCHEMA IF NOT EXISTS demo_healthcare.bronze;
&lt;/code&gt;&lt;/pre&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;CREATE EXTERNAL TABLE IF NOT EXISTS demo_healthcare.bronze.meps_person_raw
USING PARQUET
LOCATION &amp;#39;abfss://lake@sthealthcaredemo12345.dfs.core.windows.net/bronze/meps/2023/person/&amp;#39;;
&lt;/code&gt;&lt;/pre&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;CREATE EXTERNAL TABLE IF NOT EXISTS demo_healthcare.bronze.meps_office_visits_raw
USING PARQUET
LOCATION &amp;#39;abfss://lake@sthealthcaredemo12345.dfs.core.windows.net/bronze/meps/2023/office_visits/&amp;#39;;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;When I go to the Catalog, I can now browse my parquet files as SQL tables! At this point, I want to test access from my notebooks to the tables, and not read the raw parquet files. I rewrite my first cell to be&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;bronze_person_df = spark.table(&amp;#34;demo_healthcare.bronze.meps_person_raw&amp;#34;)
bronze_office_visits_df = spark.table(&amp;#34;demo_healthcare.bronze.meps_office_visits_raw&amp;#34;)
print(&amp;#34;Bronze tables loaded successfully.&amp;#34;)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;and after it runs, I can access the data in a dataframe like I&amp;rsquo;m used to!&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;print(&amp;#34;bronze_person_df row count:&amp;#34;, bronze_person_df.count())
print(&amp;#34;bronze_office_visits_df row count:&amp;#34;, bronze_office_visits_df.count())
&amp;gt; bronze_person_df row count: 18920
&amp;gt; bronze_office_visits_df row count: 135096
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Interestingly, my bronze row data now shows 18920 records for the person dataset. When I ran my analysis locally, it showed 18919 rows. This is likely just a header row discrepancy, but it&amp;rsquo;s best to make sure.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;display(bronze_person_df.limit(10))&lt;/code&gt; shows me &lt;code&gt;['Prop_0', 'Prop_1', 'Prop_2', 'Prop_3', 'Prop_4']&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Sure enough, the service autogenerated column names because it interpreted the first row as data, not as headers. I must have missed a checkbox somewhere. Back in my Data Factory, I go to Author &amp;gt; Datasets and choose my Person dataset. Frustratingly, &amp;ldquo;First row as header&amp;rdquo; is unchecked, so I check it, go to the Schema tab, and upload my sample I originally used. I see the proper header names now. I double-check that it&amp;rsquo;s checked for the office visits dataset as well. Also, I go to my Parquet dataset for Person, and import the schema again, since that will now be different, and then I hit Publish All. Back to my pipeline, and I trigger them again to generate new parquet files, and I run my SQL to recreate the bronze SQL dataset, since the schema changed. That didn&amp;rsquo;t actually fix it, but when I deleted the SQL table for the Person data and created a new one, appending v2 to the end of it, it worked. Interesting quirk.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Fixed SQL sources" loading="lazy" src="https://brettgfitzgerald.com/posts/first-steps-in-azure/11-fixed-sql.jpg"&gt;&lt;/p&gt;
&lt;p&gt;Finally! I have our bronze data landed in my system and accessible. In the silver layer, I&amp;rsquo;m looking to have the data cleaned up. Consistent, readable, correct typing, and easy to build gold tables later, which will be used to answer various business questions.&lt;/p&gt;
&lt;p&gt;This will be a lot of SQL and Python in a notebook, so I&amp;rsquo;m not going to add all the code here. At some point, I might add it to my GitHub repo so you can see what I&amp;rsquo;m doing. Reach out and let me know if you&amp;rsquo;re interested in that. For now, I&amp;rsquo;ll be working in my 2nd Notebook, since the first is basically loading the data from the Bronze (raw) dataset and validating that it came through the pipeline ok.&lt;/p&gt;
&lt;p&gt;Basically, in my notebook, I do the following:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Load the bronze tables&lt;/li&gt;
&lt;li&gt;Inspect the tables, just to validate that they are what I&amp;rsquo;m expecting&lt;/li&gt;
&lt;li&gt;Normalize the column names to lowercase to make things easier downstream&lt;/li&gt;
&lt;li&gt;Create new dataframes from the large datasets, selecting only the columns I need&lt;/li&gt;
&lt;li&gt;Build the &lt;code&gt;silver.member&lt;/code&gt; dataset with the following criteria:
&lt;ul&gt;
&lt;li&gt;Rename technical source fields to business-friendlier names&lt;/li&gt;
&lt;li&gt;Keep the raw code columns for traceability&lt;/li&gt;
&lt;li&gt;Add decoded label columns for readability&lt;/li&gt;
&lt;li&gt;Derive age_band
&lt;img alt="Silver Member" loading="lazy" src="https://brettgfitzgerald.com/posts/first-steps-in-azure/12-silver-member.jpg"&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Similarly, build the &lt;code&gt;silver.office_visit&lt;/code&gt; dataset
&lt;ul&gt;
&lt;li&gt;One row per visit&lt;/li&gt;
&lt;li&gt;Keep both charge and expenditure&lt;/li&gt;
&lt;li&gt;Preserve coded values and friendly labels&lt;/li&gt;
&lt;li&gt;Create a usable date&lt;/li&gt;
&lt;li&gt;Add visit_count = 1 to make later aggregation easy
&lt;img alt="Silver Office Visit" loading="lazy" src="https://brettgfitzgerald.com/posts/first-steps-in-azure/13-silver-office-visit.jpg"&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Check data for weird occurrences of nulls, obviously incorrect row counts, or unexpected values&lt;/li&gt;
&lt;li&gt;Create my schema, if it doesn&amp;rsquo;t already exist&lt;/li&gt;
&lt;li&gt;Write the data to the tables&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;So, a decent amount of data manipulation, and then I write out data to the tables. Fortunately, my spot check of data looked fine, and my row counts matched between inputs and outputs, which was expected. So I now have a Silver dataset!&lt;/p&gt;
&lt;p&gt;&lt;img alt="Silver Datasets" loading="lazy" src="https://brettgfitzgerald.com/posts/first-steps-in-azure/14-silver-datasets.jpg"&gt;&lt;/p&gt;
&lt;h2 id="build-gold-data-layers"&gt;Build Gold data layers&lt;/h2&gt;
&lt;p&gt;I&amp;rsquo;m on a roll now, so it&amp;rsquo;s time to start shaping the data into business-ready outputs. Example questions I might be able to answer are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Who are the high-cost members?&lt;/li&gt;
&lt;li&gt;How does utilization change month to month?&lt;/li&gt;
&lt;li&gt;Which segments drive the most spend?&lt;/li&gt;
&lt;li&gt;What patterns would a benefits stakeholder care about?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Similar to the silver dataset, I&amp;rsquo;m not going to drop all my code in here. I&amp;rsquo;ll just summarize my notebook. Here&amp;rsquo;s what I do:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Load the Silver tables.&lt;/li&gt;
&lt;li&gt;Build &lt;code&gt;gold.member_annual_spend&lt;/code&gt;. This gives me insights such as:
&lt;ul&gt;
&lt;li&gt;annual spend&lt;/li&gt;
&lt;li&gt;annual visit count&lt;/li&gt;
&lt;li&gt;segment fields&lt;/li&gt;
&lt;li&gt;a high-cost flag (is this person in the top 10% of spenders?)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Build &lt;code&gt;gold.monthly_utilization&lt;/code&gt;. Shift from event-level rows to month-level business summary.&lt;/li&gt;
&lt;li&gt;Build &lt;code&gt;gold.spend_by_segment&lt;/code&gt;. Comparison of age bands, insurance groups, and regions.&lt;/li&gt;
&lt;li&gt;Build &lt;code&gt;gold.high_cost_members&lt;/code&gt;. Just a filtered view of members who cross the 10% threshold.&lt;/li&gt;
&lt;li&gt;Write the Gold tables so they are accessible to the business.&lt;/li&gt;
&lt;li&gt;Validate the Gold layer.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This is a far less technical exercise and more of a business analytical exercise. I tried to ask simple, but relevant, questions that these two original datasets might help answer. After crafting the datasets around this analysis, I chose to write them as Gold datasets so that we can start querying them directly, instead of having to compute the analysis every time. In production, I would be more aware of the context of the questions. Does this data need to be real- or near-realtime? Then we might benefit from calculating the Gold data at the time of analysis, assuming that our Silver dataset is changing frequently.&lt;/p&gt;
&lt;p&gt;Now that I have my notebooks written that translate from Bronze to Silver to Gold, I want to build out my whole pipeline. Currently, I&amp;rsquo;m only using my data factory to copy from the Excel files into parquet files, which are then mapped to the bronze delta tables. Let&amp;rsquo;s mature this pipeline a bit!&lt;/p&gt;
&lt;h2 id="building-the-data-pipeline"&gt;Building the data pipeline&lt;/h2&gt;
&lt;p&gt;I go back to my Data Factory. After loading my Pipeline, I find the activity for Notebook, drag that onto my pipeline, and name it &lt;code&gt;nb_build_silver&lt;/code&gt;. I browse to my 02_build_silver notebook that I built and select it. After that, I repeat those steps but load in my 03_build_gold notebook instead. Once all my components are in, I start connecting them. Dragging the checkmark from each of my Copy Data blocks to my nb_build_silver block ensures that they &lt;em&gt;both&lt;/em&gt; run before the silver block executes. Then, I drag the checkmark from Silver to Gold. Because I&amp;rsquo;m using the checkmark spot, these will only continue execution when the previous block is successful. If this were production, I would have a validation notebook that would check row counts, existence of required data (high-cost threshold), etc., and have that execute. For now, we&amp;rsquo;ll just rename the pipeline to &lt;code&gt;pl_meps_excel_to_gold&lt;/code&gt; and run it!&lt;/p&gt;
&lt;p&gt;&lt;img alt="End to End Pipeline" loading="lazy" src="https://brettgfitzgerald.com/posts/first-steps-in-azure/15-end-to-end-pipeline.jpg"&gt;&lt;/p&gt;
&lt;p&gt;Oh no! When I ran it in debug, my nb_build_silver activity failed with the message &lt;code&gt;Standard_DS3_v2 is currently not available in location 'eastus'.&lt;/code&gt; I&amp;rsquo;m not sure exactly why this happened, but I make a mental note to revisit this later. In the meantime, I open up my Azure Databricks Linked Service in the data factory and switch the &amp;ldquo;Select Cluster&amp;rdquo; from &amp;ldquo;New Job Cluster&amp;rdquo; to &amp;ldquo;Existing Interactive Cluster&amp;rdquo; and select the cluster that exists there. I&amp;rsquo;m not sure what this is, or why it&amp;rsquo;s different, but I&amp;rsquo;m looking forward to understanding this whole process better.&lt;/p&gt;
&lt;p&gt;I debug the pipeline again and let it run. This time it ran successfully! My silver notebook took over 13 minutes to run, which seems slow to me. When I ran it from the notebook itself, each cell took less than a minute to run. In production, I&amp;rsquo;d probably pare down this notebook to not have as many sanity checks and simply be data processing. For now, I&amp;rsquo;ll claim victory in completing a full end-to-end pipeline that takes raw Excel files and transforms them into business-ready Delta tables! Next up, Power BI visualizations!&lt;/p&gt;
&lt;h2 id="power-bi-visualizations"&gt;Power BI Visualizations&lt;/h2&gt;
&lt;p&gt;First, I make sure that I have Power BI Desktop installed. Then, I go into my Databricks workspace, go to the Marketplace, and search for Power BI Desktop Partner Connect Integration. I connect it to my cluster, and then download the Connection file. This opens in Power BI. I use Ubuntu as my daily driver, but I&amp;rsquo;m using my Windows installation for these steps. Once I&amp;rsquo;m logged in and connected, I choose my gold Delta tables registered in Databricks, specifically&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;demo_healthcare.gold.member_annual_spend&lt;/li&gt;
&lt;li&gt;demo_healthcare.gold.monthly_utilization&lt;/li&gt;
&lt;li&gt;demo_healthcare.gold.spend_by_segment&lt;/li&gt;
&lt;li&gt;demo_healthcare.gold.high_cost_members&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;As I select these, I&amp;rsquo;m pretty excited to see the data that I&amp;rsquo;ve curated appearing in a desktop application. Intellectually, I now know how it all works, but it&amp;rsquo;s still pretty neat to see the fruit of my labor appearing here. I hit the Load button and it starts populating the data.&lt;/p&gt;
&lt;p&gt;It turns out that adding simple visualizations in Power BI is&amp;hellip; well&amp;hellip; simple! I won&amp;rsquo;t go into detail here, since my goal was to focus on the Azure side of data analytics.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Power BI Visualizations" loading="lazy" src="https://brettgfitzgerald.com/posts/first-steps-in-azure/16-power-bi-visualizations.jpg"&gt;&lt;/p&gt;
&lt;p&gt;From this, we can see that if someone is paying more than $4,704 per month, they are in the top 10% of members. Our largest spenders are the 65+ age bracket, which makes sense. Interestingly, the largest total spend is from those under 65 with private insurance. This could be due to the total number of members in that group, rather than their average expenditures. Further analysis would be necessary, and really quite simple with the data that I&amp;rsquo;ve curated!&lt;/p&gt;
&lt;h2 id="wrapping-it-all-up"&gt;Wrapping it all up&lt;/h2&gt;
&lt;p&gt;All around, it was a joy to work in Azure. I hit a couple of &amp;ldquo;gotchas&amp;rdquo; that I&amp;rsquo;m still digging into, such as the timeouts on my compute, and having to create a new Delta table to house my corrected schema when I accidentally included my column headers as data. I learned about storage locations in Azure, processing data pipelines with a Data Factory, managing the three layers of data (Bronze, Silver, and Gold), and how to go from raw data into useful insights in an automated fashion. There are several areas for production hardening here, like logging, validation of my Data Factory steps, notifications, scheduling and triggers, etc. But for now, I&amp;rsquo;m pretty happy with what I&amp;rsquo;ve built in pretty short order!&lt;/p&gt;</description></item><item><title>Project Management with Gemini CLI</title><link>https://brettgfitzgerald.com/posts/project-management-with-gemini-cli/</link><pubDate>Wed, 11 Feb 2026 00:00:00 +0000</pubDate><guid>https://brettgfitzgerald.com/posts/project-management-with-gemini-cli/</guid><description>&lt;h2 id="project-or-task-management"&gt;Project (or Task) Management&lt;/h2&gt;
&lt;p&gt;In my day job, I work as a product owner for an Advanced Analytics / Data Science team. It&amp;rsquo;s a bunch of really smart people that use many analytical techniques including machine learning, statistical models, and a whole host of other tools. We work with teams across most of the business to help understand markets, demand, supply chain, sales&amp;hellip; just about everything. Generally speaking, we have a lot of stakeholders and several projects in-flight at any given time. Much of the work we do is proof-of-concept with new tools, or building and refining analytical engines to help accelerate business areas. The nature of this work usually involves multiple feedback cycles and waiting to hear from our business stakeholders on the effectiveness of our work. That leads to many projects in flight and in various states of their POC -&amp;gt; MVP -&amp;gt; Iterate -&amp;gt; Deliver-to-supporting-team lifecycle.&lt;/p&gt;
&lt;p&gt;With so many projects in flight, it&amp;rsquo;s very easy for me to lose sight of the details within the projects, and what next-best action I need to be focused on, any given day. While I generally &amp;ldquo;get&amp;rdquo; the big picture, I&amp;rsquo;m less of a detail-oriented mind, and things can slip through the cracks.&lt;/p&gt;
&lt;h2 id="efforts-up-until-now"&gt;Efforts Up Until Now&lt;/h2&gt;
&lt;p&gt;I wrote earlier about how I was &lt;a href="https://brettgfitzgerald.com/posts/how-i-used-google-adk-and-ai-agents-to-take-control-of-my-todoist-backlog/"&gt;experimenting with Google&amp;rsquo;s Agent Development Kit&lt;/a&gt; and having it interact with Todoist for managing my tasks. This was great, but it lacked context and prioritization. It quickly grew to become unmanageable. In my personal life, I use &lt;a href="https://obsidian.md/"&gt;Obsidian.md&lt;/a&gt; to manage my &amp;ldquo;&lt;a href="https://amzn.to/4ammFzO"&gt;second brain&lt;/a&gt;&amp;rdquo; using the &lt;a href="https://fortelabs.com/blog/para/"&gt;PARA&lt;/a&gt; method of organization. It&amp;rsquo;s nice because I can quickly capture ideas for later processing. I tried using this for work, but once something was logged and dealt with, I struggled with follow-up tasks bubbling back up. Even worse, if I was waiting for a reply to something, it would likely get lost in my notes and never be seen again.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m also a fan of David Allen&amp;rsquo;s book, &amp;ldquo;&lt;a href="https://amzn.to/3OAkaT4"&gt;Getting Things Done&lt;/a&gt;&amp;rdquo;. This idea of having an inbox for ideas and tasks to be sorted and scheduled into specific actionable areas resonates with me. I have leveraged this system in the past with mild success. It still didn&amp;rsquo;t do a great job of handling &amp;ldquo;waiting for&amp;hellip;&amp;rdquo; types of tasks. I could file a follow-up to be addressed in a week, but I get a response the following day, I would struggle to find the task that I scheduled out. It became more managing my task manager than actually getting things done.&lt;/p&gt;
&lt;p&gt;Today, I&amp;rsquo;ve sort of mashed techniques and tools together to create an abomination of productivity.&lt;/p&gt;
&lt;h2 id="paraigtd"&gt;PARAIGTD&lt;/h2&gt;
&lt;p&gt;Don&amp;rsquo;t use that name. It&amp;rsquo;s just a mashup of letters to show that I&amp;rsquo;m using the PARA method of organization, combined with the Getting Things Done method of getting things done, and leveraging AI to help me keep it all organized. I have a DASHBOARD.md file that lists my current projects in either an &amp;ldquo;in progress&amp;rdquo; or &amp;ldquo;backlog&amp;rdquo; state, and all the immediately actionable tasks associated with those projects. Each project has its own directory and dashboard. Those individual dashboards have ToDo lists of tasks, organized with the Obsidian Tasks plugin, which is how I have my master list of actionable tasks on my master Dashboard. Meeting transcripts are archived in those project directories as well. Because this is all done in markdown, it makes it very easy to leverage a CLI agent to take action.&lt;/p&gt;
&lt;p&gt;My workflow is something like this:&lt;/p&gt;
&lt;p&gt;Each morning, when I start work, I load up &lt;a href="https://github.com/google/gemini-cli"&gt;Gemini CLI&lt;/a&gt; in the directory of my Obsidian vault which corresponds to my work. I tell it my calendar for the day (future improvement, build a skill to allow read-only access to my calendar), and then ask it for help prioritizing my tasks. The agent churns for a bit, and then gives me a suggestion for how to structure my day and the most impactful work I can accomplish. I compare this to my dashboard, and get to work!&lt;/p&gt;
&lt;p&gt;Throughout the day, as emails, chats, or meetings happen, I drop either the outcomes of those, or direct copy/pastes of conversations and transcriptions into the agent and ask it to update my project files. It does a decent job of identifying the project, marking off tasks, adding new ones, and updating the project documentation. New tasks get a &amp;ldquo;due date&amp;rdquo; of a reasonable date, which ensures that they bubble up in the future. Additionally, the agent is able to examine the project files very quickly and update future tasks that might have taken me a considerable amount of time to locate and manage.&lt;/p&gt;
&lt;p&gt;At the end of each day, I conduct an end-of-day audit with the agent, log what was decided and completed, and update any future tasks that need to be revised.&lt;/p&gt;
&lt;h2 id="whats-next"&gt;What&amp;rsquo;s next?&lt;/h2&gt;
&lt;p&gt;I&amp;rsquo;m not sure. I keep a GEMINI.md file up to date with how I want the agent to act, and I&amp;rsquo;m frequently tweaking that. Here&amp;rsquo;s an excerpt:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;## 1. Role &amp;amp; Persona
You are a **Executive Obsidian Assistant** combining David Allen&amp;#39;s *Getting Things Done* (GTD) with Tiago Forte&amp;#39;s *Second Brain*.
* **Primary Goal:** Maintain a &amp;#34;State of Flow&amp;#34; for the user (Brett). Ensure every item in the system has a home and a next action.
* **Method:** You manage the Obsidian Vault structure, ensuring links are valid and the `DASHBOARD.md` is always actionable.
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;That file is constantly evolving, which is why I&amp;rsquo;m not pasting it in its entirety. I&amp;rsquo;m sure if you wanted to, you could copy this blog post into Gemini, ChatGPT or whatever and ask it to generate an AGENTS.md file for your own use, and give it a shot. Let me know how it goes!&lt;/p&gt;</description></item><item><title>When Context Clicks: The Power of Anchor Moments at Work</title><link>https://brettgfitzgerald.com/posts/when-context-clicks/</link><pubDate>Thu, 16 Oct 2025 11:35:05 -0400</pubDate><guid>https://brettgfitzgerald.com/posts/when-context-clicks/</guid><description>&lt;h2 id="when-context-clicks"&gt;When context clicks&lt;/h2&gt;
&lt;p&gt;I had one of those conversations today where, at first, nothing landed. We were discussing whether we could allow orders for items still in transit by estimating when they&amp;rsquo;d arrive, be received, and be ready to pick so that customers could buy them before the warehouse technically had them &amp;ldquo;in stock.&amp;rdquo;&lt;/p&gt;
&lt;p&gt;I was tracking along, but not connecting it&amp;hellip; until one term surfaced: ATP. I didn&amp;rsquo;t know what ATP was until someone explained it me.&lt;/p&gt;
&lt;p&gt;At Gordon Food Service, ATP (Available to Promise) refers to product that&amp;rsquo;s been unloaded at the loading dock but hasn&amp;rsquo;t yet been slotted into a pick location. In other words: present at the distribution center, not yet pickable. This concept instantly transported me back to a &lt;a href="https://www.instacart.com/company/instacart-business/introducing-will-call-delivery-a-fast-reliable-solution-for-supply-chain-challenges/"&gt;rush-order pilot I worked on last year with Instacart&lt;/a&gt;. We were doing something novel: enabling rush orders directly from our Distribution Centers. Along the way we discovered a subtle trap: some items were &amp;ldquo;available to promise,&amp;rdquo; but not actually available to pick. They&amp;rsquo;d been received at the dock but not slotted. Customers could order them; associates couldn&amp;rsquo;t pick them. Same building, different reality.&lt;/p&gt;
&lt;p&gt;That prior experience became the anchor. The moment ATP came up today, the whole discussion clicked into place. The new concept, &amp;ldquo;sell in-transit inventory when timing makes sense,&amp;rdquo; latched onto an old lesson: &amp;ldquo;availability states matter.&amp;rdquo; My mental model updated with this connection immediately.&lt;/p&gt;
&lt;h2 id="anchor-moments-and-conceptual-adjacency"&gt;Anchor Moments and &amp;ldquo;Conceptual Adjacency&amp;rdquo;&lt;/h2&gt;
&lt;p&gt;I think of these as Anchor Moments: a familiar node in your experience graph that lets a new idea connect quickly and securely. You can also think of it like a &amp;ldquo;birthday paradox for concepts.&amp;rdquo; In the birthday paradox, you don&amp;rsquo;t need 366 people to find two with the same birthday. With just 23, there&amp;rsquo;s a 50% chance two share a birthday. Similarly, as your set of real-world experiences grows, the odds of a useful connection to an existing experience or concept (an anchor), rise quickly. Once that connection appears, learning accelerates.&lt;/p&gt;
&lt;p&gt;Over the past three years at GFS, I&amp;rsquo;ve collected a lot of individualized experiences across teams and domains. Each one felt isolated at first. But the more nodes I add, the more often I feel that &amp;ldquo;click.&amp;rdquo; That&amp;rsquo;s when I can add real value. By seeing how decisions in one domain ripple into another, or by recognizing a pattern early because I&amp;rsquo;ve seen its shadow somewhere else.&lt;/p&gt;
&lt;h2 id="why-this-matters-beyond-a-nice-feeling"&gt;Why this matters (beyond a nice feeling)&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Speed&lt;/strong&gt;: Anchor Moments compress onboarding and analysis time. You ramp faster because your brain has more attachment points.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Quality&lt;/strong&gt;: You spot edge cases sooner (e.g., &amp;ldquo;available to purchase&amp;rdquo; != &amp;ldquo;available to pick&amp;rdquo;).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Trust&lt;/strong&gt;: When you can translate across domains such as ops, data science, product, etc. people experience you as a connector, not a bottleneck.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="how-leaders-can-manufacture-more-anchor-moments"&gt;How leaders can manufacture more Anchor Moments&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Deliberate rotations&lt;/strong&gt;: Short stints shadowing adjacent teams (receiving, slotting, picking, replenishment, transportation planning) build sticky context. I had the joy of riding along with one of our truck drivers a couple months ago, and that experience created countless &amp;ldquo;anchors&amp;rdquo; in my mental model.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Journaling&lt;/strong&gt;: Write these findings down. Tell the story (see what I&amp;rsquo;m doing here?).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Concept glossary&lt;/strong&gt;: Maintain a living glossary in the team&amp;rsquo;s workspace. Define acronyms and show how terms differ across systems (&amp;ldquo;ATP&amp;rdquo; may mean different things to different groups. Call it out).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Story bank&lt;/strong&gt;: Capture short &amp;ldquo;field notes&amp;rdquo; from projects: the problem, the insight, the fix. These become future anchors for others.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="closing"&gt;Closing&lt;/h2&gt;
&lt;p&gt;We often think expertise is about mastering more facts. In practice, it&amp;rsquo;s about building a graph of experiences so new information has somewhere to attach. That&amp;rsquo;s what today&amp;rsquo;s conversation reminded me: when context clicks, contribution follows.&lt;/p&gt;</description></item><item><title>From Idea to Play Store: Shipping an AI Image Editor with Gemini + Firebase</title><link>https://brettgfitzgerald.com/posts/building-an-ai-image-generator/</link><pubDate>Mon, 06 Oct 2025 10:13:12 -0400</pubDate><guid>https://brettgfitzgerald.com/posts/building-an-ai-image-generator/</guid><description>&lt;h2 id="chasing-nano-banana"&gt;Chasing Nano Banana&lt;/h2&gt;
&lt;p&gt;When Google quietly dropped the Gemini image model codenamed &amp;ldquo;Nano Banana&amp;rdquo; (Google’s Gemini 2.5 Flash Image model) and only exposed it through an API, I was disappointed that I couldn&amp;rsquo;t immediately test it out. Since I&amp;rsquo;ve been enjoying building things recently, first thought was, &amp;ldquo;I&amp;rsquo;ll just build the app I want.&amp;rdquo; I wanted a fast way to play with the model, optionally add a reference photo, and see what kind of edits it could muster. That became Picture Wizard.&lt;/p&gt;
&lt;h2 id="early-experiments"&gt;Early experiments&lt;/h2&gt;
&lt;p&gt;I&amp;rsquo;ve never really built an Android app, except for a few tutorials in the past. I&amp;rsquo;ve had decent success vibe coding, but I wasn&amp;rsquo;t sure how to integrate Cursor with Android Studio, or if Cursor could even reliably build an Android app. It turned out to not be an issue. Cursor was great for quick iteration, and I could still control the build process through Android Studio. I used it strictly for compiling and emulator runs while Cursor and I filled in the features.&lt;/p&gt;
&lt;h2 id="planning-with-copilots"&gt;Planning with copilots&lt;/h2&gt;
&lt;p&gt;As the surface area grew, I realized I found Cursor providing quick, but unreliable changes. Many features for overarchitected, versioning was inconsistent, and managing Android dependencies seemed to challenge the editor. I iterated on the workflow I developed when I was building &lt;a href="https://adeptli.org"&gt;Adeptli&lt;/a&gt;. I chose a feature, described it to Codex, and asked for an implementation plan. I found most of my time was spent iterating through the development of the implementation plan, which I shopped around to ChatGPT, Gemini, Codex, and Gemini-CLI. It was interesting to see the different perspectives each model had on the plan. In the end, I took all those inputs and synthesized something that I felt good about, before starting a build, with an agent.&lt;/p&gt;
&lt;h2 id="getting-real-with-firebase"&gt;Getting real with Firebase&lt;/h2&gt;
&lt;p&gt;Most of my learning curve was on Firebase. I wired up Auth for Google sign-in, built Firestore collections for generations, and leaned on Storage for the image pipeline. Every time I thought something was done, I found another edge case. Billing retries, storage permissions, security rules. It was fun to learn and explore, and Firebase really makes a lot of the backend simple. After a couple of days, I had a single-activity Jetpack Compose app that could accept a prompt, optionally reuse a reference, and display Nano Banana&amp;rsquo;s handiwork side by side.&lt;/p&gt;
&lt;div style="display: flex; gap: 20px; margin: 20px 0; flex-wrap: wrap;"&gt;
&lt;div style="flex: 1; min-width: 300px;"&gt;
&lt;figure&gt;
&lt;img loading="lazy" src="images/7ec8cc42f4be4ba8df80773769ef2674-d_d.webp"
alt="Original image from Zillow"/&gt; &lt;figcaption&gt;
&lt;p&gt;Original image from Zillow&lt;/p&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/div&gt;
&lt;div style="flex: 1; min-width: 300px;"&gt;
&lt;figure&gt;
&lt;img loading="lazy" src="images/PictureWizard_1759847807246_add_a_small_deck_to_this_house.png"
alt="Porch-friendly remix of the same house"/&gt; &lt;figcaption&gt;
&lt;p&gt;Porch-friendly remix of the same house&lt;/p&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Those two shots are from a prompt my wife and I obsessed over: &amp;ldquo;What if this house had a porch?&amp;rdquo; Picture Wizard made it stupidly easy to experiment, and suddenly we were iterating on renovation ideas every night.&lt;/p&gt;
&lt;h2 id="the-beta-gauntlet"&gt;The beta gauntlet&lt;/h2&gt;
&lt;p&gt;Then came Google&amp;rsquo;s reality check: you need twelve active testers and a fourteen-day soak before the Play Store will even look at a production submission. I scrambled to find volunteers, set up Firebase app distribution, and spent the waiting period shipping polish. I kept pushing builds, collecting feedback through email and text messages, and watching the roadmap evolve in real time. The testers were amazing. One friend cartoonified vacation photos, another lasered his son&amp;rsquo;s portrait after he stripped out the background with the app. Every share was proof that the tool was more than a tech demo.&lt;/p&gt;
&lt;h2 id="launch-and-life"&gt;Launch and life&lt;/h2&gt;
&lt;p&gt;Once the clock expired, I hit submit and Picture Wizard went live. The release notes were almost anticlimactic compared to the wild beta cycle, but the payoff came at home. My wife uses it constantly while we evaluate houses, sketching patios, porches, and new siding in minutes. That feedback loop, seeing her light up, then hearing from testers whose imaginations ran wild, made every late-night debugging session worth it.&lt;/p&gt;
&lt;h2 id="whats-next"&gt;What&amp;rsquo;s next&lt;/h2&gt;
&lt;p&gt;Now that the app is out there, I&amp;rsquo;m focused on tightening the loop between idea and iteration: better gallery tools, richer sharing, and more guidance for people who are new to generative image prompts. Nano Banana already feels less mysterious, and Picture Wizard keeps finding new ways to be useful. I&amp;rsquo;m just happy I followed the urge to build when the API landed. This one has been a lot of fun!&lt;/p&gt;
&lt;p&gt;Check out &lt;a href="https://play.google.com/store/apps/details?id=com.brettfitzgerald.picturewizard&amp;amp;pcampaignid=blog_post"&gt;Picture Wizard&lt;/a&gt;, and give some feedback on the &lt;a href="https://discord.gg/CwBQMuzT"&gt;discord&lt;/a&gt; channel!&lt;/p&gt;</description></item><item><title>Building a SaaS Product: The Hidden 80% That Nobody Talks About</title><link>https://brettgfitzgerald.com/posts/building-a-saas-product-the-hidden-80-percent/</link><pubDate>Fri, 11 Jul 2025 00:00:00 +0000</pubDate><guid>https://brettgfitzgerald.com/posts/building-a-saas-product-the-hidden-80-percent/</guid><description>&lt;blockquote&gt;
&lt;p&gt;&amp;ldquo;I&amp;rsquo;ve got a great idea for a weekend project!&amp;rdquo;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Two months later&amp;hellip;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&amp;ldquo;I&amp;rsquo;m launching my weekend project!&amp;rdquo;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="the-dream-vs-the-reality"&gt;The Dream vs. The Reality&lt;/h2&gt;
&lt;p&gt;I want to give anyone the power to learn anything, however they want to. That was the idea. With the Age of A.I. changing the way we think and work, I thought this would be a great time to get started turning that project into a reality. As the technology advances, so can my solution. So I whipped up a proof-of-concept in a couple of days, thought to myself &amp;ldquo;that&amp;rsquo;s not bad!&amp;rdquo; and decided to launch it to get feedback. I just needed to wrap up a couple of technical details so other people could play with it, and I&amp;rsquo;d be all set, getting valuable user feedback!&lt;/p&gt;
&lt;p&gt;Those technical details? Infrastructure, security, user management, payments, hosting, and a hundred other things that users never see but absolutely need for the product to work in the real world.&lt;/p&gt;
&lt;h2 id="the-20-bespoke-learning-plans-to-learn-anything-affordably"&gt;The 20%: Bespoke learning plans to learn anything, affordably&lt;/h2&gt;
&lt;p&gt;This is the fun part. This is what gets you excited about building the product in the first place. For me, this included:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The opportunity&lt;/strong&gt;: I wanted a place that a user could describe what they wanted to learn and what they already know, and the system would generate a learning plan to get them from where they are to where they are going. No need to pay for an expensive course where you already know a quarter of the material. Or you pay for a course and it&amp;rsquo;s too far beyond your current skillset to engage with. I want to give people a custom-tailored plan to accomplish their learning goals. And I want to do it for cheap. Don&amp;rsquo;t pay for a course that you don&amp;rsquo;t complete. Only pay for the portions that you work through.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Technical Implementation&lt;/strong&gt;: I wanted to play with current A.I. technologies. Not just for doing my own work, but for help others. Sure, I want to &amp;ldquo;vibe code&amp;rdquo; to see what that&amp;rsquo;s all about, but I want to create something that solves a real problem and helps real people.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;User Experience&lt;/strong&gt;: I don&amp;rsquo;t have a background in UI/UX, but I do love people in general. Whatever I build, I want it to be fun to use. I don&amp;rsquo;t want to create frustration in the tool itself. Learning has it&amp;rsquo;s own challenges and frustrations. The container for learning shouldn&amp;rsquo;t add to those.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="the-80-the-infrastructure-nobody-talks-about"&gt;The 80%: The Infrastructure Nobody Talks About&lt;/h2&gt;
&lt;p&gt;As I mentioned, it took me a couple of days to build the core functionality. I wanted to play with &lt;a href="https://cursor.com"&gt;Cursor&lt;/a&gt;, since I hadn&amp;rsquo;t done that before. And I really wanted to keep my costs very low. I don&amp;rsquo;t know if anyone other than me actually wants this platform. I don&amp;rsquo;t want to spend hundreds of dollars if I&amp;rsquo;m the only one using it. I just want to build something and see if anyone else wants to use it.&lt;/p&gt;
&lt;p&gt;But&amp;hellip; since this is a personalized learning plan, I need to allow people to login to the site to see &lt;em&gt;their&lt;/em&gt; specifc learning plans. That means&amp;hellip;&lt;/p&gt;
&lt;h3 id="user-accounts"&gt;User Accounts!&lt;/h3&gt;
&lt;p&gt;And if I have user accounts, that means that I need to also enable user registration, password changes, and password resets. Also, I&amp;rsquo;ll need to restrict some pages to just be authenticaed pages, so people see just their specific content and no one else&amp;rsquo;s. Thanks to Cursor, it only takes a couple more days to hammer these things out. I&amp;rsquo;m only working a couple of hours each on this project, since my family and my full-time job take precedeence over my side-project.&lt;/p&gt;
&lt;p&gt;It sure would be nice to get &lt;em&gt;someone&lt;/em&gt; into the system to take a look at it. So far, this is all just sitting on my local machine. I&amp;rsquo;ll want to publish it somewhere so someone can look at it. That means&amp;hellip;&lt;/p&gt;
&lt;h3 id="hosting"&gt;Hosting!&lt;/h3&gt;
&lt;p&gt;I&amp;rsquo;m familiar with AWS, so I started going down that path and cobbling together a solution. ChatGPT and Gemini were guiding me through this process, but luckily I reached out my buddy Nate with my implementation plan. He quickly informed me that what I was doing was fantastic and scalable to the enterprise level, but massively overkill for what I need. He pointed me to &lt;a href="https://fly.io/"&gt;Fly.io&lt;/a&gt; for cheap and lightweight hosting. I signed up, figured out my database, backend, and frontend hosting solutions, registered a &lt;a href="https://adeptli.org"&gt;adeptli.org&lt;/a&gt;, which forced me to come up with a name, figure out SSL, point my DNS, create Docker containers and fly .toml files, build a CI/CD pipeline, and deploy my application to the world wide web. That was another two days of effort. It&amp;rsquo;s never as easy as adverised&amp;hellip;&lt;/p&gt;
&lt;p&gt;I never actually coded in the user registration piece, so as of now, no one can create an account. I have to manually create an account for any new user, which at this point, I&amp;rsquo;m ok with. Why? The user experience is still terrible. This is still an &lt;em&gt;idea&lt;/em&gt;, not a product. At this stage of the idea life cycle, I need cheerleaders, not critics. I need to be selective of who sees it. There will certainly be time for criticism later (and that&amp;rsquo;s a very important time)!&lt;/p&gt;
&lt;p&gt;I don&amp;rsquo;t mind footing the bill for these initial users, because I value their feedback, and they&amp;rsquo;re my friends. But eventually I&amp;rsquo;ll want to open this up for more people, and allow anyone to create an account. From there, anyone could, in theory, start generating their own learning plans, which I&amp;rsquo;m still very excited about! Learning plans generated by A.I. agents to enable anyone to learn anything!&lt;/p&gt;
&lt;p&gt;However, each one of those learning plans costs me some actual money. I don&amp;rsquo;t want to just expose modern A.I. agents on the web for free, because if word gets around, I&amp;rsquo;ll go bankrupt very quickly! So I need to charge something so it doesn&amp;rsquo;t get abused. That means I need&amp;hellip;&lt;/p&gt;
&lt;h3 id="payment-processing"&gt;Payment Processing!&lt;/h3&gt;
&lt;p&gt;I&amp;rsquo;ll need testing accounts, production accounts, refunds management, PCI considerations, failed transaction handling, and a host of other concerns.&lt;/p&gt;
&lt;p&gt;Fine, I can do that. &lt;a href="https://www.lemonsqueezy.com/"&gt;Lemon Squeezy&lt;/a&gt; seems to make it pretty simple. They seem to cover everything. I can probably charge a few cents to the end user per request, and then bespoke education will be available to the masses! But Lemon Squeezy charges $.50 per transaction, plus a fee. And users probably don&amp;rsquo;t want to have to go through another payment every time they want to load their next chapter. But maybe they do? If I charge a dollar per chapter, that really only brings in 45ish cents to cover the costs of the AI generation, hosting, domain name, and all that. If a person could agree to purchase more than one chapter at a time, that would save me a lot in transaction fees. So that means I can offer bulk discounts to the users, but then I have to have packages of chapters. The easiest way to do that is to implement a&amp;hellip;&lt;/p&gt;
&lt;h3 id="credit-system"&gt;Credit System!&lt;/h3&gt;
&lt;p&gt;Everyone these days is charging a subscription. ChatGPT, Claude, Gemini, Netflix, Disney, Amazon, t-shirts, food, basically anything you want, you can get more than you need via subscription. I don&amp;rsquo;t want people to pay for more than they need, so I&amp;rsquo;m fairly anti-subscription. If I want to give volume discounting, that means I need to sell packages of &amp;ldquo;credits&amp;rdquo;. So I come up with a set of packages to sell through Lemon Squeezy, a way for users to buy those packages, credit management, and charging credits for chapters (but not the first one!). That&amp;rsquo;s all well and good, but I still want my friends to have free access for feedback. And I might want to gift some credits to people in the beginning to get early feedback. And maybe I need to disable an account for abuse or something. That means I need&amp;hellip;&lt;/p&gt;
&lt;h3 id="admin-interface"&gt;Admin Interface!&lt;/h3&gt;
&lt;p&gt;Yes, I&amp;rsquo;ll want to be able to add credits to users, disable accounts, manually create accounts, and other administrative tasks. Eventually I&amp;rsquo;ll want some reporting to see how people are actually using the tools, but for now that should be fine. That one was simple. Just a couple hours of coding with Cursor knocked out an admin interface. Don&amp;rsquo;t forget to secure it! Now I&amp;rsquo;ll be able to serve the users better. Speaking of the users, once they go to &lt;a href="https://adeptli.org"&gt;Adeptli.org&lt;/a&gt;, they&amp;rsquo;ll probably want to see what this is all about. That means I need&amp;hellip;&lt;/p&gt;
&lt;h3 id="a-landing-page"&gt;A Landing Page!&lt;/h3&gt;
&lt;p&gt;Something that shows what the heck this actually is! And while I&amp;rsquo;m at it, I&amp;rsquo;ll probably also need a privacy policy and terms of service. And those will need contact information, but I don&amp;rsquo;t want to just put my personal email address out there for the whole world to see. That means I need&amp;hellip;&lt;/p&gt;
&lt;h3 id="email-accounts"&gt;Email Accounts!&lt;/h3&gt;
&lt;p&gt;Over to Google for $8 / mo for a &amp;ldquo;professional&amp;rdquo; account, and then hooking all that up. Jumping through all the security and authorization hoops there, and getting it set up on my phone. Say hello to &lt;a href="mailto:admin@adeptli.org"&gt;admin@adeptli.org&lt;/a&gt;! (no really, say hello!) That&amp;rsquo;s the email I&amp;rsquo;ll want to use for any communication around this project, since, as I stated earlier, my family is my top priority so I want decent boundaries. I don&amp;rsquo;t want server alerts going to my personal email account. Oh yeah! Server alerts! That means I need&amp;hellip;&lt;/p&gt;
&lt;h3 id="server-monitoring"&gt;Server Monitoring!&lt;/h3&gt;
&lt;p&gt;Luckily, &lt;a href="https://fly.io/"&gt;Fly.io&lt;/a&gt; makes this pretty straightforward to handle, with their partnership with Sentry. I&amp;rsquo;ve never used Sentry, but getting it ingterated wasn&amp;rsquo;t too bad. Just a few hours to get it up and running and reporting. May as well throw in some Google Analytics so I can see how people are using my site, beyond just what&amp;rsquo;s going wrong. It is nice to know that now I&amp;rsquo;ll be aware very quickly if my database goes down. I don&amp;rsquo;t want people to lose the credits they paid for. That means I need&amp;hellip;&lt;/p&gt;
&lt;h3 id="database-backups"&gt;Database Backups!&lt;/h3&gt;
&lt;p&gt;Again &lt;a href="https://fly.io/"&gt;Fly.io&lt;/a&gt; makes this easy. Oh wait, nope, I didn&amp;rsquo;t choose managed Postgres for my DB at the beginning, since there was an extra charge. I&amp;rsquo;m on unmanaged, and I don&amp;rsquo;t want to build my own database backup solution. So I&amp;rsquo;ll have to create a managed Postgres DB that has automatic backups, then migrate all my existing content over there. That only took&amp;hellip; half a day :( But hey, I get to learn a lot! And that&amp;rsquo;s my goal! Enabling anyone to learn anything, the way they want! But if it&amp;rsquo;s truly available to anyone, I have one last piece. That means I need&amp;hellip;&lt;/p&gt;
&lt;h3 id="user-registration"&gt;User registration!&lt;/h3&gt;
&lt;p&gt;Yes, that&amp;rsquo;s the last piece of the puzzle. Pretty straightforward. Let people in, and let people use the application. But I don&amp;rsquo;t want bots creating accounts, since I let people create lesson plans and go through their first chapter at no cost. The could incur significant fees if abused. So I&amp;rsquo;ll have to enable one of those annoying &amp;ldquo;verify your email&amp;rdquo; mechanisms. That shouldn&amp;rsquo;t be too hard, especially because I already created email accounts for the site. But once that is in place, I&amp;rsquo;ve built an MVP! And launched it!&lt;/p&gt;
&lt;h2 id="8020"&gt;80/20&lt;/h2&gt;
&lt;div style="display: flex; gap: 20px; margin: 20px 0; flex-wrap: wrap;"&gt;
&lt;div style="flex: 1; min-width: 300px;"&gt;
&lt;figure&gt;
&lt;img loading="lazy" src="images/initial_commit.png"
alt="Initial commit on April 28"/&gt; &lt;figcaption&gt;
&lt;p&gt;Initial commit - April 28&lt;/p&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/div&gt;
&lt;div style="flex: 1; min-width: 300px;"&gt;
&lt;figure&gt;
&lt;img loading="lazy" src="images/launch_commit.png"
alt="Launch commit on July 9"/&gt; &lt;figcaption&gt;
&lt;p&gt;Launch commit - July 9&lt;/p&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/div&gt;
&lt;/div&gt;
So all in all, I wrote my first proof of concept in about two days. Then, to wrap the website "fundamentals" around it took me another *two months*. As mentioned, I have a lot of competing priorities and this was a fun side project for me. Will it gain any traction? Beats me. But I'm going to continue to refine it and hammer away at it. Now that I have the "admin" side of the project in a functional state, I can start iterating on the core functionality and improving it.
&lt;h2 id="conclusion"&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;I&amp;rsquo;m pretty confident I did this The Hard Way. I&amp;rsquo;m sure there are boilerplate frameworks for sites with user registration, security, authentication, payments, etc. Likely, they do a better job than I did, and are more secure. I quite possibly could have saved myself a month or more of time and effort with a quick google search.&lt;/p&gt;
&lt;p&gt;But I can look at this little project and say &amp;ldquo;I built this.&amp;rdquo; Or rather, &amp;ldquo;I basically guided AI tools through the development process to build this.&amp;rdquo; And I got to experiment with Cursor, Jules, Gemini, ChatGPT, Claude, Lovable, Bolt.new, and a host of other up and coming tools.&lt;/p&gt;
&lt;p&gt;If I had to do it all over, I&amp;rsquo;d start by looking for a modern web boilerplate package to start from. Maybe I would replace it one day, if that became the best value-add action for my project, but it would certainly accelerate me to getting my idea in front of people.&lt;/p&gt;
&lt;p&gt;Maybe no one will use this. If you&amp;rsquo;re interested in checking it out, I&amp;rsquo;d love to have you register over at &lt;a href="https://adeptli.org"&gt;adeptli.org&lt;/a&gt;. I made a &lt;a href="https://discord.gg/DFgjGagDtM"&gt;discord&lt;/a&gt; server so that I can chat with you and hear your feedback. I would really value any feedback that you have. If you read this whole post and singed up on Adeptli, drop me a message on the Discord and I&amp;rsquo;ll credit your account a bit so you can really test out the functionality.&lt;/p&gt;
&lt;script type="application/ld+json"&gt;
{
"@context": "https://schema.org",
"@type": "BlogPosting",
"headline": "Building a SaaS Product: The Hidden 80% That Nobody Talks About",
"description": "My journey building a software service website...",
"author": {
"@type": "Person",
"name": "Your Name"
},
"datePublished": "2025-07-11"
}
&lt;/script&gt;</description></item><item><title>Taming My Todoist Beast with Google ADK and AI Agents</title><link>https://brettgfitzgerald.com/posts/how-i-used-google-adk-and-ai-agents-to-take-control-of-my-todoist-backlog/</link><pubDate>Wed, 02 Jul 2025 00:00:00 +0000</pubDate><guid>https://brettgfitzgerald.com/posts/how-i-used-google-adk-and-ai-agents-to-take-control-of-my-todoist-backlog/</guid><description>&lt;h2 id="my-todoist-was-a-mess"&gt;My Todoist Was a Mess&lt;/h2&gt;
&lt;p&gt;I&amp;rsquo;ve been a loyal Todoist user for years. It&amp;rsquo;s been my trusty sidekick for keeping my work organized. But recently, things got a little out of hand. A small internal restructure at the organization I work for meant my workload grew significantly. More projects, more stakeholders, and a couple more people on my team. And a constant stream of new tasks hitting my inbox. I was struggling to keep my head above water. I was busy, but I wasn&amp;rsquo;t making progress on the things that actually mattered.&lt;/p&gt;
&lt;p&gt;I knew I needed to do something different. One of my teammates had been experimenting with the &lt;a href="https://github.com/google/adk"&gt;Google ADK (Agent Development Kit)&lt;/a&gt; and it got me thinking: could I build my own AI assistant to help me make sense of the chaos?&lt;/p&gt;
&lt;h2 id="why-i-chose-google-adk"&gt;Why I Chose Google ADK&lt;/h2&gt;
&lt;p&gt;I&amp;rsquo;d seen some cool demos of Google ADK, but I wanted to see if I could use it to solve a real-world problem. My teammate was using it to pull data in from BigQuery and make sense of it. My problem was simple: I needed to figure out what to work on next. I needed a way to cut through the noise and find the high-impact tasks that would actually move the needle.&lt;/p&gt;
&lt;h2 id="building-my-ai-assistant"&gt;Building My AI Assistant&lt;/h2&gt;
&lt;p&gt;I&amp;rsquo;m not a professional developer, but I like to tinker. I started by sketching out what I wanted my AI assistant to do:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Talk to Todoist to get my tasks.&lt;/li&gt;
&lt;li&gt;Help me figure out which tasks were most important.&lt;/li&gt;
&lt;li&gt;Give me guidance on what to work on next.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;With Google ADK, I was able to create a few different &amp;ldquo;agents&amp;rdquo; that worked together:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;Smart Prioritization Agent&lt;/strong&gt; that looks at how recent a task is, its impact, and how much effort it will take.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;Project Manager Agent&lt;/strong&gt; that helps me break down big, scary tasks into smaller, more manageable ones.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;Coordinator Agent&lt;/strong&gt; that figures out which agent to send my requests to.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I also set up a simple rule: all the details about a task go in the description, and any updates or decisions get logged as comments. This keeps my Todoist nice and tidy.&lt;/p&gt;
&lt;h2 id="how-it-works---a-refinement-session"&gt;How It Works - A Refinement Session&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;img alt="The agent analyzing a task, asking clarifying questions, and proposing a new description and comment" loading="lazy" src="https://brettgfitzgerald.com/posts/how-i-used-google-adk-and-ai-agents-to-take-control-of-my-todoist-backlog/images/TaskAgents1.jpg"&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Most of my tasks in Todoist were simply one-liners where I captured a thought of a task, to be done or prioritized later. I didn&amp;rsquo;t want to lose track of them. But they didn&amp;rsquo;t have a lot of context or detail. So the first thing I needed to do was to refine my backlog of tasks. My ADK tool does just that, when I tell it that I want to. It pulls in all my open tasks, reads the descriptions and comments, and asks me questions to fill in any blanks.&lt;/p&gt;
&lt;h2 id="how-it-works---prioritization"&gt;How It Works - Prioritization&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;img alt="The agent presenting a prioritized daily plan, grouping tasks by impact and effort" loading="lazy" src="https://brettgfitzgerald.com/posts/how-i-used-google-adk-and-ai-agents-to-take-control-of-my-todoist-backlog/images/TaskAgents2.jpg"&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Once I had all my tasks refined, I can ask my agents to tell me my top priority tasks for the day, and it will present me with a game plan. These are prioritized based on how recent the most recent action was taken on a task, what the anticipated impact of the task is, and how much effort my next action is.&lt;/p&gt;
&lt;p&gt;Finally, it updates my tasks in Todoist with the new priorities and any notes we discussed.&lt;/p&gt;
&lt;h2 id="the-results"&gt;The Results&lt;/h2&gt;
&lt;p&gt;The difference has been night and day. Instead of staring at a giant list of tasks, I get a clear, actionable plan every morning. I know what I need to work on, why it&amp;rsquo;s important, and what the next steps are. I feel like I&amp;rsquo;m back in the driver&amp;rsquo;s seat, making real progress on the things that matter.&lt;/p&gt;
&lt;h2 id="bugs"&gt;Bugs&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;img alt="The agent updating Todoist tasks and confirming the new priorities" loading="lazy" src="https://brettgfitzgerald.com/posts/how-i-used-google-adk-and-ai-agents-to-take-control-of-my-todoist-backlog/images/TaskAgents3.jpg"&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The most common bug that I have is that sometimes the Agent says it will wait for my input, but then it goes ahead and executes whatever changes it wants to. As a safeguard, I don&amp;rsquo;t have an tool set up for completing a Task. I don&amp;rsquo;t want to lose any tasks if I&amp;rsquo;m not paying super close attention to it. Plus, I don&amp;rsquo;t want to give away that task-completing-dopamine-hit to anyone other than me. Gotta feed my addiction ;)&lt;/p&gt;
&lt;p&gt;Also, one time it created four copies a task. So watch out for that.&lt;/p&gt;
&lt;h2 id="what-i-learned"&gt;What I Learned&lt;/h2&gt;
&lt;p&gt;This was my first time really diving into Google ADK, and it was a lot of fun. I learned a ton about how to design and build AI agents, and I got to play with some cool new technology. But more importantly, I built something that actually makes my life easier.&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;re feeling buried in your to-do list, take a look at &lt;a href="https://github.com/controversy187/todoist-adk"&gt;my project&lt;/a&gt;. It&amp;rsquo;s a somple tool, and sometimes the best way to learn something new is to build something that solves your own problems.&lt;/p&gt;</description></item><item><title>Cursor AI: Rediscovering the Joy of Code (A PM's Journey)</title><link>https://brettgfitzgerald.com/posts/delving-into-cursor/</link><pubDate>Tue, 13 May 2025 00:00:00 -0500</pubDate><guid>https://brettgfitzgerald.com/posts/delving-into-cursor/</guid><description>&lt;h2 id="delving-into-cursor"&gt;Delving into Cursor&lt;/h2&gt;
&lt;p&gt;Ok, I&amp;rsquo;m late to the game. I just started using &lt;a href="https://cursor.sh"&gt;Cursor&lt;/a&gt; to write code. To be fair, writing code isn&amp;rsquo;t really part of my day-to-day job as a Product Manager for an advanced data analytics team, but I wanted to scratch that &amp;ldquo;builder&amp;rdquo; itch in me. Also, I wanted better data than what is available through Jira&amp;rsquo;s API. That means I need to write some code, probably in Python since that&amp;rsquo;s my jam. And why not use these AI tools that everyone else is using? But which one(s) should I use?&lt;/p&gt;
&lt;h2 id="cursor-vs-windsurf-vs-gemini-code-assist-vs-others"&gt;Cursor vs. Windsurf vs. Gemini Code Assist vs. Others&lt;/h2&gt;
&lt;p&gt;Cursor. Why? No reason. Gotta start somewhere, and that&amp;rsquo;s what the kids are talking about.&lt;/p&gt;
&lt;h2 id="getting-set-up"&gt;Getting set up&lt;/h2&gt;
&lt;p&gt;Super simple to get Cursor running. Create an account, download the software, and go. Up and running. It looks very familiar (I guess it&amp;rsquo;s a fork of VSCode, which itself resembles Sublime Text, etc.). There&amp;rsquo;s a file browser in the left sidebar, and primary tabbed coding window. In Cursor, though, there&amp;rsquo;s a chatbox on the right that connects to their AI agent. After signup, you&amp;rsquo;re given two weeks of their Pro version for free. I honestly don&amp;rsquo;t know what their free version includes. That was really hard to determine on their website. Full disclosure, I still don&amp;rsquo;t know. Regardless, that&amp;rsquo;s about all you need to get setup.&lt;/p&gt;
&lt;h2 id="first-steps"&gt;First steps&lt;/h2&gt;
&lt;p&gt;Not knowing where to start, I typed in the description of the application I wanted to build. I&amp;rsquo;ve worked with LLMs enough to know to ask if it has any questions for me before it starts doing anything. Sure enough, it asked about technologies to use, and some basic structure questions. Once I answered those, it started writing my code for me! It proposed several files and I blindly accepted the proposals. In short order, I had a basic application running!&lt;/p&gt;
&lt;h2 id="iterating"&gt;Iterating&lt;/h2&gt;
&lt;p&gt;The app itself wasn&amp;rsquo;t worth using yet. Several things were non-functional or looked terrible, but I started to correct the issues one at a time. Through this entire process, I didn&amp;rsquo;t write any code. I simply described the change I wanted to make, and Cursor made a suggestion. I accepted it, and tested the results, going back and forth with the agent. I hear a lot about Cursor&amp;rsquo;s superpower: Tab completion. Describe something, hit Tab, and Cursor fills in the rest. I still haven&amp;rsquo;t used that. The Agentic build is doing everything I ask of it.&lt;/p&gt;
&lt;h2 id="limitations"&gt;Limitations&lt;/h2&gt;
&lt;p&gt;Eventually, my conversation started losing context. A little note at the bottom of the chat window informed me that starting a new chat will yield better results. But would Cursor pick up where it left off? It turned out, no. A new chat was a new chat. It had the context of the codebase, but only the details of the files that I specifically mentioned. This floundering and context-loss made me realize I needed a better way to guide Cursor, which led me to&amp;hellip;&lt;/p&gt;
&lt;h2 id="a-project-plan"&gt;A Project Plan&lt;/h2&gt;
&lt;p&gt;As a recovering Agilist, I don&amp;rsquo;t like having a plan. I like to just build things. But a plan gives a person a larger context for what their work does, where it leads, and what it fits into. And that&amp;rsquo;s just what a coding agent needs. So I stopped building for a bit, then told Cursor what my project goals are, and asked it to create a markdown file describing the project, building a checklist of incremental steps we would need to accomplish the project. It happily complied! From that point on, as we accomplished tasks, we checked them off, started a new chat, and picked up right where we left off. Tagging the project plan and the relevant files in a new chat very quickly reacclimated Cursor to what we were doing.&lt;/p&gt;
&lt;p&gt;More recently, as features that I&amp;rsquo;m building into this application are more sizeable, I&amp;rsquo;m creating Feature plans in addition to the Project plan. So I can give Cursor the overall context of the project, then give it the more granular context of the feature we&amp;rsquo;re building. Keeping these agentic chats small seems to keep them more intellegent.&lt;/p&gt;
&lt;h2 id="bug-loops"&gt;Bug loops&lt;/h2&gt;
&lt;p&gt;I did stumble on some long loops a bug being introduced, then three or four steps of remediation, then it reintroduced the same bug again. As a concise example, I&amp;rsquo;m new to Big Query and was asking for Cursor to store and update some Big Query records.&lt;/p&gt;
&lt;p&gt;Me: Grab the data from the API and update the table in BQ with it.
Cursor: Sure, here&amp;rsquo;s the code.
Me: I ran it and it&amp;rsquo;s adding every record as a new record. I want to update the existing records, and add any new ones. Think &amp;ldquo;Upsert&amp;rdquo;.
Cursor: Got it. Here&amp;rsquo;s the new code that adds new records and updates existing ones.
Me: I ran that, and now Big Query is complaining about not updating a streaming buffer.
Cursor: That makes sense. The streaming buffer hasn&amp;rsquo;t finished writing to the table from our last operation, so you can&amp;rsquo;t update those records. I&amp;rsquo;ll refactor the code to accommodate this by creating new rows for each record we get back from the API
Me: No, that&amp;rsquo;s where we started!&lt;/p&gt;
&lt;p&gt;In order to break out of some of these loops, I had to do some learning (from a co-worker) and learned about Merging records. I mentioned that to Cursor, and it quickly leveraged that method to accomodate the streaming buffers.&lt;/p&gt;
&lt;h2 id="final-thoughts"&gt;Final Thoughts&lt;/h2&gt;
&lt;p&gt;I described this to my wife. I like building things and solving problems. I used to write code for a living, but I have a horrible memory. I knew &lt;em&gt;how&lt;/em&gt; I wanted to solve a problem, but I spent so much time looking up coding references, examples, or debugging things that seemed pretty far into the weeds. Solving the problems was fun, but writing the code was more of a tedius hoop I had to jump through. Heaven forbid I had to go back through someone else&amp;rsquo;s code to try and discover what they were attempting to do!&lt;/p&gt;
&lt;p&gt;Cursor has reignited my joy in coding. I am focused on building and solving problems, not remembering syntax and keeping a complex process flow in my active memory. I can delegate the detailed parts to an AI agent who is, frankly, better at keeping them straight. I&amp;rsquo;m learning how to interact with the agent in a way that we both meet with success. Small steps, incremental value delivery. Tight feedback loops. It&amp;rsquo;s all that agile stuff. But the fundamental agile stuff, not the meetings, roles, process, and Agile-Industry.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s fun.&lt;/p&gt;
&lt;p&gt;After my two week free Pro trial, the Agent chat just threw errors at me. It said I should try again later, but I never got it to work. I did end up paying the $20 / month for the paid pro version so that I could continue building my application. Frankly, I&amp;rsquo;m getting excited again about building more things. Ideas keep coming like they used to. When I was new to programming and I naively felt like I could build anything with a handful of for loops and if statements. The feeling of ability seemed to unlock so many ideas. Now I&amp;rsquo;m feeling that excitement again. Is Cursor (and agentic programming in general) perfect? Nope! But it&amp;rsquo;s really good, and it&amp;rsquo;s going to get better!&lt;/p&gt;</description></item><item><title>Obsidian, MCP Servers, and Supercharging Your Second Brain with AI</title><link>https://brettgfitzgerald.com/posts/mcp-server-experiences/</link><pubDate>Wed, 02 Apr 2025 00:00:00 -0500</pubDate><guid>https://brettgfitzgerald.com/posts/mcp-server-experiences/</guid><description>&lt;h2 id="my-journey-with-mcp-servers"&gt;My Journey with MCP Servers&lt;/h2&gt;
&lt;p&gt;I recently learned of MCP (Model Control Protocol) servers through a &lt;a href="https://news.ycombinator.com/item?id=43410866"&gt;post on Hacker News&lt;/a&gt;. The premise seems really neat. MCP is essentially a protocol that allows AI models like Claude to interact with external tools and services through a standardized interface. I can write a very simple server and create &amp;ldquo;tools&amp;rdquo; for the Claude.ai desktop application to connect to and use. The example in the post created some tools that gave Claude access to read and write to the local filesystem (unrestricted, by the way). The original intent was to have Claude write some application. I cloned and ran the &lt;a href="https://github.com/ZbigniewTomanek/my-mcp-server"&gt;sample MCP server&lt;/a&gt; from the article to play with it, and was immediately impressed. This unlocks so many opportunities to integrate an LLM with countless services!&lt;/p&gt;
&lt;p&gt;Hold on to that thought&amp;hellip;&lt;/p&gt;
&lt;h2 id="my-second-brain"&gt;My Second Brain&lt;/h2&gt;
&lt;p&gt;For the past several years, I&amp;rsquo;ve been using Tiago Forte&amp;rsquo;s &lt;a href="https://www.buildingasecondbrain.com/"&gt;Build a Second Brain&lt;/a&gt; method using Obsidian.md for my second brain. I implemented the &lt;a href="https://fortelabs.com/blog/para/"&gt;PARA Method&lt;/a&gt; for organization, and create a Daily Note every day to link my chronological life to my disorganized brain. It&amp;rsquo;s been a good system, even if I don&amp;rsquo;t use it to it&amp;rsquo;s fullest. At the very least, I have all my notes synced with me between my desktop, laptop, and phone, thanks to Syncthing. That being said, Obsidian&amp;rsquo;s search capabilities are relatively lackluster.&lt;/p&gt;
&lt;p&gt;This is where my MCP server can help.&lt;/p&gt;
&lt;h2 id="getting-things-going"&gt;Getting things going&lt;/h2&gt;
&lt;p&gt;To get it all setup, I basically cloned the MCP server linked above, and then set it up as they described, creating the server config in the Claude Desktop app. Since I&amp;rsquo;m running Linux, I had to &lt;a href="https://github.com/aaddrick/claude-desktop-debian"&gt;hack around a bit&lt;/a&gt; to get the Windows Claude client running, but it works fine. With the MCP server connected, I now showed that I had several tools available to Claude.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Available Tools" loading="lazy" src="https://brettgfitzgerald.com/posts/mcp-server-experiences/available-tools.png"&gt;&lt;/p&gt;
&lt;p&gt;After some sample queries, I validated that Claude now had full access to my filesystem!&lt;/p&gt;
&lt;h2 id="plumbing-the-depths-of-my-second-brain"&gt;Plumbing the depths of my second brain&amp;hellip;&lt;/h2&gt;
&lt;p&gt;Since I write a Daily Note every day, I first asked Claude to summarize my daily notes for the month of March 2025. It came up with accurate and surprisingly detailed insights.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;March 2025 Daily Notes Summary
Personal Life
Family Activities:
Started the month at ****** with the ****** and ****** families, though you noted it was chaotic with misaligned schedules
Went skiing with ****** at Crystal Mountain on March 10th, where you had a meaningful father-son conversation about life and relationships
Your daughter ****** attended Middle School Ministry several times
References to your family (******, ******, ******, ******) appear consistently throughout the month
Fitness:
Maintained consistent workout schedule with 15 recorded workout days
Primarily practiced Jiu-Jitsu at ****** (both gi and nogi classes)
Also incorporated running into your fitness routine
Tracked your weight throughout the month, ranging from 179 lbs to 187 lbs, with an average around 182 lbs
Spiritual Practice:
Maintained daily quiet time starting mid-month (March 18-31)
Studied the Gospel of Matthew, progressing sequentially from Matthew 12:38 through Matthew 14:33
Each quiet time session appears well-documented with dedicated notes
Professional Life
Regular Meetings:
****** (almost daily)
Weekly kickoff meetings (Mondays)
One-on-one meetings with various team members including ******, ******, and ******
Key Projects:
****** Rush Orders project with weekly check-ins
****** project with multiple coordination meetings
Corporate Injury Analysis work
Supply Chain ****** activities and meetings
Non******stock ETA modeling work (created ******)
Professional Development:
Participated in Looker Studio training for data visualization
Worked on Google Maps Optimization POC
Set up Gemini in VSCode
Explored Jira metrics via API
People Management:
Reached out to ****** about a ******
Provided feedback on ******&amp;#39;s Flow Metrics presentation
Worked with various team members including ******, ******, ******, and ******
Your month shows a balanced life with consistent attention to physical fitness, spiritual growth, family time, and professional responsibilities, with a particular focus on data and supply chain projects at work.
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This was great! For years, my Second Brain has been a storage device. I had the ability to go back and manually retrieve that information, but I had to read the notes into my short-term (actual) brain, and hold it there while I search for other information to connect it to. Obsidian and the Second Brain method make this simpler by encouraging the use of internal links in notes, but now I had another tool for synthesizing meaningful information from all these connections!&lt;/p&gt;
&lt;h3 id="creating-notes"&gt;Creating Notes&lt;/h3&gt;
&lt;p&gt;Equipped with a tool for analyzing my second brain, I turned toward project management. I&amp;rsquo;ve had a few ideas for side projects that I wanted to flesh out a bit. I&amp;rsquo;ve worked with LLMs in the past to bounce ideas off and refine some thoughts, but now I could generate documentation around these ideas, store that information, and retrieve it later for ongoing processing and development! I took a conversation about a project I&amp;rsquo;m thinking through, described my desired goal, and then asked Claude to develop a plan for a low-code, low-cost MVP to test market fit. It described the approach, I refined it a bit, and then asked Claude to store the project documentation in my Obsidian Vault. Boom, project plan and action steps created!&lt;/p&gt;
&lt;p&gt;&lt;img alt="Project Plan" loading="lazy" src="https://brettgfitzgerald.com/posts/mcp-server-experiences/project-plan.jpg"&gt;&lt;/p&gt;
&lt;h3 id="reflection"&gt;Reflection&lt;/h3&gt;
&lt;p&gt;MCP Servers seem extremely powerful. For such a long time, LLMs have seemed contained to limited use cases. Plugins for code editors have allowed functionality in coding and chatbots are common. Further integrations have required the use of APIs and coding to leverage the power of LLMs in other contexts. Now, with MCP Servers, it really feels like we have simple-to-create interfaces that allow LLMs to interact with the rest of the digital world. What will you create?&lt;/p&gt;</description></item><item><title>Video Compression Analysis</title><link>https://brettgfitzgerald.com/posts/video-compression-analysis/</link><pubDate>Wed, 12 Feb 2025 05:00:00 -0500</pubDate><guid>https://brettgfitzgerald.com/posts/video-compression-analysis/</guid><description>&lt;h2 id="a-videography-hobbyist"&gt;A videography hobbyist&lt;/h2&gt;
&lt;p&gt;In my free time, I like shooting videos of my family&amp;rsquo;s adventures and doing some basic editing. I shoot on my cellphone and a GoPro. For the past several years, I have rendered my final projects in 1080p at 24 frames per second. I liked the ability to shoot in 4k and still &amp;ldquo;zoom in&amp;rdquo; digitally to 1080. That also let me shoot slow motion video at 1080, and match my final render resolution. I recently got a newer GoPro, so now I can shoot 4k at 120 fps, which allows me slow down to 20% speed if I render my final project at 24 fps.&lt;/p&gt;
&lt;p&gt;With this advent of being able to shoot slow motion in 4k resolution, I decided to start rendering my projects in 4k, by default. I&amp;rsquo;m also experimenting with doing very quick edits, just splicing the day&amp;rsquo;s footage together, applying automatic color balancing per-clip, and then rendering at 60fps. This is more of a &amp;ldquo;home movie&amp;rdquo; of a day or event, rather than a curated, edited highlight video. I do all this in Davinci Resolve. Previously, I would be able to render my 1080 videos at 24fps and be happy with the file size and quality of picture. Now that I am rendering at four times the resolution and two and a half times the framerate, my output filesize has ballooned, and I need to pay better attention to my compression.&lt;/p&gt;
&lt;p&gt;I want to find a good balance between output filesize and quality for my home movies.&lt;/p&gt;
&lt;h2 id="comparison-of-projects"&gt;Comparison of projects&lt;/h2&gt;
&lt;p&gt;In the past, I would shoot for around 100 megabytes per minute of rendered video. So a 5 minute video, rendered at a resolution of 1080, at 24 frames per second would come in around 500 MB. For videos I really spent time on, I would bump up my quality settings and I&amp;rsquo;d be happy with a 3 gig file for a 5 minute video.&lt;/p&gt;
&lt;p&gt;I recently rendered a video using Davinci Resolve&amp;rsquo;s 4k &amp;ldquo;Master&amp;rdquo; preset. So a resolution of 4k, at 60 frames per second, and a duration of 22 minutes came in at a whopping 75 gigabytes (~3.4 GB / min). I used Resolve&amp;rsquo;s &amp;ldquo;YouTube&amp;rdquo; preset, and that reduced the filesize to 1.8 GB.(~80 MB / min). That is a significant difference!&lt;/p&gt;
&lt;p&gt;For reference, my input files, shot on the GoPro Hero 13 were shot in 4k, mostly at 120 fps. They totaled 18.2GB, so my rendered file was actually four times larger than my source material!&lt;/p&gt;
&lt;p&gt;The two questions I have are:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;What is the difference in output files between these two?&lt;/li&gt;
&lt;li&gt;Is there a noticeable difference in visible quality?&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="differences-in-objective-data"&gt;Differences in objective data&lt;/h2&gt;
&lt;p&gt;I wrote a &lt;a href="https://gist.github.com/controversy187/0d33948ba3afeb5ba4c4d2fb9ae8113f"&gt;python script&lt;/a&gt; that compares various aspects of the videos. I also ran them through the various presets in &lt;a href="https://handbrake.fr/"&gt;Handbrake&lt;/a&gt; to see how they compare. The video is 21:49 long. These are the results of that comparison, in order of increasing filesize.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Filename&lt;/th&gt;
&lt;th&gt;Bitrate (kbps)&lt;/th&gt;
&lt;th&gt;Resolution&lt;/th&gt;
&lt;th&gt;Framerate (FPS)&lt;/th&gt;
&lt;th&gt;Video Codec&lt;/th&gt;
&lt;th&gt;Filesize&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Source Video (Sample).MP4&lt;/td&gt;
&lt;td&gt;120000&lt;/td&gt;
&lt;td&gt;3840x2160&lt;/td&gt;
&lt;td&gt;119.88&lt;/td&gt;
&lt;td&gt;hevc&lt;/td&gt;
&lt;td&gt;67 &lt;strong&gt;MB&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Resolve - YouTube - h264.mp4&lt;/td&gt;
&lt;td&gt;11618&lt;/td&gt;
&lt;td&gt;3840x2160&lt;/td&gt;
&lt;td&gt;60&lt;/td&gt;
&lt;td&gt;h264&lt;/td&gt;
&lt;td&gt;1.8 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Resolve - YouTube - h265.mp4&lt;/td&gt;
&lt;td&gt;10566&lt;/td&gt;
&lt;td&gt;3840x2160&lt;/td&gt;
&lt;td&gt;60&lt;/td&gt;
&lt;td&gt;hevc&lt;/td&gt;
&lt;td&gt;1.7 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Resolve - Master - h264 - HandBrake - Fast.mp4&lt;/td&gt;
&lt;td&gt;37948&lt;/td&gt;
&lt;td&gt;3840x2160&lt;/td&gt;
&lt;td&gt;60&lt;/td&gt;
&lt;td&gt;hevc&lt;/td&gt;
&lt;td&gt;6 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Resolve - Master - h264 - HandBrake - VeryFast.mp4&lt;/td&gt;
&lt;td&gt;41025&lt;/td&gt;
&lt;td&gt;3840x2160&lt;/td&gt;
&lt;td&gt;60&lt;/td&gt;
&lt;td&gt;hevc&lt;/td&gt;
&lt;td&gt;6.5 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Resolve - Master - h264 - HandBrake - HQ.mp4&lt;/td&gt;
&lt;td&gt;57001&lt;/td&gt;
&lt;td&gt;3840x2160&lt;/td&gt;
&lt;td&gt;60&lt;/td&gt;
&lt;td&gt;hevc&lt;/td&gt;
&lt;td&gt;9 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Resolve - Master - h264 - HandBrake - Super HQ.mp4&lt;/td&gt;
&lt;td&gt;78210&lt;/td&gt;
&lt;td&gt;3840x2160&lt;/td&gt;
&lt;td&gt;60&lt;/td&gt;
&lt;td&gt;hevc&lt;/td&gt;
&lt;td&gt;12.5 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Resolve - Master - h265.mp4&lt;/td&gt;
&lt;td&gt;473927&lt;/td&gt;
&lt;td&gt;3840x2160&lt;/td&gt;
&lt;td&gt;60&lt;/td&gt;
&lt;td&gt;h264&lt;/td&gt;
&lt;td&gt;75.7 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Resolve - Master - h264.mp4&lt;/td&gt;
&lt;td&gt;474208&lt;/td&gt;
&lt;td&gt;3840x2160&lt;/td&gt;
&lt;td&gt;60&lt;/td&gt;
&lt;td&gt;h264&lt;/td&gt;
&lt;td&gt;75.8 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Obviously, Handbrake is going a good job at compressing the video, and lowering the bitrate, thus reducing filesize. But how do the videos actually &lt;em&gt;look&lt;/em&gt;?&lt;/p&gt;
&lt;h2 id="subjective-comparison"&gt;Subjective comparison&lt;/h2&gt;
&lt;p&gt;Here are images captured from each of the above videos.&lt;/p&gt;
&lt;div id="gallery"&gt;
&lt;a href="images/Resolve - YouTube - h264.jpg" data-sub-html="Davinci Resolve, YouTube preset, h264"&gt;
&lt;img src="images/Resolve - YouTube - h264.jpg" width="100%"&gt;
Davinci Resolve, YouTube preset, h264
&lt;/a&gt;
&lt;a href="images/Resolve - YouTube - h265.jpg" data-sub-html="Davinci Resolve, YouTube preset, h265"&gt;
&lt;img src="images/Resolve - YouTube - h265.jpg" width="100%"&gt;
Davinci Resolve, YouTube Preset, h265
&lt;/a&gt;
&lt;a href="images/Resolve - Master - h264 - HandBrake - Fast.jpg" data-sub-html="Handbrake, Fast"&gt;
&lt;img src="images/Resolve - Master - h264 - HandBrake - Fast.jpg" width="100%"&gt;
Handbrake, Fast
&lt;/a&gt;
&lt;a href="images/Resolve - Master - h264 - HandBrake - VeryFast.jpg" data-sub-html="Handbrake, Very Fast"&gt;
&lt;img src="images/Resolve - Master - h264 - HandBrake - VeryFast.jpg" width="100%"&gt;
Handbrake, Very Fast
&lt;/a&gt;
&lt;a href="images/Resolve - Master - h264 - HandBrake - HQ.jpg" data-sub-html="Handbrake, High Quality"&gt;
&lt;img src="images/Resolve - Master - h264 - HandBrake - HQ.jpg" width="100%"&gt;
Handbrake, High Quality
&lt;/a&gt;
&lt;a href="images/Resolve - Master - h264 - HandBrake - Super HQ.jpg" data-sub-html="Handbrake, Super High Quality"&gt;
&lt;img src="images/Resolve - Master - h264 - HandBrake - Super HQ.jpg" width="100%"&gt;
Handbrake, Super High Quality
&lt;/a&gt;
&lt;a href="images/Resolve - Master - h264.jpg" data-sub-html="Davinci Resolve, Master - h264"&gt;
&lt;img src="images/Resolve - Master - h264.jpg" width="100%"&gt;
Davinci Resolve, Master - h264
&lt;/a&gt;
&lt;a href="images/Resolve - Master - h265.jpg" data-sub-html="Davinci Resolve, Master - h265"&gt;
&lt;img src="images/Resolve - Master - h265.jpg" width="100%"&gt;
Davinci Resolve, Master - h265
&lt;/a&gt;
&lt;/div&gt;
&lt;p&gt;In these very specific examples, you can immediately see that the DaVinci Resolve YouTube presets are not good. The h264 version shows artifiacts on the right side of the frame and all detail in the snow on the ground is completely lost. Interestingly, the h265 codec doesn&amp;rsquo;t lose as much detail, and is slightly smaller. In the case were you need a small file, it seems like the h265 does a better job at these lower bitrates.&lt;/p&gt;
&lt;p&gt;When we jump up into the Handbrake re-encodes, things get noticeably better. Honestly, from the still frames it&amp;rsquo;s very hard (for me) to tell the difference between these images, all the way through to the masters. Even when I watch the playback of the videos themselves, I&amp;rsquo;m hard pressed to see any difference. It could be that the source clips themselves only have a 120Mbps bitrate, and we&amp;rsquo;re re-encoding at a higher bitrate for the masters (474Mbps).&lt;/p&gt;
&lt;h2 id="conclusions"&gt;Conclusions&lt;/h2&gt;
&lt;p&gt;To really judge this fairly, I probably should have rendered everything out from DaVinci Resolve and manually adjusted the bitrate. Based on these tests, though, I&amp;rsquo;m not seeing a noticeable loss in quality between the 474Mbps best quality from Resolve and a re-encoded to 78Mbps in Handbrake. For the time being, I&amp;rsquo;m planning to render out from Resolve and limiting my bitrate to 80Mbps. That&amp;rsquo;s only 590 MB or so per minute of video, which isn&amp;rsquo;t bad for what I&amp;rsquo;m doing with them.&lt;/p&gt;</description></item><item><title>Build a Large Language Model From Scratch</title><link>https://brettgfitzgerald.com/posts/build-a-large-language-model/</link><pubDate>Thu, 06 Feb 2025 12:57:27 -0500</pubDate><guid>https://brettgfitzgerald.com/posts/build-a-large-language-model/</guid><description>&lt;h2 id="building-a-large-language-model-from-scratch"&gt;Building a large language model from scratch&lt;/h2&gt;
&lt;p&gt;I&amp;rsquo;m a machine learning / A.I. hobbyist. The technologies fascinate me, and I can&amp;rsquo;t seem to learn enough about them. Sebastian Raschka&amp;rsquo;s book, Build a Large Language Model (From Scratch) caught my eye. I don&amp;rsquo;t recall how I stumbled on it, but I found it when it was still in early access from Manning Publications. I purchased it, and started working through it as the final chapters were being written and released. I just completed the book and all the included work and loved every minute of it.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Build a Large Language Model From Scratch by Sebastian Raschka" loading="lazy" src="https://brettgfitzgerald.com/posts/build-a-large-language-model/llm-book.jpg"&gt;&lt;/p&gt;
&lt;h2 id="my-approach"&gt;My approach&lt;/h2&gt;
&lt;p&gt;A while ago, I read some advice about learning programming from digital books and tutorials. The advice was to never copy and paste code from samples but to hand-type all the code. I took that approach with this book. I typed every single line of code (except for a couple of blocks which were highly repetitive and long). You can see all my work here: &lt;a href="https://github.com/controversy187/build-a-large-language-model"&gt;https://github.com/controversy187/build-a-large-language-model&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I did my best to work in section chunks. I didn&amp;rsquo;t want to start a section unless I had the time dedicated to completing it. Some sections are pretty short, others are fairly involved and time-consuming.&lt;/p&gt;
&lt;p&gt;I built this in Jupyter Notebooks on my laptop, which is pretty underpowered for this type of work. The premise of the book was that you can build an LLM on consumer hardware, and it can perform decently well. As I&amp;rsquo;m writing this, I&amp;rsquo;m currently fine-tuning my model locally. My model is about 50 steps into a 230-step tuning, and I just crossed the 20-minute execution time mark. The earlier code samples ran quicker, but the last few sections used larger models, which slowed things down considerably.&lt;/p&gt;
&lt;p&gt;I didn&amp;rsquo;t do most of the supplemental exercises. I tend to have an &amp;ldquo;I want to do ALL THE THINGS!&amp;rdquo; personality. The drawback is that if I take the time to do all the things, I eventually get long-term distracted and never actually finish what I started. So I sort of rushed through this book. I even took several weeks off around Christmas and New Year&amp;rsquo;s. But I got back into it and powered through the last few chapters.&lt;/p&gt;
&lt;p&gt;So, more or less, I read through the chapters and wrote all the mandatory coding assignments.&lt;/p&gt;
&lt;h2 id="learnings"&gt;Learnings&lt;/h2&gt;
&lt;p&gt;What can I tell you about large language models? A lot more than I could before I started this book, but certainly not all the things the author attempted to teach me. I&amp;rsquo;ll summarize my understanding, but I could be wrong about some of these things, and I most certainly forgot or misunderstood others.&lt;/p&gt;
&lt;h3 id="tokenization--vocabulary"&gt;Tokenization &amp;amp; Vocabulary&lt;/h3&gt;
&lt;p&gt;A large language model starts its life by building a vocabulary of text. A massive amount of text is distilled down into a list of unique words. Each word is then translated into an integer because computers like numbers more than they like words. This process is referred to as &amp;ldquo;tokenization&amp;rdquo;, where the word is replaced with a numerical token. So now we have a list of unique tokens, which is the vocabulary of the large language model.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# Build a more advanced tokenizer&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;text &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;Hello, world. Is this-- a test?&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;result &lt;span style="color:#f92672"&gt;=&lt;/span&gt; re&lt;span style="color:#f92672"&gt;.&lt;/span&gt;split(&lt;span style="color:#e6db74"&gt;r&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#39;([,.:;?_!&amp;#34;()&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;\&amp;#39;&lt;/span&gt;&lt;span style="color:#e6db74"&gt;]|--|\s)&amp;#39;&lt;/span&gt;, text)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;result &lt;span style="color:#f92672"&gt;=&lt;/span&gt; [item&lt;span style="color:#f92672"&gt;.&lt;/span&gt;strip() &lt;span style="color:#66d9ef"&gt;for&lt;/span&gt; item &lt;span style="color:#f92672"&gt;in&lt;/span&gt; result &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; item&lt;span style="color:#f92672"&gt;.&lt;/span&gt;strip()]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;print(result)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# Outputs &amp;#34;[&amp;#39;Hello&amp;#39;, &amp;#39;,&amp;#39;, &amp;#39;world&amp;#39;, &amp;#39;.&amp;#39;, &amp;#39;Is&amp;#39;, &amp;#39;this&amp;#39;, &amp;#39;--&amp;#39;, &amp;#39;a&amp;#39;, &amp;#39;test&amp;#39;, &amp;#39;?&amp;#39;]&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;all_words &lt;span style="color:#f92672"&gt;=&lt;/span&gt; sorted(set(result))
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;vocab_size &lt;span style="color:#f92672"&gt;=&lt;/span&gt; len(all_words)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;print(vocab_size)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# Outputs 10&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# Display the first 51 tokens in our vocabulary.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;vocab &lt;span style="color:#f92672"&gt;=&lt;/span&gt; {token:integer &lt;span style="color:#66d9ef"&gt;for&lt;/span&gt; integer,token &lt;span style="color:#f92672"&gt;in&lt;/span&gt; enumerate(all_words)}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;for&lt;/span&gt; i, item &lt;span style="color:#f92672"&gt;in&lt;/span&gt; enumerate(vocab&lt;span style="color:#f92672"&gt;.&lt;/span&gt;items()):
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; print(item)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# Outputs:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;,&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;--&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;.&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;2&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;?&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;3&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;Hello&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;4&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;Is&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;5&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;a&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;6&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;test&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;7&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;this&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;8&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;world&amp;#39;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;9&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# In this example, the id 9 represents the word &amp;#34;world&amp;#34;. 5 represents &amp;#34;Is&amp;#34;. etc.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This is where my understanding gets fuzzy. We didn&amp;rsquo;t get very far before that happened, &amp;rsquo;eh? Now, we take that massive amount of text we were using earlier to create the vocabulary (or a subset, or totally different text), and we tokenize the entire text. We do this by using the vocabulary we built previously and substituting the words in the training text for their equivalent token value. This is now our training text.&lt;/p&gt;
&lt;h3 id="model-training--relationships"&gt;Model Training &amp;amp; Relationships&lt;/h3&gt;
&lt;p&gt;With that complete, we can &amp;ldquo;train&amp;rdquo; the model. This process involves taking each token in the vocabulary and building a relationship to each other token in the vocabulary, based on those tokens&amp;rsquo; relative positions to each other in the training text. So if the word &amp;ldquo;cat&amp;rdquo; is followed by the word &amp;ldquo;jump&amp;rdquo;, the model records that relationship. But it also records the relationship of the word &amp;ldquo;cat&amp;rdquo; to other words in the text. So &amp;ldquo;jump&amp;rdquo; follows &amp;ldquo;cat&amp;rdquo;, but maybe it does so more frequently when they are close to the word &amp;ldquo;mouse&amp;rdquo;. And maybe less frequently when they are close to the word &amp;ldquo;nap&amp;rdquo;. Recording ALL these relationships would require a massive dataset, so the relationships are mathematically reduced and approximated. There are definitely more technical terms to use, and the book went into them. I definitely forget them, though.&lt;/p&gt;
&lt;h3 id="text-generation-process"&gt;Text Generation Process&lt;/h3&gt;
&lt;p&gt;Now, if you provide a starter text to the model, it will try to complete the text for you. Continuing our example, if I gave the model the text &amp;ldquo;My cat saw a mouse and it&amp;rdquo;, based on the word cat being close to the word mouse, it might predict the word &amp;ldquo;jumped&amp;rdquo; to come next. So it appends the word &amp;ldquo;jumped&amp;rdquo; to the text I submitted, and then it takes that whole new sentence and feeds it back into itself. So now the input text is &amp;ldquo;My cat saw a mouse and it jumped&amp;rdquo;. The next output word could be &amp;ldquo;on&amp;rdquo;, so it appends that word and feeds this concatenated output back into its input.&lt;/p&gt;
&lt;p&gt;&lt;del&gt;Every time it does a loop like this, it tokenizes the entire input (or up to a limit, known as a context limit or context window) and then calculates the most likely next token, then converts it all back to text for us to read.&lt;/del&gt; &lt;em&gt;&lt;a href="#update-2025-02-17"&gt;See update&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;h3 id="model-weights--distribution"&gt;Model Weights &amp;amp; Distribution&lt;/h3&gt;
&lt;p&gt;&lt;del&gt;Saving all those relationships between the tokens are known as the &amp;ldquo;weights&amp;rdquo; of the model.&lt;/del&gt; &lt;em&gt;&lt;a href="#update-2025-02-17"&gt;See update&lt;/a&gt;&lt;/em&gt; Those can be distributed, so if you train a model on a given training text, you can give that to your friends and they can use those model weights to predict text similar to that training text.&lt;/p&gt;
&lt;h3 id="fine-tuning"&gt;Fine Tuning&lt;/h3&gt;
&lt;p&gt;Fine-tuning is the process of training a model for specific&amp;hellip; things. My mind is getting fuzzier here, so I&amp;rsquo;m not going to go into this deeper. Suffice it to say, that you start with a base language model and continue to train it using specific input and output pairs. In the book, we built a spam classifier that determined if a given message was spam or not, as well as a model that will follow instructions. That&amp;rsquo;s actually the one that&amp;rsquo;s being trained right now as I write this post, so I&amp;rsquo;m not sure how it will turn out. Based on the fact that it&amp;rsquo;s published in a book, I think it will come out just fine.&lt;/p&gt;
&lt;p&gt;So while I&amp;rsquo;m not completely done with the book, I&amp;rsquo;m very nearly there. I did learn a lot of great concepts, although obviously some of them weren&amp;rsquo;t retained. It would probably behoove me to go back through the book again and quickly breeze through it, in order to refresh my memory and cement my learnings.&lt;/p&gt;
&lt;h2 id="meta-learnings"&gt;Meta learnings&lt;/h2&gt;
&lt;p&gt;Other than the technical aspects of Large Language Models, what else did I learn through this experience?&lt;/p&gt;
&lt;p&gt;Through my experiment with typing all the code samples by hand, I can say that my time would have been better spent with a different approach. If I do this again, I&amp;rsquo;ll probably not type all the code snippets, but rather &amp;ldquo;type&amp;rdquo; them in my mind, and really understand what each line does. The times I learned the most were actually when I made a typo and had to go back through my code to debug it. That forced me to understand what was happening so I could figure out what went wrong.&lt;/p&gt;
&lt;p&gt;I learn better with paper, rather than a digital book. I don&amp;rsquo;t know why. I had both available to me, and I read the first couple of chapters in the paper book. That information stuck better. Maybe because it was earlier in the book and simpler to understand, or maybe the format played into it. But I enjoyed it better, regardless.&lt;/p&gt;
&lt;p&gt;I didn&amp;rsquo;t have to &amp;ldquo;figure out&amp;rdquo; anything, and I think that hampered my learning. There are supplemental exercises in the book, where the author gives you a problem and you have to figure out how to solve it. The answers are given in his GitHub repository. That would have slowed me down a lot, but I&amp;rsquo;m very confident that I would have learned the material better.&lt;/p&gt;
&lt;h2 id="whats-next"&gt;What&amp;rsquo;s next?&lt;/h2&gt;
&lt;p&gt;I&amp;rsquo;m torn right now. I want to understand this material better, but I wonder if getting into lower-level, specific material might help me understand AI and machine learning better. What will likely happen is that I&amp;rsquo;ll copy and paste this content into Claude.ai and suggest a path forward for me.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="update-2025-02-17"&gt;Update: 2025-02-17&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://www.linkedin.com/in/sebastianraschka/"&gt;Sebastian Raschka&lt;/a&gt; sent me a kind message in response to this post and clarified some of my thinking. To quote him:&lt;/p&gt;
&lt;blockquote&gt;
&lt;ol&gt;
&lt;li&gt;&amp;ldquo;Every time it does a loop like this, it tokenizes the entire input (or up to a limit, known as a context limit or context window)&amp;rdquo;. You do this initially when you parse the input text. But then you technically don&amp;rsquo;t need to re-tokenize anything. You can leave the generated output in the tokenized form when creating the next token.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;What I mean is if the text is&lt;/p&gt;
&lt;p&gt;&amp;ldquo;My cat saw a mouse&amp;rdquo;&lt;/p&gt;
&lt;p&gt;The tokens might be &amp;ldquo;123 1 5 6 99&amp;rdquo; (numbers are arbitrary examples). Then the LLM generates the token 801 for &amp;ldquo;jump&amp;rdquo;. Then you simply use &amp;ldquo;123 1 5 6 99 801&amp;rdquo; as the input for the next word.&lt;/p&gt;
&lt;p&gt;When you show the output to the user, then you convert back into text.&lt;/p&gt;
&lt;ol start="2"&gt;
&lt;li&gt;&amp;ldquo;Saving all those relationships between the tokens are known as the “weights” of the model.&amp;rdquo;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I would say that relationships between tokens are the attention scores. The model weights are more like values that are involved in computing things like the attention scores (and other things).&lt;/p&gt;
&lt;p&gt;Now that you finished the book, in case you are bored, I do also have some more materials as bonus material in the GitHub repository.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;d say the GPT-&amp;gt;Llama conversion (&lt;a href="https://github.com/rasbt/LLMs-from-scratch/tree/main/ch05/07_gpt_to_llama"&gt;https://github.com/rasbt/LLMs-from-scratch/tree/main/ch05/07_gpt_to_llama&lt;/a&gt;) and the DPO preference tuning (&lt;a href="https://github.com/rasbt/LLMs-from-scratch/blob/main/ch07/04_preference-tuning-with-dpo/dpo-from-scratch.ipynb"&gt;https://github.com/rasbt/LLMs-from-scratch/blob/main/ch07/04_preference-tuning-with-dpo/dpo-from-scratch.ipynb&lt;/a&gt;) are maybe the most interesting ones.&lt;/p&gt;
&lt;p&gt;I also just uploaded some PyTorch tips for increasing the training speed of the model: &lt;a href="https://github.com/rasbt/LLMs-from-scratch/tree/main/ch05/10_llm-training-speed"&gt;https://github.com/rasbt/LLMs-from-scratch/tree/main/ch05/10_llm-training-speed&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;These materials are less polished than the book itself, but maybe you&amp;rsquo;ll still find them useful!&lt;/p&gt;
&lt;/blockquote&gt;</description></item><item><title>New Blog</title><link>https://brettgfitzgerald.com/posts/new-blog/</link><pubDate>Wed, 05 Feb 2025 00:00:00 +0000</pubDate><guid>https://brettgfitzgerald.com/posts/new-blog/</guid><description>&lt;h2 id="why"&gt;Why?&lt;/h2&gt;
&lt;p&gt;Does the internet need another blog? Definitively, no. Do I have original insights that you will benefit from reading? Most likely not. So what&amp;rsquo;s the point of this blog?&lt;/p&gt;
&lt;p&gt;I recently changed jobs, and I&amp;rsquo;m learning a lot. I retain information better when I describe it to someone. I also have a fear that if I constantly regurgitate my ongoing education to my close family, they will eventually want to murder me. That&amp;rsquo;s where this blog comes in. I&amp;rsquo;m going to teach you what I&amp;rsquo;m learning, so I can learn it better.&lt;/p&gt;
&lt;p&gt;Inevitably, I&amp;rsquo;ll forget about this blog. Posts will become less frequent, and then stop completely. At some point, I&amp;rsquo;ll just stop writing here completely. After a while, I&amp;rsquo;ll find a new use for my domain name, and this will cease to exist, except in the ever growing dataset of archive.org. So, let&amp;rsquo;s get on with it!&lt;/p&gt;</description></item></channel></rss>