<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Pete Letkeman</title>
    <description>The latest articles on Forem by Pete Letkeman (@pbaletkeman).</description>
    <link>https://forem.com/pbaletkeman</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1234322%2Fee63d7e8-0339-4890-8e4b-ca26dfa04662.png</url>
      <title>Forem: Pete Letkeman</title>
      <link>https://forem.com/pbaletkeman</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/pbaletkeman"/>
    <language>en</language>
    <item>
      <title>Using AI to Learn and Prepare for Certification Exams</title>
      <dc:creator>Pete Letkeman</dc:creator>
      <pubDate>Sun, 03 May 2026 02:08:58 +0000</pubDate>
      <link>https://forem.com/pbaletkeman/using-ai-to-learn-and-prepare-for-certification-exams-1og</link>
      <guid>https://forem.com/pbaletkeman/using-ai-to-learn-and-prepare-for-certification-exams-1og</guid>
      <description>&lt;p&gt;Certification prep can feel like a maze. There are endless courses, playlists, blogs, and PDFs — and choosing where to start often takes longer than the studying itself. If you’re willing to bring AI/LLMs into your workflow, you can skip a lot of that friction. AI can help you build a study plan, generate exam‑style questions, and even simulate full practice tests.&lt;/p&gt;

&lt;p&gt;Here’s how I’ve been using AI as a study partner in a way that fits naturally into a developer workflow.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Build a Study Plan with AI
&lt;/h2&gt;

&lt;p&gt;If you’re staring at a blank page wondering how to begin, give the AI a snapshot of your background and constraints. The more specific the input, the more useful the output.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example scenario:&lt;/strong&gt;&lt;br&gt;
You’re a Java developer with five years of experience, familiar with Spring Boot and Hibernate. You want to learn Python 3.12 in eight weeks with only five hours per week.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example prompt:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;I'm a Java developer with five years of experience, familiar with Spring Boot and Hibernate.
Create a customized study plan to help me learn Python 3.12.
I have five hours a week and need to learn it in eight weeks.
ALWAYS verify all sites you link to before citing them.
Include videos, reading materials, and hands-on practice.
Double-check that all recommendations are valid and up to date.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives the AI enough context to produce a structured, time‑boxed plan with curated resources instead of generic advice.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Generate Exam‑Style Questions
&lt;/h2&gt;

&lt;p&gt;Most certifications publish an exam outline or topic breakdown. That outline is your blueprint — it tells you exactly what the exam will cover.&lt;/p&gt;

&lt;p&gt;A few examples of publicly available exam guides:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://learn.github.com/certification/COPILOT" rel="noopener noreferrer"&gt;https://learn.github.com/certification/COPILOT&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.databricks.com/learn/training/certification#certifications" rel="noopener noreferrer"&gt;https://www.databricks.com/learn/training/certification#certifications&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.broadcom.com/support/education/software/certification/exams/spring-pro-develop-exam" rel="noopener noreferrer"&gt;https://www.broadcom.com/support/education/software/certification/exams/spring-pro-develop-exam&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once you have the outline, you can ask AI to generate questions that match the categories and weightings.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example prompt:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Using the exam outline from https://learn.github.com/certification/COPILOT,
create 100 exam-like multiple-choice questions of varying difficulty.
Follow the categories and weights from the exam guide.
ENSURE all questions are unique, accurate, and fully validated.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run this multiple times and you’ll quickly build a large, diverse pool of exam‑style questions aligned with the actual blueprint.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Create Practice Tests
&lt;/h2&gt;

&lt;p&gt;With a study plan and a question bank, you can simulate real exam conditions.&lt;/p&gt;

&lt;p&gt;A few practical tips:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Keep the question format consistent (you can enforce this in your prompt).&lt;/li&gt;
&lt;li&gt;Randomize both the question order and the answer choices.&lt;/li&gt;
&lt;li&gt;Don’t memorize answers — focus on understanding the underlying concepts.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Example question format:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Question: What is the answer to life, living, and the universe?
A) 42
B) 40
C) unknown
D) none
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once you have a set of questions, you can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Build a lightweight quiz engine&lt;/strong&gt; in your language of choice, or&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Ask AI to scaffold one for you.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I built a simple quiz engine in VS Code that loads questions from a file, shuffles them, and randomizes answer order. It’s repeatable, language‑agnostic, and perfect for drilling through large question sets.&lt;/p&gt;




&lt;h2&gt;
  
  
  My Work &amp;amp; Experiments
&lt;/h2&gt;

