<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Любовь Авдеева</title>
    <description>The latest articles on DEV Community by Любовь Авдеева (@strelok25dev).</description>
    <link>https://dev.to/strelok25dev</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4092083%2F44ad8a34-c4f0-4c56-b651-08be195fe431.jpg</url>
      <title>DEV Community: Любовь Авдеева</title>
      <link>https://dev.to/strelok25dev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/strelok25dev"/>
    <language>en</language>
    <item>
      <title>Parsing financial PDFs without chasing 100% accuracy</title>
      <dc:creator>Любовь Авдеева</dc:creator>
      <pubDate>Tue, 01 Sep 2026 07:47:02 +0000</pubDate>
      <link>https://dev.to/strelok25dev/parsing-financial-pdfs-without-chasing-100-accuracy-2dn</link>
      <guid>https://dev.to/strelok25dev/parsing-financial-pdfs-without-chasing-100-accuracy-2dn</guid>
      <description>&lt;p&gt;We needed a reliable way to pull structure out of financial PDFs: text, tables, page numbers, and bounding boxes. Not “roughly understand the document” — a checkable artifact we could index and query later.&lt;/p&gt;

&lt;p&gt;This is not a tutorial on Docling. It’s a short write-up of four problems we actually hit, the options we considered, and why we chose the path we did.&lt;/p&gt;

&lt;h2&gt;
  
  
  The goal
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Input:&lt;/strong&gt; a PDF&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Output:&lt;/strong&gt; a structured object with text chunks, tables, multi-page table groups, and provenance (&lt;code&gt;page_no&lt;/code&gt;, &lt;code&gt;bbox&lt;/code&gt;) for every element. Plus a strict schema so nothing silently drifts downstream.&lt;/p&gt;

&lt;p&gt;Stack for this stage: Docling for layout-aware parsing, Pydantic for the schema, tests from day one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem 1. One form type “lost” its tables
&lt;/h2&gt;

&lt;p&gt;We fed in a dense regulatory form. The parser found &lt;strong&gt;one&lt;/strong&gt; table. The other three were flattened into text items — stock name, date, transaction code, quantity — each as a separate &lt;code&gt;TextItem&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Options we considered:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Custom coordinate clustering&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Rebuild rows and columns from X/Y positions ourselves.&lt;br&gt;&lt;br&gt;
Pros: we might recover all four tables.&lt;br&gt;&lt;br&gt;
Cons: several days of work that mostly reimplements what a layout parser already tries to do — for one awkward form type.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ask an LLM to reconstruct the table&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Pros: sometimes it understands context.&lt;br&gt;&lt;br&gt;
Cons: non-deterministic, slow, expensive. Bad fit for a base layer that must be repeatable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Change the test document&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Treat this form as a known edge case, switch to a report where tables have clear borders, and move on.&lt;/p&gt;

&lt;p&gt;We took the third option. Not because those forms don’t matter, but because polishing one edge case for a week would block proving that the rest of the pipeline works on normal documents.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Takeaway:&lt;/strong&gt; prove the pipeline on standard data first. Special forms can wait.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem 2. Bounding-box validation was a silent &lt;code&gt;pass&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;In the schema, if &lt;code&gt;left &amp;gt; right&lt;/code&gt; or &lt;code&gt;top &amp;gt; bottom&lt;/code&gt;, the code just did &lt;code&gt;pass&lt;/code&gt;. Classic “I’ll fix it later.” In production that means garbage coordinates flow downstream without anyone noticing.&lt;/p&gt;

&lt;p&gt;Options:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Leave it — fast, unsafe
&lt;/li&gt;
&lt;li&gt;Raise &lt;code&gt;ValueError&lt;/code&gt; and fail the whole document — too aggressive; one bad block kills the file
&lt;/li&gt;
&lt;li&gt;Normalize: swap the bounds when they’re inverted&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We normalized. If a table is truly upside-down in a weird way, we might miss it. But we don’t drop the document, and we keep a clear invariant: &lt;code&gt;left &amp;lt; right&lt;/code&gt;, &lt;code&gt;top &amp;lt; bottom&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Takeaway:&lt;/strong&gt; in a strict schema, never leave a silent &lt;code&gt;pass&lt;/code&gt;. Either fix the data with an explicit rule or fail loudly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem 3. Money normalization was wrong
&lt;/h2&gt;

