{"id":2162,"date":"2026-02-20T10:38:34","date_gmt":"2026-02-20T05:08:34","guid":{"rendered":"https:\/\/sharanamshah.com\/mymusings\/?p=2162"},"modified":"2026-02-20T10:38:54","modified_gmt":"2026-02-20T05:08:54","slug":"how-to-categorize-customer-feedback-at-scale-with-python-and-llms","status":"publish","type":"post","link":"https:\/\/sharanamshah.com\/mymusings\/blog\/2026\/02\/20\/how-to-categorize-customer-feedback-at-scale-with-python-and-llms\/","title":{"rendered":"How to Categorize Customer Feedback at Scale with Python and LLMs"},"content":{"rendered":"<h1>How to Automate Customer Feedback Categorization with Python and LLMs<\/h1>\n<h2>The real-world scenario<\/h2>\n<p>Imagine you are a Lead Developer or Data Analyst at a fast-growing startup. Every Monday morning, you face a <b>data_dump<\/b> containing thousands of unstructured customer support tickets, tweets, and emails. Manually reading these to identify critical bugs versus minor feature requests is a bottleneck that delays product iterations. It is like trying to sort a mountain of mixed LEGO bricks by hand while more bricks are being poured on your head. This script acts as an automated sorting machine, reading every entry and filing it into the correct category with high precision.<\/p>\n<h2>The solution<\/h2>\n<p>We leverage Python and the OpenAI API with <b>Structured Outputs<\/b>. Instead of receiving messy text responses, we force the LLM to adhere to a specific <b>Pydantic<\/b> schema. We use <b>pathlib<\/b> for robust file system navigation to scan a directory of feedback files, process them through the model, and aggregate the results into a structured <b>report.json<\/b>.<\/p>\n<h2>Prerequisites<\/h2>\n<p>Ensure you have Python 3.10 or higher installed. You will need an OpenAI API key. Install the necessary libraries using the following command:<\/p>\n<pre><code>pip install openai pydantic python-dotenv<\/code><\/pre>\n<h2>The code<\/h2>\n<pre><code>\n\"\"\"\n-----------------------------------------------------------------------\nAuthors: Sharanam &amp; Vaishali Shah\nRecipe: Automated Feedback Classifier\nIntent: Use LLMs to convert unstructured text files into structured JSON data.\n-----------------------------------------------------------------------\n\"\"\"\n\nimport json\nfrom pathlib import Path\nfrom typing import List\nfrom pydantic import BaseModel\nfrom openai import OpenAI\nfrom dotenv import load_dotenv\n\n# Load environment variables from .env file\nload_dotenv()\n\n# Define the expected structure of our analysis\nclass FeedbackAnalysis(BaseModel):\n    sentiment: str\n    category: str\n    priority: int\n    summary: str\n\nclass FeedbackProcessor:\n    def __init__(self):\n        self.client = OpenAI()\n        self.input_dir = Path(\"feedback_input\")\n        self.output_file = Path(\"analysis_report.json\")\n        \n        # Create input directory if it does not exist\n        self.input_dir.mkdir(exist_ok=True)\n\n    def analyze_text(self, text: str) -&gt; FeedbackAnalysis:\n        \"\"\"Sends text to LLM and returns structured feedback.\"\"\"\n        response = self.client.beta.chat.completions.parse(\n            model=\"gpt-4o-mini\",\n            messages=[\n                {\"role\": \"system\", \"content\": \"Analyze the customer feedback and categorize it accurately.\"},\n                {\"role\": \"user\", \"content\": text},\n            ],\n            response_format=FeedbackAnalysis,\n        )\n        return response.choices[0].message.parsed\n\n    def process_all_files(self):\n        \"\"\"Iterates through text files and aggregates results.\"\"\"\n        results = []\n        \n        # Use pathlib to find all .txt files\n        files = list(self.input_dir.glob(\"*.txt\"))\n        \n        if not files:\n            print(f\"No files found in {self.input_dir}. Please add .txt files to process.\")\n            return\n\n        for file_path in files:\n            print(f\"Processing: {file_path.name}\")\n            content = file_path.read_text(encoding=\"utf-8\")\n            \n            analysis = self.analyze_text(content)\n            \n            # Combine original metadata with LLM analysis\n            results.append({\n                \"file_name\": file_path.name,\n                \"analysis\": analysis.model_dump()\n            })\n\n        # Save the aggregated data to a JSON file\n        self.output_file.write_text(json.dumps(results, indent=4), encoding=\"utf-8\")\n        print(f\"Success! Report generated at {self.output_file}\")\n\nif __name__ == \"__main__\":\n    processor = FeedbackProcessor()\n    processor.process_all_files()\n<\/code><\/pre>\n<h2>Code walkthrough<\/h2>\n<p>The script begins by importing <b>Path<\/b> from <b>pathlib<\/b>, which provides an object-oriented approach to handling file paths, making the code cross-platform compatible without manual slash adjustments. We define a <b>FeedbackAnalysis<\/b> class using <b>Pydantic<\/b>. This class acts as a contract; it tells the OpenAI API exactly which fields we need (sentiment, category, priority, and summary) and their data types.<\/p>\n<p>The <b>FeedbackProcessor<\/b> class handles the orchestration. Inside <b>analyze_text<\/b>, we use the <b>beta.chat.completions.parse<\/b> method. This is the modern way to handle LLM outputs because it eliminates the need for complex regex or error-prone <b>json.loads()<\/b> calls. The API returns a validated object that matches our <b>FeedbackAnalysis<\/b> model.<\/p>\n<p>The <b>process_all_files<\/b> method uses the <b>glob<\/b> function to find every <b>.txt<\/b> file in the <b>feedback_input<\/b> directory. It reads the raw text, passes it to the AI, and collects the structured objects into a list. Finally, we use <b>write_text<\/b> to save the entire dataset into <b>analysis_report.json<\/b>, ready for a dashboard or a database import.<\/p>\n<h2>Sample output<\/h2>\n<p>When you run the script, the terminal will display the progress for each file found in the <b>feedback_input<\/b> folder:<\/p>\n<pre><code>\nProcessing: ticket_001.txt\nProcessing: ticket_002.txt\nProcessing: tweet_99.txt\nSuccess! Report generated at analysis_report.json\n<\/code><\/pre>\n<p>The resulting <b>analysis_report.json<\/b> will look like this:<\/p>\n<pre><code>\n[\n    {\n        \"file_name\": \"ticket_001.txt\",\n        \"analysis\": {\n            \"sentiment\": \"Negative\",\n            \"category\": \"Bug Report\",\n            \"priority\": 1,\n            \"summary\": \"User unable to reset password via the mobile app.\"\n        }\n    }\n]\n<\/code><\/pre>\n<h2>Conclusion<\/h2>\n<p>Automating data categorization with Python and LLMs removes the human fatigue factor from high-volume operations. By using <b>Structured Outputs<\/b>, you transform an unpredictable AI into a reliable data pipeline component. This script can be scheduled as a cron job or integrated into a CI\/CD pipeline to monitor user sentiment in real-time, ensuring that critical issues never slip through the cracks again.<\/p>\n<hr style=\"margin: 50px 0;border: 0;border-top: 1px solid #eaeaea\">\n<div style=\"background-color: #fcfcfc;padding: 35px;border-left: 5px solid #d63638;border-radius: 6px\">\n<h3 style=\"margin-top: 0;color: #23282d;font-size: 24px;font-weight: 700\">\ud83d\ude80 Don&#8217;t Just Learn AI &amp; LLMs \u2014 Master It.<\/h3>\n<p style=\"font-size: 17px;line-height: 1.6;color: #444;margin-bottom: 20px\">\n            This tutorial was just the tip of the iceberg. To truly advance your career and build <strong>professional-grade systems<\/strong>, you need the full architectural blueprint.\n        <\/p>\n<p style=\"font-size: 17px;line-height: 1.6;color: #444;margin-bottom: 25px\">\n            My book, <strong><a href=\"https:\/\/sharanamshah.com\/mymusings\/home\/books-ive-authored\/large-language-models-crash-course\/\" target=\"_blank\" style=\"color: #d63638;text-decoration: underline\">Large Language Models Crash Course<\/a><\/strong>, takes you from &#8220;making it work&#8221; to &#8220;making it scale.&#8221; I cover advanced patterns, real-world case studies, and the industry best practices that senior engineers use daily.\n        <\/p>\n<p style=\"margin-bottom: 0\">\n            <a href=\"https:\/\/sharanamshah.com\/mymusings\/home\/books-ive-authored\/large-language-models-crash-course\/\" target=\"_blank\" style=\"background-color: #d63638;color: white;padding: 15px 30px;text-decoration: none;border-radius: 5px;font-weight: bold;font-size: 18px;display: inline-block\"><br \/>\n                \ud83d\udcd6 Grab Your Copy Now &rarr;<br \/>\n            <\/a>\n        <\/p>\n<\/p><\/div>\n","protected":false},"excerpt":{"rendered":"<p>Automate sentiment analysis and ticket categorization using Python and OpenAI Structured Outputs. Process unstructured data into clean JSON reports.<\/p>\n","protected":false},"author":1,"featured_media":1989,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_themeisle_gutenberg_block_has_review":false,"footnotes":""},"categories":[94],"tags":[],"class_list":["post-2162","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-everyday-automation-hacks"],"_links":{"self":[{"href":"https:\/\/sharanamshah.com\/mymusings\/wp-json\/wp\/v2\/posts\/2162","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/sharanamshah.com\/mymusings\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/sharanamshah.com\/mymusings\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/sharanamshah.com\/mymusings\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/sharanamshah.com\/mymusings\/wp-json\/wp\/v2\/comments?post=2162"}],"version-history":[{"count":1,"href":"https:\/\/sharanamshah.com\/mymusings\/wp-json\/wp\/v2\/posts\/2162\/revisions"}],"predecessor-version":[{"id":2172,"href":"https:\/\/sharanamshah.com\/mymusings\/wp-json\/wp\/v2\/posts\/2162\/revisions\/2172"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/sharanamshah.com\/mymusings\/wp-json\/wp\/v2\/media\/1989"}],"wp:attachment":[{"href":"https:\/\/sharanamshah.com\/mymusings\/wp-json\/wp\/v2\/media?parent=2162"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/sharanamshah.com\/mymusings\/wp-json\/wp\/v2\/categories?post=2162"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/sharanamshah.com\/mymusings\/wp-json\/wp\/v2\/tags?post=2162"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}