&lt;p&gt;If you want to see this approach in action, here’s the repo where I’ve been experimenting:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Main repo:&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://github.com/pbaletkeman/exam-prep" rel="noopener noreferrer"&gt;https://github.com/pbaletkeman/exam-prep&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I started with GitHub Copilot learning material (not in this repo), then moved on to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GitHub Actions:
&lt;a href="https://github.com/pbaletkeman/exam-prep/tree/main/actions-source-material" rel="noopener noreferrer"&gt;https://github.com/pbaletkeman/exam-prep/tree/main/actions-source-material&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Terraform:
&lt;a href="https://github.com/pbaletkeman/exam-prep/tree/main/terraform/learning" rel="noopener noreferrer"&gt;https://github.com/pbaletkeman/exam-prep/tree/main/terraform/learning&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Databricks:
&lt;a href="https://github.com/pbaletkeman/exam-prep/tree/main/databricks/learning" rel="noopener noreferrer"&gt;https://github.com/pbaletkeman/exam-prep/tree/main/databricks/learning&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For the fun of it I had GitHub Copilot create the same quiz engines for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/pbaletkeman/exam-prep/tree/main/quiz-engine/quiz-engine-python" rel="noopener noreferrer"&gt;Python&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/pbaletkeman/exam-prep/tree/main/quiz-engine/quiz-engine-java" rel="noopener noreferrer"&gt;Java&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/pbaletkeman/exam-prep/tree/main/quiz-engine/quiz-engine-springboot" rel="noopener noreferrer"&gt;Java SpringBoot&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/pbaletkeman/exam-prep/tree/main/quiz-engine/quiz-engine-golang" rel="noopener noreferrer"&gt;Go&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/pbaletkeman/exam-prep/tree/main/quiz-engine/quiz-engine-dart" rel="noopener noreferrer"&gt;Dart&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/pbaletkeman/exam-prep/tree/main/quiz-engine/quiz-engine-nodejs" rel="noopener noreferrer"&gt;NodeJS&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/pbaletkeman/exam-prep/tree/main/quiz-engine/quiz-engine-csharp" rel="noopener noreferrer"&gt;C#&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/pbaletkeman/exam-prep/tree/main/quiz-engine/quiz-engine-rust" rel="noopener noreferrer"&gt;Rust&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each version lives here:&lt;br&gt;
&lt;code&gt;https://github.com/pbaletkeman/exam-prep/tree/main/quiz-engine&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;It’s a great way to compare language ergonomics while keeping the logic identical.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;AI won’t replace real studying - but it &lt;em&gt;will&lt;/em&gt; remove the overhead that slows you down. Instead of spending hours hunting for resources or manually building practice material, you can jump straight into structured learning, targeted practice, and realistic exam simulations.&lt;br&gt;
If you’re preparing for a certification and want to accelerate your workflow, AI is absolutely worth adding to your toolkit.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>career</category>
      <category>learning</category>
    </item>
    <item>
      <title>AI-Powered Resume Generator: Architecture &amp; Implementation</title>
      <dc:creator>Pete Letkeman</dc:creator>
      <pubDate>Sat, 24 Jan 2026 01:18:09 +0000</pubDate>
      <link>https://forem.com/pbaletkeman/ai-powered-resume-generator-architecture-implementation-2748</link>
      <guid>https://forem.com/pbaletkeman/ai-powered-resume-generator-architecture-implementation-2748</guid>
      <description>&lt;h1&gt;
  
  
  Building an AI-Powered Resume Generator: Architecture &amp;amp; Implementation
&lt;/h1&gt;

&lt;h2&gt;
  
  
  Overview
&lt;/h2&gt;

&lt;p&gt;I've been working on a full-stack application that leverages LLMs to generate polished, professional resume content. This post is a technical walkthrough of the architecture, integration points, and key implementation details.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tech Stack:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Backend:&lt;/strong&gt; Java 21, Spring Boot 3.x, Gradle&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Frontend:&lt;/strong&gt; React 19, TypeScript, Vite&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LLM Integration:&lt;/strong&gt; OpenAI API / Ollama (OpenAI-compatible endpoint)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data Format:&lt;/strong&gt; JSON-driven resume model&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build Tooling:&lt;/strong&gt; Gradle (backend), Node (frontend)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Repository: &lt;a href="https://github.com/pbaletkeman/java-resumes" rel="noopener noreferrer"&gt;https://github.com/pbaletkeman/java-resumes&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Architecture Overview
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────┐
│   React UI  │
│ (TypeScript)│
└──────┬──────┘
       │ HTTP/REST
       ↓
┌─────────────────────────────────┐
│   Spring Boot REST API          │
│  (Java 21, Gradle 8.10)         │
│                                 │
│  ├─ ResumeController            │
│  ├─ FilesStorageService         │
│  └─ ApiService (LLM gateway)    │
└──────────┬──────────────────────┘
           │
      ┌────┴─────┐
      ↓          ↓
  ┌────────┐  ┌────────────┐
  │ Ollama │  │ OpenAI API │
  │(local) │  │  (cloud)   │
  └────────┘  └────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Key Components &amp;amp; Design Decisions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. &lt;strong&gt;REST API Layer (Spring Boot)&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;The backend exposes endpoints for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;File uploads&lt;/strong&gt; (multipart/form-data)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resume optimization&lt;/strong&gt; (async background processing)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;File retrieval&lt;/strong&gt; (results polling)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;File management&lt;/strong&gt; (list, download, delete)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Key Endpoint Pattern:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@PostMapping&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"/api/upload"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;ResponseEntity&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ResponseMessage&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;optimizeResume&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
    &lt;span class="nd"&gt;@RequestParam&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"optimize"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;optimizeJson&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="nd"&gt;@RequestParam&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"resume"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nc"&gt;MultipartFile&lt;/span&gt; &lt;span class="n"&gt;resume&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="nd"&gt;@RequestParam&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"job"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nc"&gt;MultipartFile&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="c1"&gt;// Validate inputs&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resume&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isEmpty&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isEmpty&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;ResponseEntity&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;HttpStatus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;BAD_REQUEST&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ResponseMessage&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"No file/invalid file provided"&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Spawn background thread for LLM processing (non-blocking)&lt;/span&gt;
    &lt;span class="nc"&gt;Thread&lt;/span&gt; &lt;span class="n"&gt;thread&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Thread&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;BackgroundResume&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;optimize&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
    &lt;span class="n"&gt;thread&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;start&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;// Return 202 Accepted immediately&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;ResponseEntity&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;HttpStatus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ACCEPTED&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ResponseMessage&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"generating"&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why this pattern?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;LLM API calls are slow (2-30+ seconds)&lt;/li&gt;
