About AITesterBlueprint4x
AITesterBlueprint4x is an MCP server published by PramodDutta in the RAG category: learning about AI skills, prompt engineering, RAG, MCP, Lank Flow, Lanksmith, n8n, and deepeval and RAG so.
The repository has 14 stars and 22 forks, with the last commit 11 days ago. Recent commit activity is the strongest signal that reported issues will get attention.
Install
git clone https://github.com/PramodDutta/AITesterBlueprint4xThis server has no ConduID identity, so agent calls to it are not receipted. Pin the version you install and review the source before granting it credentials.
Ask AI
Ask AI about AITesterBlueprint4x
Powered by Claude · Grounded in docs
Security checks
Static checks derived from the repository 2 days ago. Heuristics on the source, not an audit.
- ✓README presentThe repository ships documentation.
- !License declaredNo license file. Legally you have no right to use it in a product.
- ✓Tests presentA test suite exists in the repository.
- ✓Dependencies pinnedA lockfile pins dependency versions.
- ✓No dynamic code executionSource scan found no eval, dynamic Function, or shell execution.
1 check has not been run yet.
README
AITester Blueprint 4x
AI-powered test automation blueprint.
Contents
- Overview
- Chapters
- Chapter 01: LLM Basics
- Chapter 02: Prompt Engineering
- Chapter 03: Local Test Case Generator
- Chapter 04: JobKit AI
- Chapter 05: Job Tracker AI
- Chapter 06: Branding and LinkedIn Skills
- Chapter 07: AI Agents
- Chapter 08: n8n Agents
- The Jira agent pair (01, 02)
- Swapping the brain: local Ollama and Groq (03)
- Bug triage into Google Sheets (04)
- Production bug RCA pipeline (05)
- Social media content chain (06)
- Scheduled post generator with human approval (07)
- Screenshot to bug reporter (08)
- Screenshot to bug reporter UI (09)
- Cross-chapter takeaway
- Chapter 09: LangFlow
- Chapter 10: RAG Basics
- Chapter 11: RAG Implementation
- Chapter 12: QABuddy, Hybrid RAG for QA
- Chapter 13: MCP Basics
- Chapter 14: MCP Servers by Vibe Coding
- Chapter 15: Python and PyTest
- Chapters 16-21: coming next
- License
Overview
AITester Blueprint 4x Where we will learn about a lot of things related to:
- AI Agent
- MCPs
- RAG
- LLM evaluations
- Langchain
- Langflow
- ATAN
- and many more things which will make us the AI-powered tester
Chapters
Chapter 01: LLM Basics
Foundation concepts of Large Language Models and AI fundamentals.
Chapter 02: Prompt Engineering
RICE-POT template framework for structured prompt engineering, a Salesforce Selenium test framework built with the Page Object Model pattern, and a 54-skill QA prompt suite.
The 54-skill QA prompt suite
Every skill is a folder holding a SKILL.md with YAML frontmatter, so it drops straight into
~/.claude/skills/ or ~/.codex/skills/ and is invoked by name. 36 skills come from
skillmasterclass
and are hardened here for credential handling, execution authorization, network isolation, and
evidence redaction. 18 are new: API testing, AI safety and guardrails, and test deliverables.
flowchart LR
S["QA Prompt Skill Suite<br/>54 skills"]
S --> U["36 upstream<br/>hardened here"]
S --> N["18 new<br/>in this repo"]
U --> A["stlc/ · 14"]
U --> B["playwright/ · 11"]
U --> C["selenium/ · 11"]
N --> D["api_testing/ · 6"]
N --> E["safety_guardrails/ · 6"]
N --> F["test_deliverables/ · 6"]
classDef root fill:#57606a,stroke:#24292f,color:#fff
classDef core fill:#1f6feb,stroke:#0b3d91,color:#fff
classDef ext fill:#2da44e,stroke:#0f5323,color:#fff
class S,U,N root
class A,B,C core
class D,E,F ext
Where each group plugs into the lifecycle:
flowchart LR
P1["01<br/>Requirement<br/>Analysis"] --> P2["02<br/>Test<br/>Planning"] --> P3["03<br/>Test<br/>Design"] --> P4["04<br/>Test Case<br/>Development"] --> P5["05<br/>Test<br/>Execution"] --> P6["06<br/>Defect<br/>Management"] --> P7["07<br/>Test<br/>Closure"] --> GATE{{"Human<br/>review gate"}}
P3 -.-> API["api_testing/<br/>6 skills"]
P3 -.-> SAFE["safety_guardrails/<br/>6 skills"]
P4 -.-> PW["playwright/<br/>11 skills"]
P4 -.-> SE["selenium/<br/>11 skills"]
API -.-> P5
PW -.-> P5
SE -.-> P5
SAFE -.-> P6
P7 -.-> DEL["test_deliverables/<br/>6 skills"]
DEL -.-> GATE
classDef core fill:#1f6feb,stroke:#0b3d91,color:#fff
classDef pack fill:#8250df,stroke:#4a1f8f,color:#fff
classDef ext fill:#2da44e,stroke:#0f5323,color:#fff
classDef gate fill:#bf8700,stroke:#7a5600,color:#fff
class P1,P2,P3,P4,P5,P6,P7 core
class PW,SE pack
class API,SAFE,DEL ext
class GATE gate
Nothing in the suite is self-approving. Skills report only what the supplied evidence supports, mark anything unverified as unknown rather than assuming it passed, and stop at a human review gate before a plan, case set, defect, report, or release decision counts as final.
| Skill | What it does |
|---|---|
jira-requirement-analyzer |
Score a JIRA ticket against a readiness checklist, return gaps, ambiguities, risks, and clarifying questions |
test-plan-generator |
Fetch a ticket and fill the standard test plan template, stopping for human review |
test-scenario-designer |
Derive positive, negative, boundary, and cross-role scenarios traced to each acceptance criterion |
api-test-designer |
Map endpoint or contract coverage across happy path, schema, auth, negative, boundary, and idempotency |
test-case-writer |
Expand approved scenarios into preconditions, ordered steps, expected results, and required data |
test-data-generator |
Produce labeled valid, invalid, boundary, and synthetic data sets per field or scenario |
automation-script-generator |
Framework-neutral handoff for an approved case, plus a Playwright versus Selenium stack decision |
test-execution-tracker |
Record pass, fail, blocked, or not-run per case with evidence, then roll up cycle progress |
regression-suite-selector |
Map a change to the tests that exercise it and rank a risk-based run set |
bug-reporter |
Turn an observed failure into a structured, reproducible defect report |
bug-triage-assistant |
Group likely duplicates, propose severity and priority, and route a defect backlog |
rca-analyzer |
Run 5-Whys plus fishbone to separate root cause from symptom and propose corrective actions |
test-coverage-analyzer |
Build a traceability view and surface untested ACs, thin areas, and orphan tests |
test-closure-reporter |
Roll cycle metrics into highlights, risks, and an advisory go or no-go |
| Skill | What it does |
|---|---|
pw-test-generator |
Generate a TypeScript spec from an approved, detailed test case |
pw-page-object-builder |
Build a POM class with getByRole / getByTestId locators, action methods, and a static PATH |
pw-locator-fixer |
Rewrite brittle XPath, CSS class, and nth-child selectors into resilient ones with a before and after map |
pw-fixture-designer |
Design typed auth, seeded-data, and page-object fixtures with correct scope and teardown |
pw-network-mocker |
Stub responses and error states via page.route to remove backend flakiness |
pw-api-tester |
Cover an endpoint through the request context: happy path, schema, auth, negative, boundary |
pw-visual-regression |
Set up toHaveScreenshot with masking, thresholds, and a baseline strategy |
pw-accessibility-auditor |
Wire @axe-core/playwright checks and triage violations against a supplied policy |
pw-trace-analyzer |
Read a trace.zip timeline to isolate the failing action and recommend the fix |
pw-flaky-debugger |
Root-cause races, hard waits, and shared state, then propose deterministic fixes |
pw-ci-configurator |
Generate a GitHub Actions workflow with sharding, blob and HTML reports, and artifacts |
| Skill | What it does |
|---|---|
se-test-generator |
Generate a Selenium 4 plus TestNG Java test from an approved case using explicit waits |
se-page-object-builder |
Build a Java POM class with @FindBy or explicit locators plus WebDriverWait |
se-locator-strategist |
Replace brittle absolute XPath with By.id, CSS, or Selenium 4 relative locators |
se-wait-fixer |
Remove Thread.sleep and implicit-explicit wait mixing in favor of WebDriverWait or FluentWait |
se-driver-manager |
Set up Selenium Manager or WebDriverManager with browser options and a thread-safe factory |
se-framework-scaffolder |
Scaffold a Maven plus TestNG project: base test, POM package, config loader, logging, reporting |
se-data-driven-designer |
Design @DataProvider or Excel, CSV, JSON driven tests across valid, invalid, and boundary sets |
se-cross-browser-runner |
Parameterize Chrome, Firefox, and Edge execution via testng.xml or a factory |
se-grid-configurator |
Configure Selenium Grid 4 hub and node or Docker, and wire RemoteWebDriver for parallel runs |
se-report-integrator |
Integrate Allure or ExtentReports with listeners, screenshot on failure, and step logging |
se-flaky-debugger |
Diagnose StaleElementReferenceException and timing races, plus a finite rerun plan |
| Skill | What it does |
|---|---|
api-contract-validator |
Compare observed requests and responses with an OpenAPI contract and report drift |
api-collection-builder |
Turn approved cases into a runnable Postman/Newman or Bruno collection |
api-workflow-tester |
Design stateful multi-call flows across lifecycles, async jobs, events, and callbacks |
api-authorization-boundary-tester |
Build a policy-backed access matrix for BOLA and IDOR, tenant isolation, and privilege boundaries |
api-resilience-tester |
Plan bounded fault tests for timeouts, retries, 429 throttling, duplicates, and recovery |
api-performance-test-planner |
Draft workloads, thresholds, and abort conditions with an optional k6 or JMeter skeleton |
| Skill | What it does |
|---|---|
ai-threat-modeler |
Map assets, trust boundaries, memory, retrieval, tools, abuse cases, and residual risk |
prompt-injection-resilience-tester |
Cover direct and indirect injection, instruction hierarchy, obfuscation, and canary exfiltration |
sensitive-data-leakage-tester |
Probe prompts, context, retrieval, caches, logs, and retention with synthetic canaries |
ai-agent-tool-safety-tester |
Verify tool allowlists, least privilege, approval gates, dry runs, idempotency, and rollback |
content-safety-guardrail-evaluator |
Measure unsafe acceptance, appropriate refusal, over-refusal, and multi-turn policy coverage |
ai-fairness-bias-evaluator |
Run subgroup, paired counterfactual, and intersectional fairness tests on synthetic or consented data |
| Skill | What it does |
|---|---|
review-test-deliverables |
Quality-check a QA artifact and return source-located findings before peer review |
maintain-test-traceability |
Maintain a versioned RTM from requirements through cases, runs, defects, and evidence |
draft-test-status-brief |
Draft an as-of daily or weekly QA status from approved execution and risk snapshots |
assemble-release-decision-record |
Map evidence to exit criteria and record the gate decision, waivers, and conditions |
curate-test-evidence-bundle |
Inventory, hash, and version existing evidence into a shareable manifest |
prepare-qa-audit-handoff |
Index deliverables against an audit control list with custodians and chain of custody |
Install a skill
skill_source=chapter_02_Prompt_Eng/prompt_templates/api_testing/api-contract-validator
skill_destination="$HOME/.codex/skills/api-contract-validator"
if [ -e "$skill_destination" ]; then
echo "Skill already exists; compare and back it up before an explicitly approved update."
exit 1
fi
mkdir -p "$(dirname "$skill_destination")"
cp -R "$skill_source" "$skill_destination"
Then invoke it by name, such as $api-contract-validator. The 18 new skills also ship an
agents/openai.yaml so Codex picks up their metadata.
Chapter 03: Local Test Case Generator
A two-screen Streamlit application that generates test cases from Jira tickets using a local LLM with cloud fallback.
Features
- Chat-style interface: type a Jira ticket key and get structured test cases
- Fetches ticket details (summary, description, acceptance criteria) from Jira REST API
- Generates test cases using Ollama (
gemma3:1bonlocalhost:11434) by default - Automatic fallback to Groq cloud API when Ollama is unavailable
- Settings page to configure Jira credentials, LLM provider, and Groq API key
- Credentials persisted via
.env(seed) andconfig.json(runtime store) - Anti-hallucination prompt template with strict formatting rules
File structure
chapter_03_Local_TC_Generator/
├── src/
│ ├── app.py # Main Streamlit chat screen
│ ├── pages/
│ │ └── settings.py # Settings screen (Jira + LLM config)
│ ├── config_store.py # Settings persistence (.env -> config.json)
│ ├── jira_client.py # Jira REST API wrapper
│ ├── llm_client.py # Ollama + Groq orchestrator with fallback
│ ├── requirements.txt # Python dependencies
│ ├── .env # Credentials (git-ignored)
│ └── config.json # Runtime settings (git-ignored)
├── templates/
│ └── testcase_creator.md # Test case generation template
└── src/
├── Finetune_Prompt.md # Original design prompt
├── Prompt.md # Project requirements
└── plan.md # Implementation plan
Running the app
cd chapter_03_Local_TC_Generator/src
pip install -r requirements.txt
streamlit run app.py
Environment variables
Seed these in chapter_03_Local_TC_Generator/src/.env, which is git-ignored:
JIRA_URL=https://your-org.atlassian.net
JIRA_EMAIL=you@example.com
JIRA_API_TOKEN=your-jira-api-token
GROQ_API_KEY=your-groq-api-key
OLLAMA_MODEL=gemma3:1b
Data flow
User types "create test cases for JIRA-102"
-> app.py parses JIRA-102 via regex
-> jira_client.py fetches ticket from Jira REST API
-> templates/testcase_creator.md loaded and merged with ticket content
-> llm_client.py tries Ollama first, falls back to Groq
-> Test cases rendered as a formatted markdown table in chat
Chapter 04: JobKit AI
A resume tailoring toolkit that turns raw LinkedIn job exports into tailored,
per-role resumes. The resume-tailor skill reads a job description, produces a
structured spec (.specs/*.json) with highlighted skills, fit-gap notes, and
section-by-section content, then renders a polished .docx into output/.
Q&A - why use this?
- Q: When do I reach for it? A: When applying to a specific role and you want the resume keywords, ordering, and emphasis matched to that posting instead of sending one generic CV.
- Q: What does it replace? A: Manual copy-paste tailoring in Word, and the guesswork of which buzzwords a posting actually screens for.
- Q: What's the gotcha? A: The spec flags fit gaps (missing years, unverified claims) rather than papering over them. Resolve every
[confirm ...]marker before sending anything out.
flowchart LR
CSV["linkedin_jobs.csv<br/>LinkedIn export"] --> JD["Pick a job<br/>description"]
JD --> SKILL["resume-tailor<br/>skill"]
SKILL --> SPEC[".specs/job.json<br/>structured spec +<br/>fit gaps"]
SPEC --> REVIEW{"Human<br/>review"}
REVIEW --> DOCX["output/*.docx<br/>tailored resume"]
classDef src fill:#57606a,stroke:#24292f,color:#fff
classDef ai fill:#1f6feb,stroke:#0b3d91,color:#fff
classDef gate fill:#bf8700,stroke:#7a5600,color:#fff
classDef out fill:#2da44e,stroke:#0f5323,color:#fff
class CSV,JD src
class SKILL,SPEC ai
class REVIEW gate
class DOCX out
The repo ships two worked examples: specs and rendered docx files for an Accenture Test Automation Lead and a Mouser Electronics Software QA Team Lead.
Chapter 05: Job Tracker AI
A local-first kanban board for tracking job applications. Six columns from Wishlist to Rejected, drag-and-drop between stages, all data stored in the browser's IndexedDB so nothing leaves the machine.
Features
- Kanban board with six status columns: Wishlist, Applied, Follow-up, Interview, Offer, Rejected
- Drag-and-drop between columns via dnd-kit, with pointer and keyboard sensors
- Add/edit modal with company, role, LinkedIn URL, resume used, salary range, date applied, status, and notes
- Resume name autocomplete from previously used resumes
- Search by company or role, sort by newest or oldest applied date
- Header metrics: total jobs, interviews, offers
- JSON backup export and import (import replaces existing data after confirmation)
- Light/dark theme toggle persisted in localStorage
- Delete confirmation dialog, toast notifications, Escape-to-close modals
Tech stack
| Layer | Choice |
|---|---|
| UI | React 18 + Vite |
| Styling | Tailwind CSS 3 (darkMode: 'class') |
| Drag and drop | @dnd-kit/core + @dnd-kit/sortable |
| Persistence | IndexedDB via idb |
| Icons | lucide-react |
Data model
Each job is one record in the jobs object store of the job-tracker-ai
IndexedDB database, keyed by id, indexed by status and dateApplied:
{
id: 'uuid',
company: 'Accenture',
role: 'Test Automation Lead',
linkedInUrl: 'https://linkedin.com/jobs/...',
resumeUsed: 'QA_Lead_Resume',
dateApplied: '2026-08-23',
salaryRange: '',
notes: '',
status: 'applied', // wishlist | applied | follow-up | interview | offer | rejected
createdAt: '...',
updatedAt: '...',
}
Running the app
cd chapter_05_JobTrackerAI
npm install
npm run dev # http://localhost:5173
State flow
flowchart LR
LOAD["Mount<br/>getAllJobs()"] --> BOARD["Kanban board<br/>grouped by status"]
BOARD -->|"drag card"| DND["handleDragEnd<br/>saveJob(updated)"]
BOARD -->|"add / edit"| MODAL["JobModal<br/>validated form"]
MODAL --> SAVE["saveJob()"]
BOARD -->|"delete"| CONFIRM["ConfirmDelete"] --> DEL["removeJob()"]
BOARD --> EXPORT["Export JSON backup"]
EXPORT -.-> IMPORT["Import replaces<br/>all jobs"] --> REPLACE["replaceAllJobs()"]
DND --> BOARD
SAVE --> BOARD
DEL --> BOARD
REPLACE --> BOARD
classDef ui fill:#1f6feb,stroke:#0b3d91,color:#fff
classDef store fill:#8250df,stroke:#4a1f8f,color:#fff
classDef io fill:#2da44e,stroke:#0f5323,color:#fff
class LOAD,BOARD,MODAL,CONFIRM ui
class DND,SAVE,DEL,REPLACE store
class EXPORT,IMPORT io
Q&A - why use this?
- Q: When do I reach for it? A: Any active job hunt where you are juggling more than a handful of applications across stages.
- Q: What does it replace? A: The spreadsheet. Status changes become a drag instead of a cell edit, and search plus metrics come free.
- Q: What's the gotcha? A: Data lives only in that browser's IndexedDB. Use Export before clearing site data or switching machines; import replaces everything.
Chapter 06: Branding and LinkedIn Skills
A Claude/Codex skill that turns any content seed into a publish-ready pack in The Testing Academy voice. Drop in a title, rough bullets, a screenshot, a URL, a thread, or a spoken dump. The skill returns a Medium article, a LinkedIn post, LinkedIn image prompts, and a Medium cover prompt.
The voice spec, deliverable formats, and a full worked example live in chapter_06_Branding_LinkedinSkills/README.md.
What it produces
| File | Job |
|---|---|
pack-1-medium-article.md |
2,500 to 3,200 word Medium draft |
pack-2-linkedin-post.md |
Hook ladder, 220 to 260 word post, first-comment blocks |
pack-3-linkedin-image-prompts.md |
Style C v2 tweet-screenshot cards |
pack-4-medium-image-prompt.md |
Style A 16:9 cyber infographic cover |
flowchart LR
subgraph IN["Seed"]
T["Title"]
B["Bullets"]
U["URL"]
D["Dump"]
end
SKILL["content-repurpose-pack"]
subgraph OUT["Pack"]
P1["Medium<br/>article"]
P2["LinkedIn<br/>post"]
P3["LinkedIn<br/>cards"]
P4["Medium<br/>cover"]
end
T --> SKILL
B --> SKILL
U --> SKILL
D --> SKILL
SKILL --> P1 --> PUB{"Human<br/>publish"}
SKILL --> P2 --> PUB
SKILL --> P3 --> PUB
SKILL --> P4 --> PUB
classDef src fill:#57606a,stroke:#24292f,color:#fff
classDef ai fill:#1f6feb,stroke:#0b3d91,color:#fff
classDef out fill:#2da44e,stroke:#0f5323,color:#fff
classDef gate fill:#bf8700,stroke:#7a5600,color:#fff
class T,B,U,D src
class SKILL ai
class P1,P2,P3,P4 out
class PUB gate
File structure
chapter_06_Branding_LinkedinSkills/
├── files.zip # original archive
├── content-repurpose-pack.skill # packaged bundle
└── content-repurpose-pack/
├── SKILL.md # pipeline and hook protocol
└── references/
├── brand-voice.md # Hook / Story / Offer, 13 threads
├── deliverable-specs.md # exact formats for the four files
└── worked-example.md # one full input-to-output pass
Install the skill
skill_source=chapter_06_Branding_LinkedinSkills/content-repurpose-pack
skill_destination="$HOME/.claude/skills/content-repurpose-pack"
if [ -e "$skill_destination" ]; then
echo "Skill already exists; compare and back it up before an explicitly approved update."
exit 1
fi
mkdir -p "$(dirname "$skill_destination")"
cp -R "$skill_source" "$skill_destination"
For Codex, use $HOME/.codex/skills/content-repurpose-pack. Then invoke it by
name, such as $content-repurpose-pack.
Pack pipeline
flowchart LR
SEED["Any seed<br/>title, bullets,<br/>URL, dump"] --> SHAPE["Identify shape<br/>reconcile the<br/>promised number"]
SHAPE --> MINE["Mine five things<br/>thesis, framework,<br/>assets, numbers"]
MINE --> VOICE["Map to voice<br/>Hook / Story / Offer"]
VOICE --> PACK["Four pack files"]
PACK --> SWEEP["Grep sweep<br/>chai, BFSI, sprint tests"]
SWEEP --> PUBLISH{"Human<br/>publish"}
classDef src fill:#57606a,stroke:#24292f,color:#fff
classDef ai fill:#1f6feb,stroke:#0b3d91,color:#fff
classDef gate fill:#bf8700,stroke:#7a5600,color:#fff
classDef out fill:#2da44e,stroke:#0f5323,color:#fff
class SEED src
class SHAPE,MINE,VOICE,SWEEP ai
class PACK out
class PUBLISH gate
Voice spine
Same three beats on LinkedIn and Medium. Only the word budget changes. The usual miss is a great Hook and Story with no Offer.
flowchart LR
HOOK["HOOK<br/>earn the expand"] --> STORY["STORY<br/>one real receipt"]
STORY --> OFFER["OFFER<br/>so what do I do"]
classDef ai fill:#1f6feb,stroke:#0b3d91,color:#fff
classDef pack fill:#8250df,stroke:#4a1f8f,color:#fff
classDef out fill:#2da44e,stroke:#0f5323,color:#fff
class HOOK ai
class STORY pack
class OFFER out
flowchart TB
L1["Hook: 2 lines"] --> L2["Receipt with an undeniable detail"]
L2 --> L3["Two-beat punch, under 8 words"]
L3 --> L4["Steelman, then pivot"]
L4 --> L5["X is not Y. X is Z."]
L5 --> L6["One-tier offer"]
L6 --> L7["Hashtags"]
classDef ai fill:#1f6feb,stroke:#0b3d91,color:#fff
classDef pack fill:#8250df,stroke:#4a1f8f,color:#fff
classDef out fill:#2da44e,stroke:#0f5323,color:#fff
class L1,L2 ai
class L3,L4,L5 pack
class L6,L7 out
Offer rotation: four posts on a belief or a question, then one free asset, and a product link only for real launches.
flowchart LR
T1["Tier 1<br/>Belief"] --> T2["Tier 2<br/>Question"]
T2 --> T3["Tier 3<br/>Asset"]
T3 --> T4["Tier 4<br/>Product"]
classDef out fill:#2da44e,stroke:#0f5323,color:#fff
classDef ai fill:#1f6feb,stroke:#0b3d91,color:#fff
classDef pack fill:#8250df,stroke:#4a1f8f,color:#fff
class T1,T2 out
class T3 ai
class T4 pack
Hook ladder
When the ask is controversial, the skill returns three labelled variants instead of sanitizing or refusing.
flowchart TB
ASK["Controversial hooks requested"] --> A["A. Prediction<br/>forecast, not fake data"]
ASK --> B["B. Threat<br/>highest reach"]
ASK --> C["C. Receipt<br/>a story, not a claim"]
A --> LI["LinkedIn: A or C"]
C --> LI
B --> X["X: B often belongs here"]
C --> REC["Default on LinkedIn"]
classDef src fill:#57606a,stroke:#24292f,color:#fff
classDef ai fill:#1f6feb,stroke:#0b3d91,color:#fff
classDef gate fill:#bf8700,stroke:#7a5600,color:#fff
classDef out fill:#2da44e,stroke:#0f5323,color:#fff
classDef pack fill:#8250df,stroke:#4a1f8f,color:#fff
class ASK src
class A ai
class B gate
class C,REC out
class LI,X pack
Q&A - why use this?
- Q: When do I reach for it? A: When a seed needs to become a Medium + LinkedIn + image pack in this voice, not a generic rewrite.
- Q: What does it replace? A: Re-prompting from scratch for length, banned phrases, first-comment splits, and image specs.
- Q: What's the gotcha? A: A pack with no receipt is the biggest quality drop. Supply a real story, or accept a composite flagged for anonymization. No em dashes, anywhere.
Chapter 07: AI Agents
A Test Plan Agent: give it a Jira ID, get back a formal 14-section test plan where every claim traces to a real field on the ticket. Built with the B.L.A.S.T. protocol on the A.N.T. 3-layer architecture.
Why: the naive version (paste a ticket into a chat window, ask for a test plan) hallucinates acceptance criteria, produces a different shape every run, and gives you no way to tell which step went wrong when the output is bad.
Q&A - why use this?
- Q: When do I reach for it? A: When a story needs a review-ready test plan and you need every scope decision defensible to a reviewer.
- Q: What does it replace? A: Hand-writing the same 14 sections per ticket, and the re-prompting loop when a chat model drifts from your template.
- Q: What's the gotcha? A: It will refuse a thin ticket rather than invent one, returning a gap report instead. That is the design, not a bug: 2 of 4 real tickets were refused in testing.
The B.L.A.S.T. protocol
Five phases with hard gates. Nothing enters tools/ until Discovery Questions are
answered, the data schema is frozen, and the blueprint is approved.
flowchart LR
P0["Protocol 0<br/>Initialize memory"] --> GATE{{"Gates A/B/C<br/>halt until green"}}
GATE --> B["B - Blueprint<br/>5 discovery questions<br/>freeze the schema"]
B --> L["L - Link<br/>prove every connection<br/>before any logic"]
L --> A["A - Architect<br/>SOPs first, then tools"]
A --> S["S - Stylize<br/>frozen template<br/>output gates"]
S --> T["T - Trigger<br/>UI + CLI"]
classDef src fill:#57606a,stroke:#24292f,color:#fff
classDef gate fill:#bf8700,stroke:#7a5600,color:#fff
classDef ai fill:#1f6feb,stroke:#0b3d91,color:#fff
classDef out fill:#2da44e,stroke:#0f5323,color:#fff
class P0 src
class GATE gate
class B,L,A ai
class S,T out
Four memory files carry the project state: task_plan.md (phases and checklists),
findings.md (research and constraints), progress.md (a timestamped log of what was
done, what errored, what the result was), and LLM.md (the constitution: schemas,
18 behavioural rules, 10 architectural invariants).
A.N.T. 3-layer architecture
The premise: LLMs are probabilistic, business logic must be deterministic. So the pipeline is seven steps and exactly one of them calls a model.
flowchart TB
subgraph L1["Layer 1 - Architecture (architecture/)"]
SOP["6 markdown SOPs<br/>Golden Rule: SOP changes before code"]
end
subgraph L2["Layer 2 - Navigation (navigation.py)"]
NAV["Routes tools, owns failure branches<br/>parses intent with a regex, not an LLM"]
end
subgraph L3["Layer 3 - Tools (tools/)"]
T1["jira_fetch · field_map<br/>adf_flatten · normalize"]
T2["readiness · render · trace<br/>6 of 9 are pure functions"]
T3["plan_build<br/>THE ONLY probabilistic step"]
end
SOP --> NAV --> T1 --> T2
NAV --> T3
classDef src fill:#57606a,stroke:#24292f,color:#fff
classDef ai fill:#1f6feb,stroke:#0b3d91,color:#fff
classDef gate fill:#bf8700,stroke:#7a5600,color:#fff
classDef out fill:#2da44e,stroke:#0f5323,color:#fff
class SOP src
class NAV ai
class T1,T2 out
class T3 gate
The pipeline, end to end:
flowchart LR
P["prompt<br/>'plan SOAP-1'"] --> K["parse key<br/>regex"]
K --> F["fetch<br/>REST v3"]
F --> N["normalize<br/>ADF to markdown"]
N --> R{{"readiness<br/>score >= 5/11?"}}
R -->|no| GAP["gap report<br/>NO plan written"]
R -->|yes| LLM["one LLM call<br/>returns JSON"]
LLM --> V{{"schema gate<br/>2 retries max"}}
V --> RND["render<br/>frozen template"]
RND --> MD["plan.md + trace.json"]
classDef src fill:#57606a,stroke:#24292f,color:#fff
classDef ai fill:#1f6feb,stroke:#0b3d91,color:#fff
classDef gate fill:#bf8700,stroke:#7a5600,color:#fff
classDef out fill:#2da44e,stroke:#0f5323,color:#fff
class P,K,F,N src
class LLM ai
class R,V gate
class RND,MD,GAP out
Running the agent
cd chapter_07_AI_Agents/Test-Plan-Agent-Blast
pip install -r requirements.txt
cp .env.example .env # or use the Settings page
streamlit run app.py # UI on :8501
Open Settings first: add the Jira URL, email and API token plus an LLM key (DeepSeek or Groq, both OpenAI-compatible), then press both Test connection buttons. The CLI mirrors the UI:
python run.py SOAP-1 # generate a plan
python run.py "make a plan for VWO-49" # natural language, same result
python run.py --health # prove both connections
python run.py --dry-run SOAP-1 # fetch + normalize, no LLM call
Exit codes: 0 ok, 2 bad input, 3 auth, 4 not found, 5 rate limited,
6 schema violation, 7 LLM failure.
Anti-hallucination design
The model returns JSON, not markdown. render.py owns the format, so the model
never sees the template and cannot drift from it. Two schema fields do the real work:
{
"scope": [{
"type": "Error Handling",
"rationale": "Assert the 400 response names the offending field.",
"justified_by": "FR-03: the system must reject requests missing sISBN."
}],
"assumptions": [{
"field": "environment.url",
"assumed_value": "QA environment (URL not specified in ticket)",
"why": "Ticket does not provide a concrete environment URL"
}]
}
justified_by is required on every scope entry and must name the ticket fact that put
it there. A model that cannot fill it has just admitted the entry is padding, and the
schema rejects the response. That turns "please do not hallucinate" from a hopeful
instruction into a structural constraint. Anything filled without ticket evidence lands
in assumptions[] and renders into the deliverable, not into a debug log.
Field notes from the build
Three findings that only surfaced by running it against a live Jira and a live model:
| Finding | Why it matters |
|---|---|
Jira answers 404, not 401, on /issue/{key} when auth is bad |
It hides issue existence from unauthenticated callers, so an expired token presents as a missing ticket. The fix is to call /myself on a 404 to disambiguate. |
Groq counts max_tokens against the TPM limit |
The reservation is billed, not the completion. A 2,669-token prompt with max_tokens: 8000 reads as 10,669 against an 8,000 cap. Check the reservation before slimming the payload. |
config.json silently shadowed .env |
Saving credentials in the UI wrote them to config.json, which outranks .env, so later edits to .env were ignored with no indication. Settings now shows the source of every value. |
Chapter 08: n8n Agents
Nine n8n workflows plus the prompt files that drive them. The chapter walks the same
Jira-agent idea from chapter 07 across a visual low-code canvas, then pushes past it
into multi-tool agents (Jira + Google Sheets), local models, a deterministic RCA
pipeline, a LinkedIn content chain, a scheduled poster that stops for human approval
before it publishes, a screenshot-to-bug-report intake, and a custom React UI
(ui_screenshotbugAIAgent/) deployed on Vercel that wraps workflow 09 behind a
branded, paste-friendly front end.
Why: an agent loop drawn on a canvas is legible to people who will not read a call stack. It also makes the failure modes visible: you can see exactly where the LLM is allowed to decide, and where a plain node does the work.
chapter_08_n8n/
├── 01_Raw_BugTriage.prompt.md # the persona-only triage prompt (v1)
├── 02_ModifiedBugTriagePrompt.prompt.md # v2: same persona + tool-calling contract
├── 03_RCA_AI_Agent.prompt.md # the meta-prompt that generated workflow 05
├── Agents/
│ ├── 01_FetchJIRATicket_AIAgent.json # 9 nodes: chat/schedule -> agent -> Jira get
│ ├── 02_CreateJIRATicket_AIAgent.json # 9 nodes: same shape, Jira create
│ ├── 03_FetchJIRACreateTCAIAgent_Local_LLM_ollama.json # 6 nodes: local Ollama / Groq brain
│ ├── 04_BugTriageAIAgent.json # 12 nodes: Jira + Google Sheets, two tools
│ ├── 05_RCA_Chatgpt_Jira_Production_Bug_RCA_Automation.json # 35 nodes: full RCA pipeline
│ ├── 06_Social_Media_AIAgent.json # 15 nodes: topic -> post -> image -> review -> LinkedIn
│ ├── 07_Social_Post_Generator (Gemini+ Upload Post).json # 14 nodes: scheduled post + Telegram approval
│ ├── 08_Screenshot_To_Bug_Reporter_AIAgent.json # 11 nodes: screenshot -> vision -> Jira bug
│ ├── 09_Screenshot_To_Bug_Reporter_AIAgent_UI.json # webhook variant for the custom UI
│ ├── Plan.md # model research + design notes for 08
│ ├── Prompt.md # the build prompt that produced 08 and 09
│ └── screenshot-bug-reporter-live-run.png # a real run: VWO-125 filed from a login error
└── resources/
├── Jira_Bug_Triage_Sample.xlsx # the triage sheet the agent writes into
└── VWO-49_RCA.doc # a sample RCA output
Import any file through n8n > Workflows > Import from File, then attach your own
credentials. n8n stores credentials by reference, so the exported JSON carries no secrets.
Workflow 07 is the one exception worth knowing about: it calls the Gemini image endpoint
through a raw HTTP Request node, so the key travels in a plain header parameter rather
than a credential. That value ships as the placeholder YOUR_GEMINI_API_KEY. Replace it
on import, or better, move it into a Header Auth credential (see the Q&A under 07).
The Jira agent pair (01, 02)
The starting shape: a trigger, a brain, a memory, and exactly one Jira tool. A chat message like "fetch PROJ-12" or "raise a bug for the login page" becomes a real Jira API call.
flowchart LR
CT["Chat trigger"] --> AG["AI Agent"]
ST["Schedule trigger"] --> AG
BR["Brain<br/>OpenAI chat model"] --> AG
ME["Memory<br/>buffer window"] --> AG
AG --> JT["Jira tool<br/>get / create issue"]
JT --> OUT["Issue fetched<br/>or created"]
classDef src fill:#57606a,stroke:#24292f,color:#fff
classDef ai fill:#1f6feb,stroke:#0b3d91,color:#fff
classDef gate fill:#bf8700,stroke:#7a5600,color:#fff
classDef out fill:#2da44e,stroke:#0f5323,color:#fff
class CT,ST src
class AG,BR ai
class ME,JT gate
class OUT out
Q&A - why use this?
- Q: When do I reach for it? A: When the workflow should be editable by someone who does not write Python, or when it needs to run on a schedule without hosting your own app.
- Q: What does it replace? A: The glue code around auth, retries and scheduling.
- Q: What's the gotcha? A: The agent decides when to call the Jira tool, so a vague prompt can create the wrong ticket. Chapter 07's approach (deterministic fetch, one bounded LLM step) trades that flexibility for repeatability.
Swapping the brain: local Ollama and Groq (03)
The same fetch agent with the OpenAI node replaced. Two model nodes sit in the file: an Ollama Chat Model (wired in, runs on your own machine) and a Groq Chat Model (parked, swap the connection to use it). Memory is dropped, so the run is stateless.
flowchart LR
CT["Chat trigger"] --> AG["AI Agent"]
OL["Ollama Chat Model<br/>local, wired in"] --> AG
GQ["Groq Chat Model<br/>hosted, parked"] -.swap.-> AG
AG --> JT["Jira tool<br/>get an issue"]
classDef src fill:#57606a,stroke:#24292f,color:#fff
classDef ai fill:#1f6feb,stroke:#0b3d91,color:#fff
classDef alt fill:#8250df,stroke:#4c2889,color:#fff
classDef gate fill:#bf8700,stroke:#7a5600,color:#fff
class CT src
class AG,OL ai
class GQ alt
class JT gate
Q&A - why use this?
- Q: Why bother running locally? A: Ticket text is company data. A local model keeps it on your machine, and the token bill goes to zero.
- Q: What breaks? A: Small local models are weaker at tool calling. They will describe the Jira call instead of making it. Groq is the middle ground: hosted and fast, but still not OpenAI-grade at multi-step tool use.
- Q: How do I decide? A: Start on a hosted model to prove the workflow, then downgrade the brain and see if the tool calls survive.
Bug triage into Google Sheets (04)
The first genuinely multi-tool agent: it reads many Jira issues, triages each one,
and writes a row per issue into a Google Sheet keyed on jira_key. Four brains are
present in the file (OpenAI wired in, Groq and DeepSeek parked) so you can A/B the
triage quality on the same prompt.
flowchart LR
CT["Chat trigger"] --> AG["AI Agent"]
BR["Brain<br/>OpenAI / Groq / DeepSeek"] --> AG
ME["Memory<br/>buffer window"] --> AG
AG --> JT["Jira tool<br/>get many issues"]
AG --> GS["Google Sheets tool<br/>append or update row"]
JT --> LOOP["Triage issue 1 -> write<br/>triage issue 2 -> write"]
LOOP --> GS
GS --> OUT["Triage sheet<br/>keyed on jira_key"]
classDef src fill:#57606a,stroke:#24292f,color:#fff
classDef ai fill:#1f6feb,stroke:#0b3d91,color:#fff
classDef gate fill:#bf8700,stroke:#7a5600,color:#fff
classDef out fill:#2da44e,stroke:#0f5323,color:#fff
class CT src
class AG,BR ai
class ME,JT,GS,LOOP gate
class OUT out
The two prompt files next to it are the interesting part, because they are the same prompt one revision apart:
01_Raw_BugTriage.prompt.md |
02_ModifiedBugTriagePrompt.prompt.md |
|
|---|---|---|
| Length | 72 lines | 370 lines |
| Covers | Persona, severity vs priority, S0-S4 and P0-P4 scales, category taxonomy, triage checklist, rules of the desk | All of that, plus the tool contract |
| Missing | Any mention of the tools | - |
| Adds | - | Mandatory execution order, pagination and dedup rules, per-issue write loop, column-by-column sheet mapping, sheet-tool error handling, final summary format |
Version 1 produces excellent triage as text. It does not reliably call Google Sheets, because nothing in it says the agent must. Version 2 keeps the judgment intact and bolts on the execution contract: retrieve, triage one, write one, repeat, and never invent a Jira issue.
Q&A - why use this?
- Q: What is the actual lesson? A: A great persona prompt is not an agent prompt. The moment tools enter, you have to spell out the call order, the loop granularity, and what counts as success.
- Q: Why write one row at a time? A: Batching at the end means one failure loses the whole run. Per-issue writes make a partial run still useful.
- Q: Why is
jira_keythe match column? A: It makes the write idempotent. Re-running updates rows instead of duplicating them.
Production bug RCA pipeline (05)
The largest workflow in the repo: 35 nodes that turn a Jira production-bug event into a
formatted RCA workbook in Google Drive, as both a Google Sheet and an .xlsx export.
The architectural rule is the opposite of workflow 04. Jira, Drive and Sheets are not agent tools here. They are plain deterministic nodes. The AI Agent does exactly one job: turn the evidence package into structured RCA JSON, validated by an output parser.
flowchart TD
JT["Jira trigger<br/>production bug"] --> CFG["Workflow Configuration"]
MT["Manual trigger + sample event"] --> CFG
CFG --> NRM["Normalize Jira event"]
NRM --> GET["Jira: get issue + comments"]
GET --> JQL["Verify production JQL"]
JQL --> ISP{"Still a production bug?"}
ISP -->|no| SK1["Skip: JQL no longer matches"]
ISP -->|yes| AUD["Audit: read rows"]
AUD --> DUP{"Duplicate revision?"}
DUP -->|yes| SK2["Skip: already generated"]
DUP -->|no| EV["Build Jira evidence package"]
EV --> AI["AI Agent: generate RCA<br/>structured output parser"]
AI --> VAL{"RCA JSON valid?"}
VAL -->|no| ERR["Stop and error"]
VAL -->|yes| WB{"Existing workbook?"}
WB -->|yes| REUSE["Reuse workbook"]
WB -->|no| COPY["Drive: copy RCA template"]
REUSE --> POP["Sheets: clear + populate"]
COPY --> POP
POP --> XLSX["Drive: export + name + move .xlsx"]
XLSX --> LOG["Audit: append success"]
LOG --> SUM["Success summary"]
classDef src fill:#57606a,stroke:#24292f,color:#fff
classDef ai fill:#1f6feb,stroke:#0b3d91,color:#fff
classDef gate fill:#bf8700,stroke:#7a5600,color:#fff
classDef out fill:#2da44e,stroke:#0f5323,color:#fff
classDef bad fill:#cf222e,stroke:#82071e,color:#fff
class JT,MT,CFG,NRM,GET src
class AI ai
class JQL,ISP,DUP,VAL,WB,EV,AUD gate
class COPY,REUSE,POP,XLSX,LOG,SUM out
class SK1,SK2,ERR bad
Three guards run before a single token is spent: the JQL re-check (the label may have been removed since the trigger fired), the audit-table dedup (same issue, same revision, already done), and the structured-output validation (a malformed RCA stops the run instead of writing garbage into the template).
03_RCA_AI_Agent.prompt.md is the 1019-line meta-prompt that produced this workflow. It
specifies the config Set node, the template requirement, every node in the required
workflow, the RCA agent system prompt, the structured output schema, the human review
rule, and the acceptance tests. The workflow JSON is the output; the prompt is the spec.
Q&A - why use this?
- Q: Why not let the agent call Jira and Sheets itself, like workflow 04? A: Because an RCA writes into a shared template that people sign off on. You do not want the model deciding whether to copy a file. Agentic where judgment is needed, deterministic everywhere else.
- Q: What does the audit table buy me? A: Idempotency. Jira fires on every update; without the audit row you would generate a fresh RCA workbook on every comment.
- Q: Why copy a template instead of building the sheet? A: The template carries formatting, formulas, dropdowns and named worksheets. Copying preserves all of it; generating from scratch loses it.
Social media content chain (06)
Two independent pipelines in one file, both on schedule triggers. The main chain is a sequence of specialised agents rather than one agent with many tools:
flowchart LR
ST["Schedule trigger"] --> TG["Content Topic Generator"]
TG --> CC["Content Creator<br/>LinkedIn post"]
CC --> IMG["Gemini: generate image"]
CC --> HT["HashTags agent"]
IMG --> MG["Merge"]
HT --> MG
MG --> RV["Review the Post"]
RV --> LI["LinkedIn: create a post"]
ST2["Schedule trigger 2"] --> CG["Content Generator<br/>Gemini tool + memory"]
CG --> LI2["LinkedIn: create a post"]
CG --> UP["Upload Post node"]
classDef src fill:#57606a,stroke:#24292f,color:#fff
classDef ai fill:#1f6feb,stroke:#0b3d91,color:#fff
classDef gate fill:#bf8700,stroke:#7a5600,color:#fff
classDef out fill:#2da44e,stroke:#0f5323,color:#fff
class ST,ST2 src
class TG,CC,HT,RV,CG,IMG ai
class MG gate
class LI,LI2,UP out
Q&A - why use this?
- Q: Why five agents instead of one? A: Each one has a single job and a short prompt, so you can read its output and fix the one that is wrong. A single mega-agent asked to pick a topic, write, illustrate, tag and review will quietly do the last step badly.
- Q: What is the Review node for? A: It is the quality gate before anything reaches a real audience. Swap it for a manual approval node if you are not ready to auto-post.
- Q: How does this relate to chapter 06? A: Chapter 06 is the voice and hook system as a Claude skill. This is the same idea running unattended on a schedule.
Scheduled post generator with human approval (07)
Concept: a fully unattended LinkedIn poster that runs every morning at 10:00, writes the post, generates its own illustration, and then stops and asks a human on Telegram to approve before anything is published.
Why: workflow 06 gates its output with another agent. An LLM reviewing an LLM catches tone problems but will never catch the one that matters, which is a post you simply did not want to go out under your name. This chain puts a person in that seat without re-introducing the manual work of writing.
Two vendors sit in one chain on purpose. Gemini Flash picks the topic (cheap, high volume, low stakes) and GPT writes the post (the step whose quality you actually feel). The image comes from the Gemini image endpoint via a raw HTTP Request node, is converted to a binary PNG, and rides along as the LinkedIn share media.
flowchart LR
ST["Schedule trigger<br/>daily at 10:00"] --> TG["Content Topic Generator<br/>Gemini Flash + parser"]
TG --> CC["Content Creator<br/>GPT + parser<br/>title / body / tags / image prompt"]
CC --> IMG["HTTP Request<br/>Gemini image endpoint"]
IMG --> CF["Convert to File<br/>base64 -> PNG binary"]
CF --> TA{"Telegram sendAndWait<br/>human approves?"}
TA -->|approved| LI["LinkedIn: create post<br/>shareMediaCategory IMAGE"]
TA -.no reply.-> HOLD["Run waits<br/>nothing is published"]
classDef src fill:#57606a,stroke:#24292f,color:#fff
classDef ai fill:#1f6feb,stroke:#0b3d91,color:#fff
classDef gate fill:#bf8700,stroke:#7a5600,color:#fff
classDef out fill:#2da44e,stroke:#0f5323,color:#fff
classDef bad fill:#cf222e,stroke:#82071e,color:#fff
class ST src
class TG,CC,IMG ai
class CF,TA gate
class LI out
class HOLD bad
The whole gate is one node. sendAndWait suspends the execution and resumes it only when
the reply arrives, so no approval means no post rather than a default-yes:
{
"name": "Send a text message",
"type": "n8n-nodes-base.telegram",
"parameters": {
"operation": "sendAndWait",
"chatId": "859100842",
"message": "={{ $('Content Creator (Linkedin Post)').item.json.output.post_body }}"
}
}
{
"name": "Create a post",
"type": "n8n-nodes-base.linkedIn",
"parameters": {
"text": "={{ $('Content Creator (Linkedin Post)').item.json.output.post_body }}",
"shareMediaCategory": "IMAGE",
"additionalFields": {
"title": "={{ $('Content Creator (Linkedin Post)').item.json.output.post_title }}",
"visibility": "PUBLIC"
}
}
}
| 06: agent review | 07: human approval | |
|---|---|---|
| Gate | Review the Post agent |
Telegram sendAndWait |
| Blocks on | Model judgment | A real reply |
| Failure mode | Approves a bad post confidently | Nothing posts until you answer |
| Use when | Volume matters more than any single post | Your name is on it |
Q&A - why use this?
- Q: Why
sendAndWaitinstead of a Telegram send plus an IF node? A:sendAndWaitparks the execution and resumes on reply. A plain send would fire the message and continue straight into the LinkedIn node, which is exactly the thing the gate exists to prevent. - Q: Why does the image go through an HTTP Request node instead of the Gemini node? A: The image endpoint returns base64 inside a nested
steps[1].content[0].datapath, so the response needs aConvert to Filestep to become the PNG binary LinkedIn wants. That also means the key lives in a header parameter, not a credential. SwapYOUR_GEMINI_API_KEYfor a Header Auth credential (authentication: genericCredentialType) if you would rather it never touched the JSON. - Q: What's the gotcha? A: The topic-generator's output parser is set to
manualwith no schema, so the second agent's prompt reads$json.output.properties.topic.typeand picks up the literal string"string"out of the JSON Schema envelope instead of the topic. Fill in the first parser'sinputSchema(as the second one does) and the expression shortens to$json.output.topic. It is a good illustration of the real hazard in visual pipelines: a wrong expression does not crash, it quietly feeds plausible garbage forward.
Screenshot to bug reporter (08)
Concept: a hosted upload form takes a screenshot plus optional error logs, a vision model drafts a complete structured bug report from what it can actually see, and the workflow files it into Jira with the original PNG attached to the ticket.
Why: the visual detail is the part testers summarise away. The red banner, the overlapping div, the truncated error string: all of it is in the screenshot and almost none of it survives being retyped into a ticket by hand.
This is the first workflow in the chapter with a human file-upload intake. Nothing in
01-07 ingests a user-supplied file, and the string base64 appears nowhere else in the
chapter.
flowchart LR
FT["Form trigger<br/>screenshot + logs"] --> NI["Normalize intake<br/>resolve binary, build prompts"]
NI --> B64["Extract from file<br/>binary to base64"]
B64 --> GV["Groq vision<br/>qwen3.6-27b, JSON mode"]
GV --> PB["Parse bug report<br/>validate + render description"]
PB --> JC["Jira: create bug<br/>VWO / Bug"]
JC --> PA["Prepare attachment<br/>re-attach binary"]
PA --> JA["Jira: add attachment<br/>screenshot on the ticket"]
classDef src fill:#57606a,stroke:#24292f,color:#fff
classDef ai fill:#1f6feb,stroke:#0b3d91,color:#fff
classDef gate fill:#bf8700,stroke:#7a5600,color:#fff
classDef out fill:#2da44e,stroke:#0f5323,color:#fff
class FT src
class GV ai
class NI,B64,PB,PA gate
class JC,JA out
Note what is not here: no AI Agent node. The model gets exactly one bounded job, and every other step is a plain deterministic node. This is workflow 05's shape, not workflow 01's. An agent node would also hand the tool-calling loop the decision of whether to look at the image at all.
The whole model call is one HTTP node, and the key never enters the file:
{
"method": "POST",
"url": "https://api.groq.com/openai/v1/chat/completions",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "groqApi",
"jsonBody": "={{ JSON.stringify({ model: 'qwen/qwen3.6-27b', response_format: { type: 'json_object' }, messages: [ { role: 'system', content: $('Normalize Intake').first().json.system_prompt }, { role: 'user', content: [ { type: 'text', text: $('Normalize Intake').first().json.user_prompt }, { type: 'image_url', image_url: { url: 'data:' + $('Normalize Intake').first().json.mime_type + ';base64,' + $json.screenshot_b64 } } ] } ] }) }}"
}
Agents/Plan.md carries the full model research. The short version is that Groq's vision
lineup contracted through 2026: llama-3.2-11b-vision was decommissioned and Llama 4
Scout and Maverick left the supported list, so the only image-capable models left are
qwen/qwen3.6-27b and qwen/qwen3.8-27b, both Preview.
Groq qwen3.6-27b |
OpenRouter glm-5.3-flash |
|
|---|---|---|
| Input / output per M | $0.60 / $3.00 | $0.075 / $0.25 |
| Per bug report | ~$0.0036 | ~$0.0004 |
| Status | Preview | Production |
| JSON | response_format honoured |
No server-side schema enforcement |
| Pick it for | Speed | Cost, and stability |
Q&A - why use this?
- Q: Why is the binary re-attached twice? A: Because it does not survive the HTTP hop, and Jira's create response carries none either. Nodes 5 and 7 both pull it back from
$('Normalize Intake').first().binary. Forgetting this is the single most common way a file-handling n8n workflow silently posts a ticket with no attachment. - Q: Why resolve the uploaded file by
Object.keys(item.binary)[0]instead of by name? A: The Form Trigger derives the binary property name from the field label, and the exact form has changed between n8n versions. Taking the first key and re-keying it to a stablescreenshotproperty means the rest of the workflow depends on our name, not n8n's. - Q: What's the gotcha? A: A vision model asked for a bug report will always produce one. Upload a screenshot of a perfectly healthy page and a careless prompt invents a defect to fill the schema. The system prompt therefore forbids inventing anything not visible and requires a
confidencefield, and that unbroken-page run is the test worth doing first.
Screenshot to bug reporter UI (09)
Concept: workflow 08 with its Form Trigger swapped for a Webhook and a Respond to Webhook node bolted on the end, so a custom front end can call it as a JSON API and get the filed ticket back instead of an n8n-hosted HTML page.
Why: an n8n form is fine for a demo, but the moment you want your own branding, a paste -from-clipboard upload, or the Jira key rendered back to the reporter, you need a real API and a real UI in front of it.
The front end lives in ui_screenshotbugAIAgent/ (Vite + React
- Tailwind) and is deployed on Vercel. A real run, end to end:

Note what the model read off that screenshot: the exact banner text, the email in the input
(opg73@singleuseemail.site), the empty password field, the unchecked reCAPTCHA. That is
the detail a tester would never retype by hand.
flowchart LR
UI["Vercel UI<br/>paste or drop a screenshot"] --> PX["/api/report<br/>serverless proxy"]
PX --> WH["n8n Webhook<br/>POST screenshot-bug-report"]
WH --> NI["Normalize intake"]
NI --> GV["Groq vision<br/>qwen3.8-27b, JSON mode"]
GV --> JC["Jira: create + attach"]
JC --> RW["Respond to Webhook<br/>jira_key, summary, confidence"]
RW --> UI
classDef src fill:#57606a,stroke:#24292f,color:#fff
classDef ai fill:#1f6feb,stroke:#0b3d91,color:#fff
classDef gate fill:#bf8700,stroke:#7a5600,color:#fff
classDef out fill:#2da44e,stroke:#0f5323,color:#fff
class UI src
class GV ai
class PX,WH,NI gate
class JC,RW out
The browser never calls n8n. It posts to a same-origin serverless function, which forwards the raw multipart body server-side:
// ui_screenshotbugAIAgent/api/report.js
export const config = { api: { bodyParser: false } }; // keep the multipart boundary intact
export default async function handler(req, res) {
const target = process.env.N8N_WEBHOOK_URL; // never reaches the browser
const chunks = [];
for await (const chunk of req) chunks.push(chunk);
const upstream = await fetch(target, {
method: 'POST',
headers: { 'content-type': req.headers['content-type'] },
body: Buffer.concat(chunks),
});
return res.status(200).json(JSON.parse(await upstream.text()));
}
Q&A - why use this?
- Q: Why a proxy instead of calling n8n from the browser? A: Two reasons. CORS, which a same-origin request sidesteps entirely, and secrecy: a webhook URL in client-side JavaScript is readable in devtools, and anyone who finds it can file tickets into your project.
- Q: Why does
bodyParserhave to be off? A: The payload ismultipart/form-datacarrying an image. Parsing and re-serialising it destroys the boundary, and n8n then receives no file. Forward the raw bytes and setcontent-typefrom the incoming request. - Q: How long have I got? A: The proxy caps at 55s and the Vercel function at 60s. A Groq run plus two Jira calls lands around 6-30s, so it fits, but a cold start on a slow run can crowd it. The UI walks five stage labels on a timer so the wait reads as progress, not a hang.
- Q: What's the gotcha? A: The
/form/<uuid>URL n8n shows you belongs to the Form Trigger and serves HTML. Workflow 09 uses a Webhook node, so its address is/webhook/screenshot-bug-report. Point the proxy at the form URL and it half-works in the worst way: the bug gets filed, but the reply is an HTML page, so the UI reports a non-JSON response and you never see the key.
The failure worth teaching
The first live run submitted a screenshot of a perfectly healthy page. The model
returned "confidence": "high" and invented a layout defect, claiming the heading was
truncated to "File a bug from a screens". None of it was in the image.
The cause was the prompt, in two places:
| Mistake | Fix |
|---|---|
| The task framing presupposed a bug: "Analyze the screenshot and draft the bug report" | Reframed so the first instruction is to decide whether a defect exists at all, with "Nothing is wrong here" named as a correct answer |
The reporter's severity hint (S4 - cosmetic) primed it, and it duly found a cosmetic issue |
Told explicitly that the severity guess is not evidence, and neither is being asked for a report |
A generator asked for X will produce X. If "nothing to report" is a valid outcome, the prompt has to make it an explicit first-class branch, not a caveat buried in a rule list. Which is why the negative case, uploading a screenshot of a normal page and expecting low confidence, is the first test to run after any prompt or model change, not the last.
Cross-chapter takeaway
Workflows 01-04 and 06 are agentic: the model chooses which tool to call. Workflows 05, 07, 08 and 09 are deterministic with bounded LLM steps, the same shape as chapter 07's B.L.A.S.T. agent. The chapter is arranged so you feel the difference: agentic is faster to build and easier to demo, deterministic is what survives contact with a template your team signs off on.
Chapter 09: LangFlow
Concept: LangFlow is a visual low-code canvas for building AI pipelines, similar to n8n but purpose-built for LLM workflows. Drag nodes onto a canvas, connect them, and export a runnable Python pipeline or a JSON file.
Why: n8n is general-purpose automation. LangFlow is LLM-native: vector stores, prompt templates, chains, agents, and retrievers are first-class nodes, not HTTP workarounds. It is the tool you reach for when the pipeline is mostly AI reasoning rather than API orchestration.
flowchart LR
INSTALL["pip install langflow"] --> RUN["langflow run"]
RUN --> UI["Web UI on :7860"]
UI --> CANVAS["Drag nodes<br/>connect pipeline"]
CANVAS --> EXPORT["Export as Python<br/>or JSON"]
EXPORT --> DEPLOY["Run anywhere<br/>or host as API"]
classDef src fill:#57606a,stroke:#24292f,color:#fff
classDef ai fill:#1f6feb,stroke:#0b3d91,color:#fff
classDef out fill:#2da44e,stroke:#0f5323,color:#fff
class INSTALL,RUN src
class UI,CANVAS ai
class EXPORT,DEPLOY out
# Quick start
mkdir langflow-qa && cd langflow-qa
python3 -m venv venv
source venv/bin/activate
pip install langflow
langflow run # opens http://localhost:7860
Q&A - why use this?
- Q: When do I reach for LangFlow over n8n? A: When the pipeline is mostly LLM reasoning (chains, RAG, agents) rather than API orchestration. LangFlow's vector-store and retriever nodes are native; n8n's are HTTP calls.
- Q: What does it replace? A: Hand-writing LangChain Python scripts and debugging chain wiring in code. The canvas makes the flow visible.
- Q: What's the gotcha? A: LangFlow is a development tool, not a production scheduler. For scheduled, unattended runs with approval gates, n8n (chapter 08) is the better fit.
The bug triage flows
Four flows, each a step further than the last. All four fetch a Jira issue over REST, parse it, and hand it to DeepSeek with a triage prompt.
| Flow | Ticket source | Callable from a UI? |
|---|---|---|
01_LangFlow_Simple_HelloWorld |
none, ChatInput to GroqModel | the hello-world |
03_AI4X_003_..._Agentic |
hardcoded VWO-49 in the URL |
no, input is ignored |
004_AI4X_004_..._Via_UI |
ChatInput feeds {issue_key} into the URL |
yes |
Flow 03 has no ChatInput at all, so input_value has nowhere to land and every call
returns the same VWO-49 triage. Flow 004 fixes that by routing the chat input into a
Prompt Template that builds the URL:
ChatInput ──► Prompt Template https://<site>.atlassian.net/rest/api/3/issue/{issue_key}
└──► APIRequest ──► Parser ──► Prompt Template (triage) ──► DeepSeek ──► ChatOutput
Calling a flow from your own UI
Concept: every LangFlow flow is already an HTTP endpoint at
POST /api/v1/run/<flow-id>, so a front end only has to send JSON and render the reply.
Why: the LangFlow canvas is a building tool, not something you hand a tester. A 15-line fetch turns the same flow into an app your batch can actually use.
chapter_09_LangFlow/ui_bugtriage/ is that front
end: React plus react-markdown, no CSS framework, a Run button, and the model's markdown
tables rendered properly.
const res = await fetch('/api/run', { // vite proxy locally, Vercel function in prod
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
output_type: 'chat',
input_type: 'chat', // MUST be "chat" - see the gotcha below
input_value: 'VWO-51', // the ticket key; there is no "issue_key" field
session_id: 'ui-1a2b3c',
}),
});
const data = await res.json();
const text = data.outputs[0].outputs[0].results.message.data.text;
flowchart LR
B["Browser<br/>enter VWO-51"] --> PX["Proxy<br/>vite dev / Vercel fn"]
PX -->|x-api-key injected| LF["LangFlow<br/>/api/v1/run/<flow>"]
LF --> JIRA["Jira REST<br/>issue/{issue_key}"]
JIRA --> DS["DeepSeek<br/>triage prompt"]
DS --> B
classDef src fill:#57606a,stroke:#24292f,color:#fff
classDef ai fill:#1f6feb,stroke:#0b3d91,color:#fff
classDef gate fill:#bf8700,stroke:#7a5600,color:#fff
classDef out fill:#2da44e,stroke:#0f5323,color:#fff
class B src
class DS ai
class PX,LF gate
class JIRA out
Q&A - why use this?
- Q: Why
input_type: "chat"and not"text"? A: Because the flow's entry node is a ChatInput."text"targets a TextInput, which this flow does not have, so the value is dropped and the ticket key never reaches the URL. This is the single most important line in the request. - Q: Why a proxy instead of calling LangFlow straight from the browser? A: The
x-api-keyheader. In client-side JavaScript anyone can read it in devtools and then run your flows, which in this case means reading your Jira. The proxy injects it server-side and the browser posts same-origin, so CORS never arises either. - Q: What's the gotcha? A: A wrong request still returns HTTP 200 with a confident report. Send
input_type: "text"and the key is empty, so Jira getsGET /rest/api/3/issue/, answers 405 Method Not Allowed, and DeepSeek dutifully triages the error response as though it were a bug (Severity: High). Nothing in the status code tells you. The tell is token count: ~150 input tokens means it read a 405; ~2,200 means it read a real ticket.
Deploying the UI. A Vercel function cannot reach
localhost:7860- there,localhostis its own container. Either host LangFlow somewhere public or tunnel it (cloudflared tunnel --url http://localhost:7860) and set that URL asLANGFLOW_URL. Remember the flow'sAPIRequestnode stores a JiraAuthorization: Basicheader, so a public tunnel means anyone holding the API key can read your Jira through it. The flow JSONs in this repo ship that header redacted to a placeholder.
Chapter 10: RAG Basics
Concept: RAG (Retrieval Augmented Generation) is how you get an LLM to answer from your documents instead of its training data: split the document into chunks, turn each chunk into a vector, and at question time retrieve the closest chunks and paste them into the prompt.
Why: fine-tuning a model on a 7-page PRD is absurd, and pasting the whole document into every prompt gets expensive and hits context limits. Retrieval sends only the 3 paragraphs that matter.
01_RAG_Explorer is a RAG pipeline you can watch happen. Every stage is on screen: the
raw extraction, the repair, the chunks, the 768-number vectors, the similarity scores, and
the verbatim prompt that reaches the model.
Live demo: rag-explorer-tta.vercel.app
cd chapter_10_RAG_Basics/01_RAG_Explorer
./run.sh # http://localhost:5190

The five stages
flowchart LR
PDF["PDF<br/>7 pages"] --> EX["Extract<br/>pypdf"]
EX --> NM["Normalise<br/>repair the text"]
NM --> CH["Chunk<br/>180w / 40 overlap"]
CH --> EM["Embed<br/>nomic · 768d"]
EM --> ST["Store<br/>Chroma · cosine"]
Q["Question"] --> QE["Embed the query"]
QE --> SR{"Cosine<br/>similarity"}
ST --> SR
SR --> T3["Top 3 chunks"]
T3 --> LLM["gpt-oss-120b<br/>answer from context only"]
classDef src fill:#57606a,stroke:#24292f,color:#fff
classDef ai fill:#1f6feb,stroke:#0b3d91,color:#fff
classDef gate fill:#bf8700,stroke:#7a5600,color:#fff
classDef out fill:#2da44e,stroke:#0f5323,color:#fff
class PDF,Q src
class EM,QE,LLM ai
class EX,NM,CH,SR gate
class ST,T3 out
Retrieval is the whole trick, and it is just arithmetic:
// Both vectors are unit-normalised, so the dot product IS the cosine similarity.
const dot = (a, b) => { let s = 0; for (let i = 0; i < a.length; i++) s += a[i] * b[i]; return s; };
const top3 = chunks
.map((c) => ({ ...c, similarity: dot(queryVector, c.vector) }))
.sort((a, b) => b.similarity - a.similarity)
.slice(0, 3);
// similarity = 1 - cosine_distance. No keyword matching happens anywhere:
// "how do users sign in" retrieves the chunk about *authentication*.
The step everyone skips
The bundled PRD was exported from Google Docs, and pypdf returns one word per line:
Product\n \nRequirements\n \nDocument:\n \nVWO\n \nLogin\n \nDashboard
Chunk that as-is and every chunk is a column of disconnected words, which embeds into noise, which makes retrieval useless. The normaliser detects the pattern (more than 60% of lines holding a single token) and rejoins the text, stripping 2,432 characters of junk from this file. The UI shows raw and normalised side by side, because this is the step most likely to quietly ruin a RAG demo and nobody ever shows it.
Running it two ways
The same UI detects where it is and switches engines. Everything on screen is genuinely computed in both; only the model and its location change.
Local (./run.sh) |
Hosted (Vercel) | |
|---|---|---|
| Extract + chunk | pypdf, live, sliders re-run it | pre-computed at build time |
| Embeddings | nomic-embed-text, 768d, Ollama |
MiniLM, 384d, in the browser |
| Vector search | ChromaDB | cosine in JavaScript |
| Answer | openai/gpt-oss-120b on Groq |
same, via a rate-limited function |
Vercel cannot reach a local Ollama, and 79 vectors do not need a vector database, so the hosted build bakes the chunk vectors at build time and embeds the query client-side.
Q&A - why use this?
- Q: Where does my document actually go? A: Locally, nowhere. Extraction, embedding and the Chroma index all run on your machine, so you can demo the entire ingest half with nothing on the wire. Only the 3 retrieved chunks reach Groq, and only when you use the Chat tab. For a confidential document that distinction is the whole argument for local embeddings.
- Q: Why does 60% similarity count as a good match? A: Because scores are relative, not absolute. Nomic on prose rarely passes ~0.7 for a short question against a 180-word chunk. Judge hits by their ranking against each other, never against a fixed threshold, and never show students a bare percentage without that caveat.
- Q: What's the gotcha? A: Chunk size is a real trade-off with no right answer. At 60 words you get sharper scores but answers truncated mid-thought; at 400 each chunk holds full context but retrieval blurs because one vector now averages too many ideas. The slider exists so you can watch it break in both directions.
Grounding is a prompt, not a property of the model. Ask the explorer something the PRD does not cover ("what is the pricing?") and it answers "the provided document excerpts do not contain any information about pricing" instead of inventing a number. That comes from one line in the system prompt in
server/app.py, not from the model being careful. Delete the line and it will happily make something up.
Chapter 11: RAG Implementation
Concept: naive RAG built on hosted infrastructure instead of local parts: a CSV of 100 Jira test cases goes into a Pinecone index through OpenAI embeddings, and an n8n AI Agent queries it as a retrieval tool.
Why: chapter 10 explains the mechanics on a single 7-page PRD. This chapter moves to a corpus that is awkward in the way real corpora are awkward, a hundred short near-identical records, and that awkwardness changes how you have to chunk.
chapter_11_RAG_Implementation/00_NAIVE_RAG/
├── Prompt.md # the thread that generated the corpus
├── data/Wingify_Login_100_Jira_Test_Cases.csv # 100 cases, 18 fields, ~30k words
└── n8n/
├── 01_Naive_RAG.json # 15 nodes: blind text splitting
└── 01_Naive_RAG - Complete Test Cases.json # 20 nodes: one document per row
The corpus is evenly spread across six categories at 10 cases each (Authentication, Email validation, Password and boundaries, Navigation and usability, Password recovery, Sessions and Remember me), 56 negative scenarios to 44 positive.
Stack: Pinecone index airagnew, namespace wingify-login-v2, embeddings from
text-embedding-3-large, answers from gpt-5-mini.
Two workflows, one lesson
Both files have the same two halves: a form upload that ingests, and a chat trigger whose agent retrieves. The difference is entirely in how the CSV becomes documents.
flowchart LR
F["Form upload<br/>Testcase Path"] --> V1{"How does the CSV<br/>become documents?"}
V1 -->|"01_Naive_RAG<br/>15 nodes"| SPL["Recursive Character<br/>Text Splitter, defaults"]
V1 -->|"01_Naive_RAG -<br/>Complete Test Cases<br/>20 nodes"| EX["Extract CSV Rows<br/>enableBOM: true"]
EX --> BLD["Build Test Case Documents<br/>one doc per row + metadata"]
BLD --> LOOP["Loop One Test Case"]
SPL --> PC[("Pinecone<br/>airagnew / wingify-login-v2")]
LOOP --> PC
C["Chat trigger"] --> AG["AI Agent<br/>gpt-5-mini"]
AG -->|retrieval tool| PC
PC --> AG
AG --> ANS["Grounded answer"]
classDef src fill:#57606a,stroke:#24292f,color:#fff
classDef ai fill:#1f6feb,stroke:#0b3d91,color:#fff
classDef gate fill:#bf8700,stroke:#7a5600,color:#fff
classDef out fill:#2da44e,stroke:#0f5323,color:#fff
class F,C src
class AG ai
class V1,SPL,EX,BLD,LOOP gate
class PC,ANS out
Why one document per row
A test case is already a self-contained unit of about 765 characters. Turn the whole CSV into one text blob and let a character splitter cut it every N characters, and the cuts land mid-case: half of TC-014's expected result gets glued to the start of TC-015's preconditions. The embedding for that fragment describes nothing real, and retrieval returns it for neither case.
So the second workflow never splits text. It builds one document per row and attaches the row's own fields as Pinecone metadata:
// Build Test Case Documents - strips the BOM, then keeps each case whole
const fields = ['Test Case ID', 'Summary', 'Category', 'Scenario Type', 'Priority',
'Preconditions', 'Test Data', 'Test Steps', 'Expected Result',
'Execution Status', 'Assumptions / Applicability'];
return $input.all().map((item) => {
// Excel writes a UTF-8 BOM, so the first header is "Test Case ID"
const row = Object.fromEntries(
Object.entries(item.json).map(([k, v]) => [k.replace(/^/, '').trim(), v])
);
return { json: {
text: fields.map((f) => `${f}: ${row[f] ?? ''}`).join('\n'),
tc_id: row['Test Case ID'],
category: row['Category'],
scenario_type: row['Scenario Type'],
priority: row['Priority'],
}};
});
That metadata is the payoff. Once category and scenario_type are on every vector, you
can filter before you rank, which is what saves you when a hundred records all share the
same vocabulary.
Q&A - why use this?
- Q: When do I reach for Pinecone over the local Chroma in chapter 10? A: When the index has to outlive the process and be reachable from somewhere that is not your laptop. Chapter 10's Chroma file is perfect for one machine and useless to an n8n cloud workflow, which is exactly why this chapter switched.
- Q: What does one-document-per-row replace? A: The default Recursive Character Text Splitter, which is the right tool for prose and the wrong tool for records. If your rows are already atomic, splitting by character count can only damage them.
- Q: What's the gotcha? A: A hundred near-identical records is the hard case for retrieval. Every row says login, password, email, sign in, so the vectors cluster tightly and similarity scores compress into a narrow band where top-k barely discriminates. This is the point where metadata filtering, reranking, or hybrid keyword-plus-vector search stop being optional. Naive RAG is the baseline here, not the destination.
The CSV has a UTF-8 BOM. The first column reads as
"Test Case ID", sorow["Test Case ID"]throws aKeyErroragainst a header that looks perfectly correct in every editor. Both workflows handle it,Extract CSV RowswithenableBOM: trueand the Code node withreplace(/^/, ''). In Python it isencoding="utf-8-sig".
Chapter 12: QABuddy, Hybrid RAG for QA
Concept: QABuddy.ai is a self-hosted RAG over everything a QA team owns: the Selenium and Playwright frameworks, 500 test cases, Jira bugs, the PRD, meeting notes, Lucid charts and Jenkins logs. One question gets one answer with numbered citations to the exact ticket, log line or method.
Why: chapter 11 ended on its own gotcha: with near-identical records, pure vector
search stops discriminating. Real QA questions also name things that embeddings blur
(VWO-33, testLoginPositiveVWO, build #142), span several sources at once, and
sometimes ask about absence ("which features have no test cases?"), which top-k search
cannot answer at all.
Live demo: qabuddy-ai-nine.vercel.app
git clone --recurse-submodules https://github.com/PramodDutta/AITesterBlueprint4x
cd AITesterBlueprint4x/chapter_12_RAG_QA_BuddyAI
./run.sh # Qdrant + Ollama, ingests on first run, http://localhost:8300
The two framework repos are git submodules; run.sh fetches them if you cloned without
--recurse-submodules.

Five decisions
| Decision | Choice | Why |
|---|---|---|
| Embedding model | Qwen3-Embedding-4B on Ollama, 1024 dims | Top-ranked family on MTEB multilingual, reads code, 32K-token input, Apache 2.0 |
| Vector database | Qdrant | Dense and BM25 sparse vectors plus RRF fusion in one request, payload filters, single binary |
| Chunking | Along the unit a QA engineer asks about | One row per test case, one chunk per Jira comment, heading-aware PRD sections, AST-aware code, failure windows in logs |
| Reranker | bge-reranker-v2-m3 | Re-reads the fused top 24 with the question and keeps the best |
| Answer LLM | gpt-oss-120b on Groq | Fast, cheap, follows "cite or refuse" rules |
Six modes shape both retrieval and the answer: Ask anything, Failure analysis (RCA), Test design and gaps, Bug triage, Framework coding help, and Traceability (RTM). The chapter README has the per-source chunk sizes, the preprocessing rules and the deployment plan.
Hybrid retrieval, measured
flowchart LR
Q["Question + mode"] --> X["Exact id lookup<br/>VWO-33, LOGIN-002"]
Q --> D["Qwen3 dense<br/>1024d"]
Q --> B["Code-aware BM25<br/>loginToVWO -> login, vwo"]
D --> F{"Qdrant<br/>RRF fusion"}
B --> F
F --> R["bge-reranker-v2-m3<br/>top 24"]
X --> R
R --> S["Select: quotas,<br/>source caps, 3.5k tokens"]
S --> L["gpt-oss-120b<br/>cite or refuse"]
L --> A["Answer with [n] citations<br/>+ retrieval trace"]
classDef src fill:#57606a,stroke:#24292f,color:#fff
classDef ai fill:#1f6feb,stroke:#0b3d91,color:#fff
classDef gate fill:#bf8700,stroke:#7a5600,color:#fff
classDef out fill:#2da44e,stroke:#0f5323,color:#fff
class Q src
class D,R,L ai
class X,B,F,S gate
class A out
Dense search, BM25 and their fusion run as one batched Qdrant request. It also returns each retriever's own ranking, which is how the UI can show why every candidate was kept or dropped:
# qabuddy/store.py: one round trip, three result lists
reqs = [
models.QueryRequest(query=dense, using="dense", limit=prefetch_k, filter=flt, with_payload=False),
models.QueryRequest(query=sv, using="bm25", limit=prefetch_k, filter=flt, with_payload=False),
models.QueryRequest(
prefetch=[
models.Prefetch(query=dense, using="dense", limit=prefetch_k, filter=flt),
models.Prefetch(query=sv, using="bm25", limit=prefetch_k, filter=flt),
],
query=models.FusionQuery(fusion=models.Fusion.RRF),
limit=fused_k,
with_payload=True,
),
]
dense_res, sparse_res, fused_res = client().query_batch_points(s.collection, requests=reqs)
On a 24-question golden set (./run.sh eval):
| Retriever | hit@6 | MRR |
|---|---|---|
| dense only (Qwen3) | 100% | 0.83 |
| BM25 only | 100% | 0.88 |
| hybrid (RRF) | 100% | 0.91 |
| full (ids + hybrid + rerank) | 100% | 0.91 |
Every retriever finds the answer somewhere in the top 6; they differ in how high they rank it. "Is testLoginPositiveVWO flaky?" puts ticket QAB-102 at #31 by meaning but #2 by keyword: a method name is a string, not a concept.

Top-k cannot prove absence
"Which PRD features have no test cases?" retrieves the six most similar rows and never sees the missing ones. The fix happens at ingest: QABuddy writes inventory chunks (one repository summary, one chunk per test module listing every scenario, one outline per long document), and the gap and RTM modes pin them into the context with per-source quotas. With the inventory in view the model can say what is missing. On this data it reported that none of the 70 A/B testing cases are automated.
Q&A - why use this?
- Q: When is pure vector search enough? A: For prose questions over prose. As soon as questions carry identifiers (ticket keys, test ids, method names, build numbers), keyword search and exact-id lookup earn their place, and fusion means you never have to pick one.
- Q: What does the reranker add if hybrid already finds everything? A: It decides which 6-8 chunks the LLM actually reads. Retrieval metrics tie here; answers do not, because a cross-encoder reads the question and the chunk together instead of comparing two precomputed vectors.
- Q: What's the gotcha? A: Groq's on-demand tier allows 8,000 tokens per minute and counts the
max_tokensreservation, so one key serves about two answers a minute. QABuddy retries on a 429 and reports the wait separately from latency, but a team needs a paid tier or a self-hosted model.
The hosted demo is honest about what it is. Vercel cannot reach a laptop's Qdrant, Ollama or reranker, so the example questions replay recorded runs of the full pipeline (traces included), and new questions run BM25 in the browser plus a rate-limited Groq function, labelled as such in the UI. The DigitalOcean deployment (Docker Compose plus Caddy for HTTPS and a login) is in
deploy/DEPLOY.md: written, not yet run end to end.
What is left: Todo_List.md is the phase roadmap
(auto-ingestion, caching, pipeline tests, cloud deployment, then Figma and screenshots), and
PENDING_TASKS.md breaks the open work into 24
prioritised tasks, each with a "done when".
Chapter 13: MCP Basics
Concept: MCP (Model Context Protocol) is an open standard that connects AI applications (Claude Code, Codex, Cursor, Windsurf, Kiro) to external systems (Jira, Slack, Playwright, Selenium, GitHub, databases). An MCP server exposes tools, resources and prompts once, and every MCP-capable app can use them.
Why: an LLM on its own is only a brain with knowledge. An AI agent is brain + memory + tools, and MCP is how it gets the tools without a custom integration for every app and every system.
The class notes are in MCP_Inspector.md: MCP vs API,
the host/client/server roles, how to install the Playwright MCP, two ways to build your own
(vibe coding with prompts, or Python with FastMCP), the TestCreator MCP plan, and 25+ MCP ideas.
flowchart LR
subgraph HOST["MCP host: Claude Code, Cursor, VS Code"]
LLM["LLM<br/>the brain"] --> CL["MCP client<br/>1:1 per server"]
end
CL --> S["MCP server"]
S --> T["Tools<br/>verbs, like POST"]
S --> R["Resources<br/>nouns, like GET"]
S --> P["Prompts<br/>reusable recipes"]
T --> EXT["External systems<br/>Playwright, Jira, DB"]
classDef src fill:#57606a,stroke:#24292f,color:#fff
classDef ai fill:#1f6feb,stroke:#0b3d91,color:#fff
classDef gate fill:#bf8700,stroke:#7a5600,color:#fff
classDef out fill:#2da44e,stroke:#0f5323,color:#fff
class LLM ai
class CL,S gate
class T,R,P ai
class EXT out
Tools, resources and prompts
| Primitive | Think of it as | Example | API analogy |
|---|---|---|---|
| Tool | A verb the agent executes | create a Jira issue, run a test, click a button | POST |
| Resource | A read-only noun for context | a file list, today's test plan, a style guide | GET |
| Prompt | A reusable recipe on the server | /test-review, /bug-triage with arguments |
none, user-triggered |
The fastest way to feel MCP is to add an existing server. The Playwright MCP gives an AI a real browser:
# Claude Code
claude mcp add playwright -- npx @playwright/mcp@latest
// .vscode/mcp.json (VS Code)
{
"servers": {
"playwright": { "type": "stdio", "command": "npx", "args": ["@playwright/mcp@latest"] }
}
}
25+ MCP ideas for QA
The notes list 25 servers worth building, in three groups:
- Manual tester wins: test case generator, test repository search, bug report writer, requirement ambiguity checker, test data factory.
- Automation tester wins: flaky test intelligence, failure RCA, locator healer, API contract server, test impact analysis.
- Team-level servers: a test management bridge across TestRail, Zephyr, Xray and Qase; QA metrics; a shared QA prompt library.
Chapter 14 builds two of them.
Q&A - why use this?
- Q: Why MCP and not just an API? A: An API is for programs you write against it. MCP is for AI apps: one server works in Claude Code, Cursor and Codex unchanged, and the model discovers what it can do from the tool descriptions.
- Q: Should my feature be a tool, a resource or a prompt? A: A tool if it does something, a resource if it only provides context to read, a prompt if it is a recipe a person starts on purpose.
- Q: What's the gotcha? A: The model chooses tools by reading their names and descriptions. Vague or overlapping tools make it pick the wrong one, so tool design matters as much as the code behind it.
Chapter 14: MCP Servers by Vibe Coding
This chapter builds two MCP servers by talking to Claude Code, from first prompt to pushed code:
TestCreator MCP turns a 5,000-row test case CSV into 21 tools, and TestSkill Finder MCP
lets any LLM search and use a library of 100 QA agent skills. Every prompt is in
02_prompt.md.

Concept: TestCreator MCP (TC MCP) is an MCP server, built with FastMCP 4.1, that turns a 5,000-row VWO test case CSV into 21 tools any MCP client can call. You can find tests by module, priority, type, browser or device, rank what to run first, build smoke, sanity and regression suites, find duplicates and coverage gaps, write new tests in the catalog format, and export to Jira CSV, Markdown, Gherkin or Playwright stubs.
Why: a test catalog is too big to paste into a chat (about 950k tokens). Tools let the assistant ask for exactly the 20 rows and the exact counts it needs, and any QA or dev can share the same server.
The design, from data profile to phased build plan, is in
01_TestCreatorMCP.plan.md.
cd chapter_14_MCP_Create_VIBE/testcase-creator-mcp
uv sync && uv run pytest -q # 33 tests
uv run tc-mcp --transport http --port 8000 # http://127.0.0.1:8000/mcp
npx @modelcontextprotocol/inspector --transport http --server-url http://127.0.0.1:8000/mcp
Data first, then tools
The first prompt said "don't code", so the CSV was profiled before any tool was designed. What it found shaped the server:
| Finding | Design response |
|---|---|
| Only 1,520 unique scenarios behind 5,000 rows, 466 exact duplicates | "Top N" keeps the best variant per scenario and spreads picks across features, so the top 10 is not 10 browser copies of one test. find_duplicate_tests lists them for cleanup |
| 415 Deprecated tests | Hidden by default in every tool, unless you ask for them |
A/B Testing labels saved as a, regression doubled on Regression rows |
Labels are repaired at load time. The source CSV is never edited |
| About 190 tokens per full row | Responses are capped at 100 compact rows or 25 full rows, with pagination |
Users type ab testing, sdk, safari, ios |
A resolver maps loose input to exact values and answers typos with "Did you mean 'Reports'?" |
flowchart LR
C["MCP client<br/>Inspector, Claude Code, VS Code"] --> T{"stdio or<br/>stateless HTTP"}
T --> TL["21 tools<br/>FastMCP 4.1"]
TL --> RS["Resolver<br/>'ab' -> A/B Testing"]
RS --> RP["Repository<br/>5,000 rows in memory"]
CSV["vwo_5000_test_cases.csv<br/>read-only"] --> RP
OV["tc_additions.csv<br/>overlay for writes"] --> RP
RP --> RK["Ranking<br/>priority + status + type<br/>minus feature penalty"]
RK --> O["Ranked rows<br/>with rank_reason"]
classDef src fill:#57606a,stroke:#24292f,color:#fff
classDef ai fill:#1f6feb,stroke:#0b3d91,color:#fff
classDef gate fill:#bf8700,stroke:#7a5600,color:#fff
classDef out fill:#2da44e,stroke:#0f5323,color:#fff
class C,CSV,OV src
class TL,RP ai
class T,RS,RK gate
class O out
A tool is a plain Python function. FastMCP turns the type hints into the JSON schema the client sees, and the docstring into the description the LLM uses to choose the tool:
# src/tc_mcp/tools/search.py (trimmed)
@mcp.tool(annotations=READ_ONLY, tags={"search", "planning"})
def get_top_tests_for_module(
module: Annotated[str, Field(description="Module name, e.g. 'Reports', 'A/B Testing'. Aliases like 'ab', 'sdk' work.")],
count: Annotated[int, Field(ge=1, le=50, description="How many tests to return (1-50).")] = 10,
browser: Annotated[str | None, Field(description="Only this browser, e.g. 'Safari 19'.")] = None,
) -> dict[str, Any]:
"""Answer 'which tests should I run first for <module>?'. Returns a ranked shortlist,
each row with a score and a rank_reason."""
r = repo()
resolved = resolve_module(module, r.modules)
flt = TestFilter(module=resolved, browser=browser, status=["Ready", "Automated"], include_duplicates=False)
rows, applied = r.filter(flt)
picks = ranking.rank(rows, count, unique_scenarios=True)
return {"module": resolved, "results": [p.as_dict() for p in picks]}
The 21 tools
| Group | Tools |
|---|---|
| Discovery | list_modules, get_filter_options, get_test_stats |
| Find | get_test_case, search_test_cases, search_by_keyword, get_top_tests_for_module, get_similar_test_cases |
| Planning | build_test_suite, get_browser_device_matrix, get_automation_candidates, estimate_execution_effort |
| Quality | find_duplicate_tests, find_coverage_gaps, validate_test_case |
| Authoring | get_test_case_template, add_test_cases, update_test_case |
| Export | export_test_cases, convert_to_gherkin, generate_automation_stub |
There are also 4 resources (tc://schema, tc://modules, tc://stats/summary,
tc://test/{issue_key}) and 3 prompts (generate_test_cases, plan_release_run,
review_module_coverage). The two write tools are hidden unless the server starts with
--allow-write, and even then they default to dry_run=true.
Connect from any MCP client
| Client | Connection |
|---|---|
| MCP Inspector | npx @modelcontextprotocol/inspector --transport http --server-url http://127.0.0.1:8000/mcp |
| Claude Code | claude mcp add tc-mcp -- uv run --directory /ABS/PATH/chapter_14_MCP_Create_VIBE/testcase-creator-mcp tc-mcp |
VS Code (.vscode/mcp.json) |
{ "servers": { "tc-mcp": { "type": "http", "url": "http://127.0.0.1:8000/mcp" } } } |
| No clone needed | uvx --from "git+https://github.com/PramodDutta/AITesterBlueprint4x#subdirectory=chapter_14_MCP_Create_VIBE/testcase-creator-mcp" tc-mcp |
Calling it from Python, with output from the real data:
import asyncio
from fastmcp import Client
async def main():
async with Client("http://127.0.0.1:8000/mcp") as client:
top = await client.call_tool("get_top_tests_for_module", {"module": "Reports", "count": 3})
for row in top.data["results"]:
print(row["key"], row["rank_reason"])
asyncio.run(main())
# VWO-1029 Highest · Ready · Functional (score 56); 1st pick for feature 'scheduled email'
# VWO-3012 Highest · Ready · Functional (score 56); 1st pick for feature 'segment report'
# VWO-4898 Highest · Ready · Functional (score 56); 1st pick for feature 'report export'
The chapter README has every other option: Claude Desktop, Cursor, and a shared server with a bearer token.
Sharing it with students over a tunnel
A Cloudflare quick tunnel gives the local server a public HTTPS URL for as long as it runs:
uv run tc-mcp --transport http --port 8000 # read-only by default
cloudflared tunnel --url http://127.0.0.1:8000 # prints https://<random>.trycloudflare.com
In class, 14 students connected with MCP clients, and about 20 opened the link in a browser
and got Bad Request: Missing session ID. An MCP URL is not a web page: a browser sends a
plain GET, which the MCP transport rejects. There were two fixes. HTTP mode now runs
stateless (the tools keep no per-user state, so no session ID is needed), and a small ASGI
middleware (landing.py)
shows browsers a "how to connect" page. Stopping the tunnel takes the URL offline immediately.
Q&A - why use this?
- Q: Why not just give the LLM the CSV? A: 5,000 rows is about 950k tokens. Through tools, a question like "top 5 for Reports on Safari" costs one call and about 1k tokens, and counts come from code instead of the model's guess.
- Q: Why does the server never call an LLM? A: The client's model already reasons and writes. Keeping the server deterministic means it is free, works offline, needs no API keys to share, and gives the same answer every time.
- Q: What's the gotcha? A: Duplicates. Sorting 5,000 rows by priority gives you the same scenario on four browsers. Profile the data before designing tools: the 1,520 unique scenarios, not the 5,000 rows, are what the ranking has to work with.
TestSkill Finder MCP: 100 QA skills for any LLM

Concept: TestSkill Finder MCP serves a folder of 100 agent skills (each a SKILL.md with
YAML frontmatter and instructions) to any MCP client. It searches them by keyword or by a
plain-English task, chains them across the STLC, and exposes every skill as a prompt and a
resource. The skills/ folder is
the source of truth, and the server reloads whenever it changes.
Why: a skill library only helps if people can find the right skill at the moment they
need it, from whatever assistant they use. "Test case creator" should land on
test-case-writer even though no word matches exactly.
cd chapter_14_MCP_Create_VIBE/testskill-finder-mcp
uv sync && uv run pytest -q # 42 tests
uv run testskill-finder-mcp --transport http # http://127.0.0.1:8110/mcp
npx @modelcontextprotocol/inspector --transport http --server-url http://127.0.0.1:8110/mcp
Where the 100 skills come from:
- 36 are synced from PramodDutta/skillmasterclass by the server's own
tsf-mcp sync: 14 STLC skills, 11 Playwright and 11 Selenium. - 64 were added in the same format: 18 more STLC skills, plus packs for Cypress (8), API (6), performance (6), security (6), LLM evaluation (7), AI agents (5), MCP (5) and automation (3).
- The full list is in
CATALOG.md.

flowchart LR
GH["skillmasterclass<br/>36 upstream skills"] -->|tsf-mcp sync| SK["skills/ folder<br/>100 SKILL.md"]
NEW["64 new skills<br/>same format"] --> SK
SK --> CAT["Catalog<br/>parse + auto-reload"]
CAT --> IDX["Search index<br/>stems, synonyms, field weights"]
IDX --> T["15 tools"]
CAT --> P["103 prompts<br/>one per skill"]
CAT --> R["Resources<br/>skill://name"]
T --> LLM["Any LLM<br/>Claude, Copilot, Cursor"]
P --> LLM
R --> LLM
classDef src fill:#57606a,stroke:#24292f,color:#fff
classDef ai fill:#1f6feb,stroke:#0b3d91,color:#fff
classDef gate fill:#bf8700,stroke:#7a5600,color:#fff
classDef out fill:#2da44e,stroke:#0f5323,color:#fff
class GH,NEW src
class T,P,R ai
class SK,CAT,IDX gate
class LLM out
| Group | Tools |
|---|---|
| Discover | list_categories, list_skills, get_catalog_stats |
| Find | search_skills, find_skill_for_task, get_skill, get_skill_file, get_related_skills, compare_skills |
| Workflow | suggest_skill_chain, get_stlc_pipeline, export_skill (Claude Code, Copilot, Cursor) |
| Quality and write | validate_skills, plus create_skill and sync_skills_from_github when started with --allow-write |
Asking from Python, with output from the live server:
import asyncio
from fastmcp import Client
async def main():
async with Client("http://127.0.0.1:8110/mcp") as client:
hits = await client.call_tool("find_skill_for_task", {"task": "evaluate my RAG chatbot answers with DeepEval", "top_k": 2})
for row in hits.data["results"]:
print(row["rank"], row["name"], "|", row["category"])
asyncio.run(main())
# 1 rag-evaluation-designer | LLM Evaluation
# 2 deepeval-test-writer | LLM Evaluation
How skill search works
| Query | Top result | Why it matches |
|---|---|---|
| Playwright API | pw-api-tester |
pw is expanded to playwright, and both words are in the name |
| test case creator | test-case-writer |
creator, writer and generator are one synonym group after stemming |
| test plan creator | test-plan-generator |
the same synonym group, plus "test plan" in the name |
| test strategy | test-strategy-designer |
exact name match, which weighs 6x the body |
| k6 load test | k6-load-test-generator |
k6 and load are in the performance synonym group |
Each skill is scored as term weight x IDF x field weight. The field weights are:
| Field | Weight |
|---|---|
| Name | 6 |
| Title | 4 |
| Trigger phrases | 3 |
| Category | 3 |
| Description | 2 |
| When to use | 1.5 |
| Body | 0.5 |
That score is then scaled by how many of your own words matched. find_skill_for_task also
compares your sentence with each skill's quoted "Use when ..." phrases. The 17 golden queries
above are pinned in tests, so a change to the scoring that breaks them fails the build.
Q&A - why use this?
- Q: Why an MCP server instead of copying skills into
~/.claude/skills? A: Installed skills live on one machine and work with one agent. The server gives one searchable catalog to Claude, Copilot, Cursor or any LLM.export_skillstill produces the folder, Copilot prompt file or Cursor rule when you want a skill installed. - Q: How do I add my own skill? A: Drop a folder with a
SKILL.mdinto the right phase orframework-packs/<pack>-pack/and runuv run tsf-mcp validate, or callcreate_skillon a server started with--allow-write. The server picks it up without a restart, and the skill gets its own prompt. - Q: What's the gotcha? A: Upstream skills don't all share one exact layout (
test-plan-generatorwrites "Workflow (follow in order)" with### 1.steps), so the parser matches headings by prefix. Sync also downloads files from the internet: it only writes insideskills/, refuses path traversal, never touches local skills, and never runs the scripts it downloads.
Files in this chapter
| File | What it is |
|---|---|
00_Objective.md |
The goal: tests by priority, module, min/max limits, top tests per module |
01_TestCreatorMCP.plan.md |
Data profile, the 21-tool catalog, ranking formula, sharing options, phases |
02_prompt.md |
Every prompt used to build both servers, in order, with what happened |
data/vwo_5000_test_cases.csv |
The catalog: 5,000 tests, 17 modules, 72 features |
testcase-creator-mcp/ |
The FastMCP server: src/tc_mcp/ (tools, repository, ranking, validation) and tests/ |
learnings/2026-10-10-csv-catalog-to-mcp-server.md |
The approach and judgment calls, written down for reuse |
testskill-finder-mcp/ |
The second server: src/tsf_mcp/ (catalog, search, sync, tools, prompts) and tests/ |
testskill-finder-mcp/skills/ |
The source of truth: 100 skills across 7 STLC phases and 10 packs, plus CATALOG.md |
assets/ |
Three hand-drawn images rendered with Codex, with the briefs used to render them |
learnings/2026-10-10-skill-library-mcp.md |
How the skill finder was built: parallel writers, golden queries, sync safety |
Chapter 15: Python and PyTest
Concept: Python foundations for testers, as small numbered scripts you run one by one: printing, comments, keywords, identifiers, literals, variables, dynamic typing and simple math, before moving on to PyTest.
Why: everything from here on (FastMCP in chapter 14, CrewAI, LangChain, LangGraph, DeepEval) is Python, so the basics have to be automatic first.
cd chapter_15_Python_PyTest
python3 ex_01_Python_Basics/003_PrintCommand.py
The exercises so far
| Folder | Scripts | Covers |
|---|---|---|
ex_01_Python_Basics/ |
001-003 | print(), comments, sep and end |
ex_02_Keywords_Identifier_Literals/ |
004-015 | keywords (keyword.kwlist), identifiers, variables, type(), dynamic typing, complex numbers, multiple assignment, operator precedence (BODMAS), max()/min(), string + int |
flowchart LR
A["ex_01<br/>print, comments,<br/>sep and end"] --> B["ex_02<br/>keywords, identifiers,<br/>literals, variables"]
B --> C["Dynamic typing<br/>type()"]
C --> D["Next: PyTest"]
classDef src fill:#57606a,stroke:#24292f,color:#fff
classDef ai fill:#1f6feb,stroke:#0b3d91,color:#fff
classDef out fill:#2da44e,stroke:#0f5323,color:#fff
class A,B src
class C ai
class D out
Three ideas from the scripts, with the output they really print:
# 003_PrintCommand.py: sep joins the values, end replaces the newline
print("Hello", "World", "TheTestingAcademy", sep="*") # Hello*World*TheTestingAcademy
print("*", "*", sep="-", end="\t") # *-* then a tab, no newline
print("*", "*", sep="_") # *_* on the same line
# 008_Dynamic_Tye.py: a name can point to any type; type() shows the current one
age = 98
print(type(age)) # <class 'int'>
age = "Pramod"
print(type(age)) # <class 'str'>
# 015_IQ.py: str + int is an error, convert first
# print("Hello" + 15) # TypeError: can only concatenate str (not "int") to str
print("Hello" + str(15)) # Hello15
013_MathIQ.py ends with "PRAMOD" + 10 + 20 on purpose, to show that TypeError.
Identifier rules
00_RULE.md covers the
7 rules plus PEP 8 naming:
- Use only letters, digits and
_, and never start with a digit. - A name can't be a keyword, and names are case sensitive.
- There's no length limit, and
_on its own is a valid name. - Don't shadow built-ins such as
list. - Use
snake_casefor variables,UPPER_SNAKE_CASEfor constants andPascalCasefor classes.
Q&A - why use this?
- Q: Why can
agehold 98 and then "Pramod"? A: Python is dynamically typed. A variable is just a name for the last value assigned, andtype()tells you what it holds right now. - Q: How do I know if a name is allowed? A: Letters, digits and underscore, not starting with a digit, and not a keyword.
keyword.iskeyword("class")returnsTrue, soclass_nameis the usual workaround. - Q: What's the gotcha? A: Python never converts types for you in
+."PRAMOD" + 10raisesTypeError, so convert withstr()first. Naming a variablelistalso silently breakslist()for the rest of the file.
Chapters 16-21: coming next
Chapters 13-15 have landed (above). The other folders are in place, each holding a .gitkeep
until its content lands. After RAG, the course moves to giving LLMs tools (MCP), then the agent frameworks, then measuring whether any
of it works.
flowchart LR
RAG["Ch 10-12<br/>RAG"] --> MCP["Ch 13-14<br/>MCP"]
MCP --> PY["Ch 15<br/>Python + PyTest"]
PY --> AG["Ch 16-18<br/>Agent frameworks"]
AG --> EV["Ch 19-21<br/>LLM evaluation"]
classDef src fill:#57606a,stroke:#24292f,color:#fff
classDef ai fill:#1f6feb,stroke:#0b3d91,color:#fff
classDef gate fill:#bf8700,stroke:#7a5600,color:#fff
classDef out fill:#2da44e,stroke:#0f5323,color:#fff
class RAG src
class MCP,AG ai
class PY gate
class EV out
| Chapter | Topic | Folder |
|---|---|---|
| 16 | AI Agents with CrewAI | chapter_16_AI_Agent_Crew_AI/ |
| 17 | AI Agents with LangChain | chapter_17_AI_Agent_LangChain/ |
| 18 | AI Agents with LangGraph | chapter_18_AI_Agent_LangGraph/ |
| 19 | LLM Evaluation | chapter_19_LLM_Eval/ |
| 20 | DeepEval Basics | chapter_20_DeepEval_Basics/ |
| 21 | DeepEval Framework | chapter_21_DeepEval_Framework/ |
License
MIT
README fetched from the repository 2 days ago. The original is authoritative.