&lt;p&gt;Values like &lt;code&gt;($1,250)&lt;/code&gt; were supposed to become negative numbers. We got things like &lt;code&gt;-$1250&lt;/code&gt; — the dollar sign stayed. Tests that expected a clean number failed.&lt;/p&gt;

&lt;p&gt;Root cause: we stripped parentheses first, then commas, and never touched the currency symbol. Order of operations mattered more than “what looks logical.”&lt;/p&gt;

&lt;p&gt;Working approach:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Strip everything that isn’t a digit or a dot
&lt;/li&gt;
&lt;li&gt;Look at the &lt;strong&gt;original&lt;/strong&gt; string to decide if parentheses mean “negative”
&lt;/li&gt;
&lt;li&gt;If cleaning leaves an empty string, don’t invent a number — keep the original or mark it&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Takeaway:&lt;/strong&gt; normalization is a contract. Define what counts as a number, what happens to junk, and in which order rules apply.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem 4. Table-grouping heuristic was too soft
&lt;/h2&gt;

&lt;p&gt;We wanted to group multi-page tables: nearby pages, similar headers, same section. The first version used a soft check — “if a piece of one header appears inside the other.” In practice &lt;code&gt;"Net"&lt;/code&gt; happily merged with &lt;code&gt;"Net Income"&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Options:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Soft substring match — many merges, including wrong ones
&lt;/li&gt;
&lt;li&gt;Strict header equality — fewer groups, each one explainable
&lt;/li&gt;
&lt;li&gt;Similarity score (e.g. Jaccard) with a threshold — flexible, but harder to tune and debug&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For the first version we chose &lt;strong&gt;strict equality&lt;/strong&gt;. Tables marked “continued” might not merge. That’s fine. We preferred a controlled miss over silent false merges.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Takeaway:&lt;/strong&gt; for an MVP, a strict heuristic with known limits beats a clever one that fails quietly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bonus debate: “another model found 61 tables, we found 184”
&lt;/h2&gt;

&lt;p&gt;Someone ran the same files through a different stack and got a smaller table count. The temptation was to add filters until our number looked “nicer.”&lt;/p&gt;

&lt;p&gt;We didn’t. Different tools use different definitions of “table.” Our parser often treats every visual grid as a table. Some of that is noise; some is useful context. Trimming everything for a pretty metric means losing data and weakening auditability.&lt;/p&gt;

&lt;p&gt;We documented the behavior instead: this is what the parser returns; these are the limits; the system has to live with that volume.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we locked in
&lt;/h2&gt;

&lt;p&gt;By the end of this stage we had:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Deterministic parse output with provenance (page, bbox, source ref)
&lt;/li&gt;
&lt;li&gt;A strict schema with no silent holes
&lt;/li&gt;
&lt;li&gt;A conservative table-grouping rule for the MVP
&lt;/li&gt;
&lt;li&gt;Tests for schema, money normalization, and grouping
&lt;/li&gt;
&lt;li&gt;An explicit list of known limits (collapsed multi-level headers, messy complex tables, occasional leftover currency symbols)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Principles we kept:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Don’t polish the parser forever for one form type
&lt;/li&gt;
&lt;li&gt;No silent &lt;code&gt;pass&lt;/code&gt; in data contracts
&lt;/li&gt;
&lt;li&gt;In normalization, clean first, then apply meaning
&lt;/li&gt;
&lt;li&gt;Prefer a strict heuristic over a “smart” one that lies
&lt;/li&gt;
&lt;li&gt;Don’t chase someone else’s table count — define what &lt;em&gt;you&lt;/em&gt; mean by a table and make it checkable&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Next step is indexing and comparing chunking strategies (text-only vs hybrid vs table-aware). None of that matters if the PDF parse underneath is sand.&lt;/p&gt;

&lt;p&gt;If you’ve parsed similar report-style PDFs: where did switching test data save you time, and where did you still have to write custom logic on top of a layout parser?&lt;/p&gt;