&lt;li&gt;HTTP connections timeout if we wait for LLM&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;202 Accepted&lt;/code&gt; signals async processing to the client&lt;/li&gt;
&lt;li&gt;Frontend polls &lt;code&gt;/api/files&lt;/code&gt; until results appear&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. &lt;strong&gt;Async Background Processing&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;BackgroundResume&lt;/code&gt; class handles long-running operations:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;BackgroundResume&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;Runnable&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;Optimize&lt;/span&gt; &lt;span class="n"&gt;optimize&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="c1"&gt;// 1. Load LLM configuration&lt;/span&gt;
            &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;configStr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Utility&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;readFileAsString&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"config.json"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
            &lt;span class="nc"&gt;Config&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Gson&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;fromJson&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;configStr&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Config&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

            &lt;span class="c1"&gt;// 2. Build LLM request&lt;/span&gt;
            &lt;span class="nc"&gt;ChatBody&lt;/span&gt; &lt;span class="n"&gt;chatBody&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ApiService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;createChatBody&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;optimize&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

            &lt;span class="c1"&gt;// 3. Call LLM (OpenAI-compatible API)&lt;/span&gt;
            &lt;span class="nc"&gt;LLMResponse&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ApiService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;produceFiles&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;optimize&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getEndpoint&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt;
                &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getApikey&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt;
                &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getModel&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
            &lt;span class="o"&gt;);&lt;/span&gt;

            &lt;span class="c1"&gt;// 4. Save results (Markdown + PDF)&lt;/span&gt;
            &lt;span class="nc"&gt;FilesStorageService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;save&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getContent&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;

            &lt;span class="no"&gt;LOGGER&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;info&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Resume optimization completed"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Exception&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="no"&gt;LOGGER&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Background task failed: {}"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getMessage&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why background threads instead of async/await?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Simple, synchronous model&lt;/li&gt;
&lt;li&gt;No need for reactive framework overhead&lt;/li&gt;
&lt;li&gt;Easy to reason about error handling&lt;/li&gt;
&lt;li&gt;Works well for moderate concurrency&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. &lt;strong&gt;LLM Integration (OpenAI-Compatible API)&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;ApiService&lt;/code&gt; class abstracts LLM provider differences:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ApiService&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="nc"&gt;LLMResponse&lt;/span&gt; &lt;span class="nf"&gt;produceFiles&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
        &lt;span class="nc"&gt;Optimize&lt;/span&gt; &lt;span class="n"&gt;optimize&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
        &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;endpoint&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
        &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;apiKey&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
        &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;throws&lt;/span&gt; &lt;span class="nc"&gt;Exception&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

        &lt;span class="c1"&gt;// Build OpenAI-compatible request&lt;/span&gt;
        &lt;span class="nc"&gt;ChatBody&lt;/span&gt; &lt;span class="n"&gt;chatBody&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ChatBody&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="n"&gt;chatBody&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setModel&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;chatBody&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setMessages&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;createPrompt&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;optimize&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
        &lt;span class="n"&gt;chatBody&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setTemperature&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;optimize&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getTemperature&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;

        &lt;span class="c1"&gt;// Send to LLM&lt;/span&gt;
        &lt;span class="nc"&gt;HttpClient&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;HttpClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;newHttpClient&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;jsonRequest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Gson&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;toJson&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chatBody&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

        &lt;span class="nc"&gt;HttpRequest&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;HttpRequest&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;newBuilder&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;uri&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;URI&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;create&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;endpoint&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;"/v1/chat/completions"&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;header&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Authorization"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Bearer "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;apiKey&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;header&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Content-Type"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"application/json"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;POST&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;HttpRequest&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;BodyPublishers&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ofString&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jsonRequest&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

        &lt;span class="nc"&gt;HttpResponse&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;send&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
            &lt;span class="nc"&gt;HttpResponse&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;BodyHandlers&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ofString&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// Parse response&lt;/span&gt;
        &lt;span class="nc"&gt;LLMResponse&lt;/span&gt; &lt;span class="n"&gt;llmResponse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Gson&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;fromJson&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt;
            &lt;span class="nc"&gt;LLMResponse&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;
        &lt;span class="o"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;llmResponse&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why OpenAI-compatible format?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Works with Ollama (local models)&lt;/li&gt;
&lt;li&gt;Works with OpenAI (cloud models)&lt;/li&gt;
&lt;li&gt;Works with Azure OpenAI, Together.ai, etc.&lt;/li&gt;
&lt;li&gt;Single integration code path&lt;/li&gt;
&lt;li&gt;Easy to swap providers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Configuration (config.json):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"endpoint"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http://localhost:11434"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"apikey"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ollama"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"model"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"mistral:7b"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Frontend Architecture (React + TypeScript)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Core Hook: &lt;code&gt;useApi&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Centralized API communication:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;useApi&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;loading&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setLoading&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setError&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;execute&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;setLoading&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;setError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;setError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nb"&gt;Error&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Unknown error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;setLoading&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;loading&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  File Upload &amp;amp; Polling Pattern
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;MainContentTab&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;loading&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useApi&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;generatedFiles&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setGeneratedFiles&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;File&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;([]);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handleSubmit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FormData&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// 1. Upload resume + job description&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;fileService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uploadForOptimization&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="c1"&gt;// 2. Start polling for results&lt;/span&gt;
      &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;attempts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;attempts&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="c1"&gt;// 5 minutes max&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="c1"&gt;// Poll every 5s&lt;/span&gt;

        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;files&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;fileService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listFiles&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;newFiles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;files&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
          &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;endsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.pdf&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
          &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timestamp&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;uploadTime&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;newFiles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nf"&gt;setGeneratedFiles&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;newFiles&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
          &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="nx"&gt;attempts&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="c1"&gt;// UI for upload and display results&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why polling instead of WebSockets?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Simpler client/server contract&lt;/li&gt;
&lt;li&gt;Works through corporate proxies/firewalls&lt;/li&gt;
&lt;li&gt;No need for persistent connection&lt;/li&gt;
&lt;li&gt;Acceptable for batch processing workflows&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Data Model: Optimize DTO
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Optimize&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;promptType&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;      &lt;span class="c1"&gt;// ["Resume", "CoverLetter", "Skills"]&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;temperature&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;        &lt;span class="c1"&gt;// 0.0-1.0 (creativity level)&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;              &lt;span class="c1"&gt;// Model identifier from config&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;company&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;            &lt;span class="c1"&gt;// Target company name&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;jobTitle&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;           &lt;span class="c1"&gt;// Target job title&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;jobDescription&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;     &lt;span class="c1"&gt;// Full job posting text&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;resume&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;             &lt;span class="c1"&gt;// User's current resume&lt;/span&gt;

    &lt;span class="c1"&gt;// Getters/setters...&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This DTO drives:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Prompt construction&lt;/strong&gt; - What content to generate&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LLM parameters&lt;/strong&gt; - Temperature, model selection&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Output filtering&lt;/strong&gt; - Which sections to include&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Key Implementation Challenges &amp;amp; Solutions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Challenge 1: LLM Response Time
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Problem:&lt;/strong&gt; API calls can take 10-30+ seconds&lt;br&gt;
&lt;strong&gt;Solution:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Return &lt;code&gt;202 Accepted&lt;/code&gt; immediately&lt;/li&gt;
&lt;li&gt;Process async in background thread&lt;/li&gt;
&lt;li&gt;Frontend polls for completion&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Challenge 2: File Format Conversion
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Problem:&lt;/strong&gt; LLM outputs plain text; need PDF with formatting&lt;br&gt;
&lt;strong&gt;Solution:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Convert Markdown → HTML (CommonMark parser)&lt;/li&gt;
&lt;li&gt;Convert HTML → PDF (Flying Saucer library)&lt;/li&gt;
&lt;li&gt;Save both Markdown + PDF for flexibility&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Challenge 3: Local vs Cloud LLM
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Problem:&lt;/strong&gt; Different APIs for Ollama vs OpenAI&lt;br&gt;
&lt;strong&gt;Solution:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use OpenAI-compatible format (both support it)&lt;/li&gt;
&lt;li&gt;Config-driven endpoint selection&lt;/li&gt;
&lt;li&gt;Single integration point&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Challenge 4: Test Isolation
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Problem:&lt;/strong&gt; Tests failing due to state dependencies (file existence)&lt;br&gt;
&lt;strong&gt;Solution:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@BeforeEach&lt;/span&gt;
&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;setUp&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="kd"&gt;throws&lt;/span&gt; &lt;span class="nc"&gt;IOException&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;Path&lt;/span&gt; &lt;span class="n"&gt;uploadsPath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Paths&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"uploads"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="nc"&gt;Files&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;createDirectories&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uploadsPath&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="c1"&gt;// Create dummy files for delete tests, etc.&lt;/span&gt;
    &lt;span class="nc"&gt;Files&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;write&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uploadsPath&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;resolve&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"resume.pdf"&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;
        &lt;span class="s"&gt;"dummy"&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getBytes&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Deployment Considerations
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Local Development
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Terminal 1: Start Ollama&lt;/span&gt;
ollama serve
ollama pull mistral:7b

&lt;span class="c"&gt;# Terminal 2: Run backend&lt;/span&gt;
./gradlew bootRun  &lt;span class="c"&gt;# Listens on :8080&lt;/span&gt;

&lt;span class="c"&gt;# Terminal 3: Run frontend&lt;/span&gt;
&lt;span class="nb"&gt;cd &lt;/span&gt;frontend &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm run dev  &lt;span class="c"&gt;# Listens on :5173&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Cloud Deployment
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="c"&gt;# application.properties
&lt;/span&gt;&lt;span class="py"&gt;server.port&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;8080&lt;/span&gt;
&lt;span class="py"&gt;upload.path&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/data/uploads&lt;/span&gt;
&lt;span class="c"&gt;# Spring will detect OpenAI config from environment
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Testing Strategy
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;80%+ Coverage Target:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Controller Tests&lt;/strong&gt; - HTTP layer with MockMvc&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Service Tests&lt;/strong&gt; - Business logic, mocked LLM&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Integration Tests&lt;/strong&gt; - Full request flow&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Model Tests&lt;/strong&gt; - DTO serialization/validation
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./gradlew &lt;span class="nb"&gt;test&lt;/span&gt;                    &lt;span class="c"&gt;# Run all tests&lt;/span&gt;
./gradlew &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;--tests&lt;/span&gt; ClassName  &lt;span class="c"&gt;# Run specific test&lt;/span&gt;
./gradlew checkstyleMain          &lt;span class="c"&gt;# Code quality (100% compliance)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Performance &amp;amp; Scalability Notes
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Horizontal Scaling:&lt;/strong&gt; Add more backend instances behind load balancer&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rate Limiting:&lt;/strong&gt; Implement per-user quotas for LLM API costs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Caching:&lt;/strong&gt; Cache LLM responses for identical inputs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Async Queue:&lt;/strong&gt; For high volume, use message queue (RabbitMQ, Kafka)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;File Storage:&lt;/strong&gt; Consider cloud storage (S3, Azure Blob) vs local filesystem&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  ⚠️ Important Considerations
&lt;/h2&gt;