</description>
      <category>python</category>
      <category>pdf</category>
      <category>dataengineering</category>
      <category>pydantic</category>
    </item>
    <item>
      <title>How I Built a Reliable LLM Pipeline for Ad Creative Evaluation (with Strict Pydantic Contracts)</title>
      <dc:creator>Любовь Авдеева</dc:creator>
      <pubDate>Tue, 25 Aug 2026 14:02:13 +0000</pubDate>
      <link>https://dev.to/strelok25dev/how-i-built-a-reliable-llm-pipeline-for-ad-creative-evaluation-with-strict-pydantic-contracts-376j</link>
      <guid>https://dev.to/strelok25dev/how-i-built-a-reliable-llm-pipeline-for-ad-creative-evaluation-with-strict-pydantic-contracts-376j</guid>
      <description>&lt;p&gt;Art directors routinely spend 20–40 minutes per creative just checking brand guidelines, mandatory elements, and forbidden techniques.&lt;br&gt;&lt;br&gt;
I wanted to automate the first-pass review — without turning it into another unreliable ChatGPT wrapper.&lt;/p&gt;

&lt;p&gt;So I built &lt;strong&gt;CreativeAudit&lt;/strong&gt;: a production-oriented pipeline that evaluates ad creatives against a brief and returns a clear &lt;code&gt;PASS / NEEDS_REVISION / FAIL&lt;/code&gt; verdict.&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/strelok25-dev" rel="noopener noreferrer"&gt;
        strelok25-dev
      &lt;/a&gt; / &lt;a href="https://github.com/strelok25-dev/llm-creative-evaluator" rel="noopener noreferrer"&gt;
        llm-creative-evaluator
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Automated LLM pipeline for creative evaluation with Pydantic validation
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;CreativeAudit&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Automated ad creative evaluation against a brief using a local LLM.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;CreativeAudit takes a brief + creatives, builds a precise prompt, gets a structured evaluation from a language model, strictly validates the response, and returns a clear verdict (&lt;strong&gt;PASS / NEEDS_REVISION / FAIL&lt;/strong&gt;), weighted score, and explanation.&lt;/p&gt;
&lt;p&gt;This is not "just another AI chat". It is a production-oriented first-pass review pipeline: the machine catches routine and critical violations so humans only need to look at the borderline cases.&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Interface&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://github.com/strelok25-dev/llm-creative-evaluator/docs/screen_dashboard.jpg"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fstrelok25-dev%2Fllm-creative-evaluator%2FHEAD%2Fdocs%2Fscreen_dashboard.jpg" alt="CreativeAudit Dashboard"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Campaign summary metrics, creative cards with verdict, detailed scores and model explanation. Smart-input mode converts free-form text into structured JSON with human review before evaluation starts.&lt;/em&gt;&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;The Problem&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;Manual first-pass creative review is slow, expensive and inconsistent:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;An art director spends 20–40 minutes per creative checking mandatory elements, forbidden techniques and tone of voice.&lt;/li&gt;
&lt;li&gt;A campaign with 10–30 creatives turns into hours of work that does not scale.&lt;/li&gt;
&lt;li&gt;…&lt;/li&gt;
&lt;/ul&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/strelok25-dev/llm-creative-evaluator" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;h2&gt;
  
  
  The Real Problem with "Just Ask the LLM"
&lt;/h2&gt;

&lt;p&gt;A plain chat with an LLM has several fatal flaws for this use case:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Responses are inconsistent in format&lt;/li&gt;
&lt;li&gt;The model frequently forgets rules or hallucinates structure&lt;/li&gt;
&lt;li&gt;You get free-form text instead of machine-readable output&lt;/li&gt;
&lt;li&gt;Client data goes to an external cloud&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For any real workflow this is unacceptable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Design Goals
&lt;/h2&gt;

&lt;p&gt;I set a few hard requirements:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Strict output contracts&lt;/strong&gt; — invalid responses must never become scores&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Local inference&lt;/strong&gt; — no client data leaves the machine&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prompts as code&lt;/strong&gt; — versioned and editable independently of the app&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Binary compliance&lt;/strong&gt; for critical rules (a creative either violates the brand book or it doesn’t)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Human-in-the-loop&lt;/strong&gt; on free-form input&lt;/li&gt;
&lt;/ol&gt;

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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Brief + Creatives → Jinja2 Prompt → Local LLM (Ollama)
                                      ↓
                               Pydantic Validation
                                      ↓
                          Score + Verdict + Feedback

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Architecture diagram showing the flow from Brief and Creatives through Jinja2 Prompt and Ollama, followed by Pydantic Validation, resulting in Score, Verdict, and Feedback&lt;/p&gt;

&lt;p&gt;Key components:&lt;/p&gt;