&lt;h3&gt;
  
  
  LLM Hallucination Risk
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Critical:&lt;/strong&gt; LLMs can generate plausible-sounding but inaccurate content. This includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Fabricated job experiences&lt;/li&gt;
&lt;li&gt;Incorrect technical skills&lt;/li&gt;
&lt;li&gt;Made-up company names or achievements&lt;/li&gt;
&lt;li&gt;Dates and timelines that don't align with reality&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Mitigation:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Always proofread generated content&lt;/strong&gt; before using it&lt;/li&gt;
&lt;li&gt;Cross-check facts against source documents&lt;/li&gt;
&lt;li&gt;Verify all claims in the resume&lt;/li&gt;
&lt;li&gt;Consider this tool as a &lt;strong&gt;content enhancement tool&lt;/strong&gt;, not a replacement for human review&lt;/li&gt;
&lt;li&gt;Use it to refine and polish verified information, not to generate unverified content&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Processing Time
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Important:&lt;/strong&gt; File generation is NOT instant:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Local models (Ollama):&lt;/strong&gt; 30 seconds to 5+ minutes depending on model size (7B models are faster, 13B+ models take longer)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cloud models (OpenAI):&lt;/strong&gt; 5-30 seconds typically, but can vary with load&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Large job descriptions:&lt;/strong&gt; Processing time increases with input size&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Network latency:&lt;/strong&gt; Slower connections add to total time&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Frontend Polling:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Default: polls every 5 seconds for up to 5 minutes (60 attempts)&lt;/span&gt;
&lt;span class="c1"&gt;// For longer processing, increase attempts or polling interval&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;attempts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;attempts&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Adjust this for longer waits&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="c1"&gt;// 5 seconds&lt;/span&gt;
  &lt;span class="c1"&gt;// ... check for files&lt;/span&gt;
  &lt;span class="nx"&gt;attempts&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;User Experience:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Display a progress indicator during processing&lt;/li&gt;
&lt;li&gt;Show estimated wait time based on model selection&lt;/li&gt;
&lt;li&gt;Allow users to check back later via job ID&lt;/li&gt;
&lt;li&gt;Consider implementing email notifications when complete&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Code Quality Standards
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Checkstyle:&lt;/strong&gt; 100% compliance (120 char line limit)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test Coverage:&lt;/strong&gt; 80%+ target&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Java Version:&lt;/strong&gt; Java 21 LTS with modern features&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Spring Boot:&lt;/strong&gt; Version 3.5.1 with latest practices&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What's Next?
&lt;/h2&gt;

&lt;p&gt;Potential improvements:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] WebSocket support for real-time updates&lt;/li&gt;
&lt;li&gt;[ ] Template system for different resume formats&lt;/li&gt;
&lt;li&gt;[ ] Batch processing for multiple candidates&lt;/li&gt;
&lt;li&gt;[ ] Integration with LinkedIn/job boards&lt;/li&gt;
&lt;li&gt;[ ] A/B testing for LLM prompt optimization&lt;/li&gt;
&lt;li&gt;[ ] Cost analytics for OpenAI usage&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Lessons Learned
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Async by Default&lt;/strong&gt; - HTTP endpoints should never block on slow operations&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Embrace Standards&lt;/strong&gt; - OpenAI-compatible API is a superpower&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Simple Patterns &amp;gt; Complex Frameworks&lt;/strong&gt; - Background threads work great for this use case&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test Independence&lt;/strong&gt; - Always set up required state in @BeforeEach&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Config Over Code&lt;/strong&gt; - Keep LLM provider flexible via configuration&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Get Started
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Repository:&lt;/strong&gt; &lt;a href="https://github.com/pbaletkeman/java-resumes" rel="noopener noreferrer"&gt;https://github.com/pbaletkeman/java-resumes&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Quick Start:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/pbaletkeman/java-resumes
&lt;span class="nb"&gt;cd &lt;/span&gt;java-resumes
./gradlew clean build
./gradlew bootRun
&lt;span class="c"&gt;# Visit http://localhost:8080/spotlight/index.html&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Credits
&lt;/h2&gt;

&lt;p&gt;Special thanks to &lt;strong&gt;Shaw Talebi&lt;/strong&gt; for his excellent &lt;a href="https://www.youtube.com/watch?v=R5WXaxmb6m4" rel="noopener noreferrer"&gt;tutorial on building resume optimization tools&lt;/a&gt;, which served as the inspiration and starter foundation for this project.&lt;/p&gt;




&lt;p&gt;Have you built LLM integrations in Java? What patterns did you use? Drop a comment!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Discussion Topics:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Async patterns for LLM integrations&lt;/li&gt;
&lt;li&gt;Local vs cloud LLM trade-offs&lt;/li&gt;
&lt;li&gt;Resume optimization strategies&lt;/li&gt;
&lt;li&gt;Full-stack Java + React workflows&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>java</category>
      <category>ollama</category>
      <category>springboot</category>
      <category>llm</category>
    </item>
    <item>
      <title>Secure Python Litestar Site With OpenID/OIDC Using Okta Hosted Login</title>
      <dc:creator>Pete Letkeman</dc:creator>
      <pubDate>Fri, 16 Feb 2024 22:09:18 +0000</pubDate>
      <link>https://forem.com/pbaletkeman/secure-python-litestar-site-with-openidoidc-using-okta-hosted-login-38nf</link>
      <guid>https://forem.com/pbaletkeman/secure-python-litestar-site-with-openidoidc-using-okta-hosted-login-38nf</guid>
      <description>&lt;p&gt;Here is a tutorial on how to setup &lt;a href="https://www.okta.com"&gt;OKTA&lt;/a&gt; as a OpenID/OIDC security provider for a site programmed in Python 3.11.x using &lt;a href="https://www.litestar.dev"&gt;Litestar&lt;/a&gt;.&lt;br&gt;
This is done with 'Authorization Code flow'&lt;/p&gt;
&lt;h4&gt;
  
  
  About the Authorization Code grant
&lt;/h4&gt;

&lt;p&gt;The Authorization Code flow is the recommended method for controlling access to web applications capable of securely storing secrets, see &lt;a href="https://developer.okta.com/docs/guides/implement-grant-type/authcode/main/"&gt;https://developer.okta.com/docs/guides/implement-grant-type/authcode/main/&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  OKTA Setup
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Step 1
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;tr&gt;
&lt;td&gt;
After you have created your OKTA account open `Applications` =&amp;gt; `Applications` as seen on the right.
&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/t2oqsbt35lsghlo293ya.png"&gt;
&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ft2oqsbt35lsghlo293ya.png" width="308" height="630"&gt;&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h3&gt;
  
  
  Step 2
&lt;/h3&gt;

&lt;p&gt;Click &lt;code&gt;Create App Integration&lt;/code&gt;&lt;br&gt;
&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnvzikq1c8p66ntdl8xp7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnvzikq1c8p66ntdl8xp7.png" alt="Image description" width="800" height="236"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 3
&lt;/h3&gt;

&lt;p&gt;Choose &lt;code&gt;OIDC&lt;/code&gt; - Open Connect with a description of&lt;br&gt;
'Token-based OAuth 2.0 authentication for Single Sign-On (SSO) through API endpoints. Recommended if you intend to build a custom app integration with the Okta Sign-In Widget.'&lt;br&gt;
&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx7ire96qy769y2tc4o8z.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx7ire96qy769y2tc4o8z.png" alt="Image description" width="800" height="703"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And then choose &lt;code&gt;Web Application&lt;/code&gt; with a description of &lt;br&gt;
'Server-side applications where authentication and tokens are handled on the server (for example, Go, Java, ASP.Net, Node.js, PHP)'&lt;/p&gt;

&lt;p&gt;And Click &lt;code&gt;Next&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 4
&lt;/h3&gt;

&lt;p&gt;Checked the following (&lt;em&gt;may not matter&lt;/em&gt;):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Client Credentials&lt;/li&gt;
&lt;li&gt;Refresh Token&lt;/li&gt;
&lt;li&gt;Implicit (hybrid)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6czzrrbq7lk411l3643i.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6czzrrbq7lk411l3643i.png" alt="Image description" width="800" height="467"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 5
&lt;/h3&gt;

&lt;p&gt;Ensure that both &lt;code&gt;Sign-in redirect URIs&lt;/code&gt; and &lt;code&gt;Sign-out redirect URIs (Optional) have the correct values&lt;/code&gt;. By default Litestar brings up the web site on port 8000.&lt;br&gt;
&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnl1eaa565ocxxtwytxmu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnl1eaa565ocxxtwytxmu.png" alt="Image description" width="800" height="261"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 6
&lt;/h3&gt;

&lt;p&gt;In &lt;code&gt;Assignments&lt;/code&gt; select &lt;code&gt;Allow everyone in your organization to access&lt;/code&gt;.&lt;br&gt;
In &lt;code&gt;Enable immediate access (Recommended)&lt;/code&gt; select &lt;code&gt;Enable immediate access with *Federation Broker Model*&lt;/code&gt;&lt;br&gt;
Click &lt;code&gt;Save&lt;/code&gt;&lt;br&gt;
&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4pqduqiox4v6sb5hp8g6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4pqduqiox4v6sb5hp8g6.png" alt="Image description" width="800" height="369"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 7
&lt;/h3&gt;

&lt;p&gt;Create a text file named '&lt;strong&gt;.env&lt;/strong&gt;' and from the &lt;code&gt;General&lt;/code&gt; tab, copy both &lt;code&gt;Client ID&lt;/code&gt; and &lt;code&gt;Client Secret&lt;/code&gt; to the file to have something like the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CLIENT_ID=your_client_id
CLIENT_SECRET=your_client_secret
OKTA_DOMAIN=dev-123456789.okta.com
REDIRECT_URL=http://localhost:8000/authorization-code/callback
OKTA_PROMPT=consent
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And provide the you value for &lt;code&gt;OKTA_DOMAIN&lt;/code&gt;.&lt;br&gt;&lt;br&gt;
You do not to supply a value for &lt;code&gt;OKTA_PROMPT&lt;/code&gt; or even include it if you choose not to. For more details about regarding this parameter see &lt;a href="https://developer.okta.com/docs/reference/api/oidc/#parameter-details"&gt;https://developer.okta.com/docs/reference/api/oidc/#parameter-details&lt;/a&gt;.&lt;br&gt;
Using OKTA_PROMPT is very helpful when testing multiple user accounts.&lt;/p&gt;
&lt;h2&gt;
  
  
  Python Code
&lt;/h2&gt;

&lt;p&gt;Github.com repository found here &lt;a href="https://github.com/pbaletkeman/litestarOpenID"&gt;https://github.com/pbaletkeman/litestarOpenID&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Copy the .env file from the previous step to the root directory of your Python project.&lt;/p&gt;