&lt;p&gt;app/schemas.py — strict Pydantic v2 contracts&lt;/p&gt;

&lt;p&gt;prompts/*.j2 — prompts treated as versioned code&lt;/p&gt;

&lt;p&gt;app/main.py — orchestration (prompt assembly, LLM call, validation, scoring)&lt;/p&gt;

&lt;p&gt;demo/streamlit_app.py — thin UI layer&lt;/p&gt;

&lt;p&gt;The business logic is completely separated from the interface. You can call it from Streamlit, CLI, or any other service.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Engineering Decisions
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Pydantic as a Hard Contract&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The model is non-deterministic.&lt;br&gt;
 I treat the Pydantic schema as a hard boundary: if the response doesn’t match the schema, it is rejected as an error — it never becomes a fake score.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Binary Scale for Critical Compliance&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For brand-book violations I use a binary 0 or 10 score.&lt;br&gt;
 There is no “slightly violated”. This removes a lot of model subjectivity on the highest-risk criterion.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Prompts as Code (Jinja2)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Prompts live in separate .j2 files.&lt;br&gt;
 This makes them versionable, reviewable, and easy to A/B test without touching application code.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Local Inference with Ollama&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Everything runs locally.&lt;br&gt;
 Switching between qwen2.5:7b, qwen2.5:14b or llama3.2 is a single config change.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Smart Input Mode (Human-in-the-Loop)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Managers rarely provide clean JSON.&lt;br&gt;
 They paste chat fragments and rough descriptions.&lt;br&gt;
 So the pipeline first uses an LLM to extract structured data, shows the result to the user for correction, and only then runs the evaluation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scoring Model
&lt;/h2&gt;

&lt;p&gt;Each creative receives three scores:&lt;/p&gt;

&lt;p&gt;brand_alignment (1–10)&lt;/p&gt;

&lt;p&gt;constraint_compliance (0 or 10)&lt;/p&gt;

&lt;p&gt;message_clarity (1–10)&lt;/p&gt;

&lt;h2&gt;
  
  
  Final score is a weighted combination:
&lt;/h2&gt;

&lt;p&gt;total = brand × 0.4 + compliance × 0.3 + clarity × 0.3&lt;/p&gt;

&lt;p&gt;The verdict is derived from the total score and critical failures.&lt;/p&gt;

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

&lt;p&gt;I wrote 38 unit tests.&lt;br&gt;
 The external LLM is fully mocked, so tests are deterministic and run in under 2 seconds.&lt;br&gt;
 They cover schema boundaries, malformed responses, connection failures, and scoring logic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Results &amp;amp; Lessons
&lt;/h2&gt;

&lt;p&gt;The biggest wins:&lt;/p&gt;

&lt;p&gt;First-pass review time dropped from tens of minutes to seconds&lt;/p&gt;

&lt;p&gt;Output became consistent and machine-readable&lt;/p&gt;

&lt;p&gt;Critical brand violations are much harder to miss&lt;/p&gt;

&lt;p&gt;The hardest part wasn’t the LLM call — it was designing the contracts and failure modes so the system stays reliable when the model behaves badly.&lt;/p&gt;

&lt;h2&gt;
  
  
  What’s Next
&lt;/h2&gt;

&lt;p&gt;Multimodal support (evaluate the actual layouts, not only text)&lt;/p&gt;

&lt;p&gt;Model benchmarks (accuracy vs speed)&lt;/p&gt;

&lt;p&gt;REST API for integration into existing workflows&lt;/p&gt;

&lt;p&gt;Evaluation history and campaign analytics&lt;/p&gt;

&lt;p&gt;If you’re building production LLM pipelines, I’d love to hear how you handle structured output and hallucination control.&lt;/p&gt;

&lt;p&gt;How do you currently deal with unreliable LLM responses in your projects?&lt;/p&gt;

</description>
      <category>python</category>
      <category>llm</category>
      <category>pydantic</category>
      <category>ai</category>
    </item>
    <item>
      <title>AI Engineer. No fluff, just working products.</title>
      <dc:creator>Любовь Авдеева</dc:creator>
      <pubDate>Mon, 24 Aug 2026 11:08:37 +0000</pubDate>
      <link>https://dev.to/strelok25dev/image-2jfl</link>
      <guid>https://dev.to/strelok25dev/image-2jfl</guid>
      <description></description>
    </item>
  </channel>
</rss>