&lt;p&gt;For the most part the code is well documented, however here are some notes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It uses Mako templates, &lt;a href="https://www.makotemplates.org/"&gt;https://www.makotemplates.org/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;/profile is not secured, and is used to show you JWT Bearer Token which is needed to access /read-items and other secure routes, this is set in the callback method&lt;/li&gt;
&lt;li&gt;More user properties are found in the &lt;code&gt;user_response&lt;/code&gt; object located in the callback method&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Additionality PKCE
&lt;/h2&gt;

&lt;p&gt;You can use Authorization Code Grant with Proof Key for Code Exchange (PKCE) which you can read about here:&lt;br&gt;
&lt;a href="https://developer.okta.com/docs/guides/implement-grant-type/authcodepkce/main/#about-the-authorization-code-grant-with-pkce"&gt;https://developer.okta.com/docs/guides/implement-grant-type/authcodepkce/main/#about-the-authorization-code-grant-with-pkce&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;To enforce PKCE you will have to select 'Require PKCE as additional verification' as seen in the image below.&lt;br&gt;
&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq4o3728k2cl5hdxh69mu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq4o3728k2cl5hdxh69mu.png" alt="Image description" width="753" height="403"&gt;&lt;/a&gt;&lt;br&gt;
You can still use PKCE without requiring it.&lt;/p&gt;

&lt;p&gt;Here is the Python code for this,&lt;br&gt;
&lt;a href="https://github.com/pbaletkeman/okta-litestar-oidc-pkce"&gt;https://github.com/pbaletkeman/okta-litestar-oidc-pkce&lt;/a&gt;&lt;br&gt;
Which is a lot like the none PKCE code.&lt;br&gt;
Note that the following have been added/changed:&lt;/p&gt;

&lt;p&gt;lines: 181, 182, 192, 193&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;'code_challenge': code_challenge,
'code_challenge_method': 'S256',
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;line: 231&lt;br&gt;
&lt;code&gt;query_params = {'grant_type': 'authorization_code', 'code': code,&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;line: 185, 197 &lt;br&gt;
And the &lt;code&gt;response_mode&lt;/code&gt; has been changed from 'query' to 'form_post' which tells OKTA to send the data as a form post instead of using the query string. Query string is still valid and can be used if you choose to.&lt;/p&gt;

</description>
      <category>tutorial</category>
      <category>python</category>
      <category>security</category>
      <category>programming</category>
    </item>
    <item>
      <title>Secure Litestar APIs with OKTA</title>
      <dc:creator>Pete Letkeman</dc:creator>
      <pubDate>Sat, 10 Feb 2024 20:08:09 +0000</pubDate>
      <link>https://forem.com/pbaletkeman/secure-litestar-apis-with-okta-570l</link>
      <guid>https://forem.com/pbaletkeman/secure-litestar-apis-with-okta-570l</guid>
      <description>&lt;p&gt;I borrowed a lot from &lt;a href="https://developer.okta.com/blog/2020/12/17/build-and-secure-an-api-in-python-with-fastapi"&gt;https://developer.okta.com/blog/2020/12/17/build-and-secure-an-api-in-python-with-fastapi&lt;/a&gt; thanks to &lt;a href="https://developer.okta.com/blog/authors/karl-hughes/"&gt;Karl Hughes&lt;/a&gt; for this article.&lt;/p&gt;

&lt;p&gt;Litestar framework for Python 3.x creating APIs.&lt;br&gt;
For more information about Litestar visit &lt;a href="https://litestar.dev/"&gt;https://litestar.dev/&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  OKTA Setup
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Step 1
&lt;/h3&gt;

&lt;p&gt;After creating a developer account with okta click 'Applications' then 'Create App Integration' as shown in the image&lt;br&gt;
&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5n3pswjhkulldplf9f5t.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5n3pswjhkulldplf9f5t.png" alt="Image description" width="790" height="334"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 2
&lt;/h3&gt;

&lt;p&gt;Choose 'API Services' with a description of&lt;br&gt;
'Interact with Okta APIs using the scoped OAuth 2.0 access tokens for machine-to-machine authentication.'&lt;br&gt;
&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvchz84nydn97hs2qcqbt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvchz84nydn97hs2qcqbt.png" alt="Image description" width="800" height="463"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 3
&lt;/h3&gt;

&lt;p&gt;Give the integration a name&lt;br&gt;
&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu47f1u2gbkf5g3w55e5i.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu47f1u2gbkf5g3w55e5i.png" alt="Image description" width="800" height="233"&gt;&lt;/a&gt;Result Screen&lt;br&gt;
&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsjdcun93vr6utbvggl65.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsjdcun93vr6utbvggl65.png" alt="Image description" width="765" height="632"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 4
&lt;/h3&gt;

&lt;p&gt;In the directory for the Python project create a file named .env with the following:&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;&lt;code&gt;OKTA_CLIENT_ID=Value From The Previous Step&lt;br&gt;
OKTA_CLIENT_SECRET=Value From The Previous Step&lt;br&gt;
OKTA_ISSUER="{oktaDomain}/oauth2/default"&lt;br&gt;
OKTA_TOKEN="{oktaDomain}/oauth2/default/v1/token"&lt;br&gt;
OKTA_INTROSPECTION="{oktaDomain}/oauth2/default/v1/introspect"&lt;br&gt;
OKTA_AUDIENCE="api://default"&lt;br&gt;
OKTA_SCOPE="items"&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5
&lt;/h3&gt;

&lt;p&gt;Now go to 'Security' -&amp;gt; 'API' shown below:&lt;br&gt;
&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fts27wqi0ndy1r9nytsau.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fts27wqi0ndy1r9nytsau.png" alt="Image description" width="800" height="529"&gt;&lt;/a&gt;&lt;br&gt;
Note that values for Audience and Issuer URI are in .env file.&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 6
&lt;/h3&gt;

&lt;p&gt;Click on the edit icon you should see something like this:&lt;br&gt;
&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh66p8dilmrnj0f12y94u.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh66p8dilmrnj0f12y94u.png" alt="Image description" width="800" height="567"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 7
&lt;/h3&gt;

&lt;p&gt;Try the 'Metadata URI' in a new tab in you web browser to see something like:&lt;br&gt;
&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3zzqizbwsayqfcwdjd50.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3zzqizbwsayqfcwdjd50.png" alt="Image description" width="746" height="349"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 8
&lt;/h3&gt;

&lt;p&gt;Click 'Scopes' -&amp;gt; 'Add Scope'&lt;br&gt;
&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flr2kmr2njvxrx1sc3rgz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flr2kmr2njvxrx1sc3rgz.png" alt="Image description" width="800" height="227"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 9
&lt;/h3&gt;

&lt;p&gt;Add a scope such as what is shown below:&lt;br&gt;
&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fofusyayh5tdaunblyt42.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fofusyayh5tdaunblyt42.png" alt="Image description" width="728" height="742"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 10
&lt;/h3&gt;

&lt;p&gt;Complete the .env file with the correct information including replacing &lt;code&gt;{oktaDomain}&lt;/code&gt; with your okta domain and having the correct value for &lt;code&gt;OKTA_SCOPE="items"&lt;/code&gt;, if you changed yours that is and note that OKTA_SCOPE is important for remote validation.&lt;/p&gt;
&lt;h2&gt;
  
  
  Python Code
&lt;/h2&gt;

&lt;p&gt;You can find the final code for this here &lt;a href="https://github.com/pbaletkeman/litestarOKTA"&gt;https://github.com/pbaletkeman/litestarOKTA&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Large segments of this code came from &lt;a href="https://docs.litestar.dev/latest/usage/security/jwt.html"&gt;https://docs.litestar.dev/latest/usage/security/jwt.html&lt;/a&gt;.&lt;br&gt;
To learn more about Litestar security go here &lt;a href="https://docs.litestar.dev/latest/usage/security/index.html"&gt;https://docs.litestar.dev/latest/usage/security/index.html&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Download the files to your local system and copy them to your Python project directory.&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 11
&lt;/h3&gt;

&lt;p&gt;Install the required Python libraries using&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;&lt;code&gt;pip install -r requirements.txt&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;Note, that this includes all of litestar[full] which may be more than you need.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 12
&lt;/h3&gt;

&lt;p&gt;Libraries of note:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;base64&lt;/code&gt; is used to encode your &lt;code&gt;client id&lt;/code&gt; and &lt;code&gt;client secret&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;httpx&lt;/code&gt; is used to connect to the OKTA server and return the JWT token&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;okta_jwt.jwt&lt;/code&gt; is used to validate the JWT token locally, which is quicker, but less secure than JWT token validation on the OKTA server&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;starlette.config&lt;/code&gt; is used to import the .env file into your application, you may want to use &lt;code&gt;python-dotenv&lt;/code&gt; instead&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 13
&lt;/h3&gt;

&lt;p&gt;In this sample project we only ever will have one user/account which means that we can do this&lt;br&gt;
&lt;br&gt;
 &lt;code&gt;API_USER_DB: dict[str, OAuthSchema] = {}&lt;/code&gt;&lt;br&gt;
&lt;br&gt;
 but if you have more than one account you should rethink how your database of users is stored.&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 14
&lt;/h3&gt;

&lt;p&gt;This project includes a method for local JWT token validation, but it is not used, the remote JWT token validate is used instead.&lt;br&gt;
Remote validation is more secure, takes longer and increases system resource usage as this makes a call to the &lt;a href="https://developer.okta.com/docs/reference/api/oidc/#introspect"&gt;Introspection&lt;/a&gt; endpoint.&lt;br&gt;
The local validation method is &lt;code&gt;def validate(token: str) -&amp;gt; bool:&lt;/code&gt; and the remote validation method is &lt;code&gt;def validate_remote(token: str) -&amp;gt; bool:&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Notes
&lt;/h2&gt;
&lt;h4&gt;
  
  
  Note 1
&lt;/h4&gt;

&lt;p&gt;When running this project you should have the following endpoints:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;/login

&lt;ul&gt;
&lt;li&gt;uses Authorization header to login&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;/form-login

&lt;ul&gt;
&lt;li&gt;uses data from HTML Form to login&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;/json-login

&lt;ul&gt;
&lt;li&gt;uses data from JSON body to login&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;as shown below:&lt;br&gt;
&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgujinm6u6o9mx95a8vju.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgujinm6u6o9mx95a8vju.png" alt="Image description" width="496" height="297"&gt;&lt;/a&gt;&lt;br&gt;
Each endpoint has it's purpose and it's up to you to choose the appropriate one.&lt;/p&gt;
&lt;h4&gt;
  
  
  Note 2
&lt;/h4&gt;

&lt;p&gt;In &lt;code&gt;json_login_handler&lt;/code&gt; and &lt;code&gt;form_login_handler&lt;/code&gt; there is some funky magic happening here:&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;&lt;code&gt;auth_header = 'Basic ' + str(base64.b64encode((data.client_id + ':' + data.client_secret).encode('ascii')))[2:-1]&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;To get the token we must send a authorization header of 'basic' a base 64 encoded version of the username:password. However Python prepends the letter "b" and then surrounds the value with a single quote which is why we are using [2:-1]&lt;/p&gt;

</description>
      <category>tutorial</category>
      <category>python</category>
      <category>api</category>
      <category>security</category>
    </item>
  </channel>
</rss>
