<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Swapnanil Saha</title>
    <description>The latest articles on Forem by Swapnanil Saha (@swapnanilsaha).</description>
    <link>https://forem.com/swapnanilsaha</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3939906%2F9f37b94e-be6e-42b9-a63e-34b65dca3522.jpeg</url>
      <title>Forem: Swapnanil Saha</title>
      <link>https://forem.com/swapnanilsaha</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/swapnanilsaha"/>
    <language>en</language>
    <item>
      <title>India's DPDP Act 2023 Explained — And How AI Handles Data Principal Requests at Scale</title>
      <dc:creator>Swapnanil Saha</dc:creator>
      <pubDate>Thu, 21 May 2026 21:46:43 +0000</pubDate>
      <link>https://forem.com/swapnanilsaha/indias-dpdp-act-2023-explained-and-how-ai-handles-data-principal-requests-at-scale-38ib</link>
      <guid>https://forem.com/swapnanilsaha/indias-dpdp-act-2023-explained-and-how-ai-handles-data-principal-requests-at-scale-38ib</guid>
      <description>&lt;p&gt;&lt;em&gt;This post is for informational purposes only and does not constitute legal advice. The DPDP Act 2023 and its implementing Rules 2025 are relatively new — requirements may evolve through further notifications or guidance. Verify the current position with a qualified data protection lawyer before making compliance decisions.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;Your company just received this email:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"I would like to know all personal data your organisation holds about me. This is a formal request under the DPDP Act."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It lands in a shared &lt;code&gt;privacy@yourcompany.com&lt;/code&gt; inbox. Someone reads it. Forwards it to legal. Legal forwards it to engineering. Engineering says they need to check three databases. Nobody notes the date it arrived. Three weeks pass. When someone finally circles back, there are nine days left on the 30-day window the DPDP Rules require. Not enough time to locate the data, get legal sign-off, draft a response in the right language, and send it.&lt;/p&gt;

&lt;p&gt;On day 32, you're in violation.&lt;/p&gt;

&lt;p&gt;That scenario is the default for most Indian companies right now. Not because they're careless — but because nobody built the infrastructure for it.&lt;/p&gt;

&lt;p&gt;I built &lt;strong&gt;DPDP Copilot&lt;/strong&gt; to close that gap: a self-hosted operator tool that accepts public data requests, classifies them with Claude, drafts compliant multilingual replies, tracks every action as immutable evidence, and monitors SLA status in real time.&lt;/p&gt;

&lt;p&gt;But before the tool, you need to understand what you're actually dealing with. Let's start with the law.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://swapnanilsaha.com/tools/dpdp-copilot/" rel="noopener noreferrer"&gt;→ Full tool page and live demo&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 1: What the DPDP Act 2023 Actually Requires
&lt;/h2&gt;

&lt;p&gt;The Digital Personal Data Protection Act 2023 received presidential assent on 11 August 2023 and represents India's first comprehensive data protection legislation. Its structure borrows from GDPR while adapting to India's specific context — a 1.4 billion-person population, 22 scheduled languages, deep mobile penetration, and a digital public infrastructure layer (UPI, Aadhaar, DigiLocker) that most jurisdictions don't have.&lt;/p&gt;

&lt;p&gt;The implementing Rules — the Digital Personal Data Protection Rules 2025 — were notified on 13 November 2025, giving the Act its operational teeth.&lt;/p&gt;

&lt;p&gt;Here's what the law actually mandates, stripped of legalese, focusing on the parts most engineering and compliance teams get wrong.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Four Rights Every Data Principal Has
&lt;/h3&gt;

&lt;p&gt;The Act grants every "data principal" — the person whose data is being processed — four actionable rights. When someone exercises any of these, your organisation (as the "data fiduciary") has a legal obligation to respond.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Right of Access (Section 11)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Any person can ask you: what personal data do you hold about me, and for what purpose? You must provide a summary of the data being processed, the processing activities, and the identities of any other data fiduciaries or processors with whom their data has been shared. The Act doesn't specify a format, but silence is not sufficient.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Right of Correction and Completion (Section 12(a))&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If a person believes data you hold is inaccurate, incomplete, or misleading, they can demand you correct or complete it. You must either act on the request or explain in writing why you're not.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Right of Erasure (Section 12(b))&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A person can request deletion of their personal data from your systems. There are exceptions — data held for legal obligations, fraud prevention, pending litigation — but these exceptions have to be documented and justified, not just asserted.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Right to Grievance Redressal (Section 13)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Any person can file a grievance if they believe their rights under the Act have been violated. You must provide a mechanism to receive and respond to grievances. The Rules 2025 specify this mechanism must be genuinely accessible.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Response Timelines
&lt;/h3&gt;

&lt;p&gt;The DPDP Rules 2025 (notified November 2025) set specific mandatory windows for responding to data principal requests:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Access, Correction, and Erasure requests (Sections 11–12)&lt;/strong&gt;: Data fiduciaries must respond within &lt;strong&gt;30 days&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Grievance Redressal (Section 13)&lt;/strong&gt;: Grievances must be resolved within a maximum of &lt;strong&gt;90 days&lt;/strong&gt; from receipt.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are calendar days. For reference: GDPR (EU) also requires responses within one month for most data subject requests; California's CCPA gives 45 days. India's framework is broadly comparable to GDPR in its demands — but applies at the scale of 1.4 billion people, across 22 scheduled languages. That's where the operational challenge is categorically harder.&lt;/p&gt;

&lt;p&gt;30 days sounds like a lot. For a company with no structured process, it evaporates fast. A request that lands in a shared inbox on a Friday, takes three days to be noticed, gets forwarded twice, waits a week for a legal review, and then requires manual drafting in the data principal's language — you're out of time before anyone writes the first sentence.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A note on the DPDP Copilot tool's SLA default&lt;/strong&gt;: The tool's internal SLA clock defaults to 7 days — intentionally more conservative than the 30-day legal window. Most mature compliance programmes target internal deadlines that are significantly tighter than the regulatory maximum, so that normal delays (review cycles, approvals, language checks) don't push you to the edge. The 7-day default is configurable via &lt;code&gt;orgs.sla_days&lt;/code&gt;. When the Rules are read by your legal team and a specific target is agreed, you set it once in the database.&lt;/p&gt;

&lt;h3&gt;
  
  
  What "Evidence" Actually Means Under DPDP
&lt;/h3&gt;

&lt;p&gt;The Act and the Rules create a documentation burden that most organisations underestimate. You need to be able to prove:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;That the request was received on a specific date&lt;/li&gt;
&lt;li&gt;That it was handled (classified and routed) in a timely manner&lt;/li&gt;
&lt;li&gt;What response you gave and when&lt;/li&gt;
&lt;li&gt;Whether the response fulfilled the request or why it couldn't&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is audit evidence. If the Data Protection Board investigates a complaint, you need to produce this trail. A forwarded email chain is not audit evidence. A Slack thread is not audit evidence. An append-only timestamped log — with the original message, the classification, the drafted response, and the send event — is audit evidence.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Financial Exposure
&lt;/h3&gt;

&lt;p&gt;The Act's First Schedule specifies penalties by category of failure. The two most operationally relevant:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;₹250 crore (~$30M USD)&lt;/strong&gt; — Failure to implement &lt;strong&gt;reasonable security safeguards&lt;/strong&gt; to prevent personal data breaches (Section 8(5)). This is the preventive obligation — having security measures in place. The penalty applies even where a breach subsequently occurs and the fiduciary claims they didn't anticipate it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;₹200 crore&lt;/strong&gt; — Failure to &lt;strong&gt;notify the Data Protection Board and affected data principals&lt;/strong&gt; when a personal data breach does occur (Section 8(6)). The notification obligation is separate from the security obligation — you can get penalised for both.&lt;/p&gt;

&lt;p&gt;Other penalty tiers: ₹200 crore for violations related to children's personal data (Section 9); ₹150 crore for Significant Data Fiduciary obligation failures; ₹50 crore for other provision breaches.&lt;/p&gt;

&lt;p&gt;The Data Protection Board, once fully constituted, will have adjudicatory powers to investigate and levy these penalties. Failing to acknowledge or respond to a data principal request, if that person escalates to the Board, creates a documented paper trail of non-compliance before any investigation begins.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 2: Why Your Current Process Fails (And Why That's the Default)
&lt;/h2&gt;

&lt;p&gt;Let me describe the most common setup I've seen when talking to Indian companies dealing with DPDP requests:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;code&gt;privacy@&lt;/code&gt; email address that gets checked sporadically&lt;/li&gt;
&lt;li&gt;No clock tracking — the 30-day window doesn't appear anywhere visible until it's almost gone&lt;/li&gt;
&lt;li&gt;No classification — the person who reads it decides manually whether it's an access request, deletion request, or complaint&lt;/li&gt;
&lt;li&gt;Reply drafted manually, from scratch, in English, by whoever processes it that week&lt;/li&gt;
&lt;li&gt;No audit trail beyond the email itself, which may be deleted if an inbox is cleaned&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This isn't negligence. It's the logical outcome of a process designed before the Act existed. The process was "email us with your concern" — and it worked fine when data requests were rare. The DPDP Act changes the legal weight of those requests, but most companies haven't updated their infrastructure to match.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Three Ways Manual Processes Break
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;1. The deadline blind spot&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When a request lands in an email inbox, the 30-day clock doesn't appear anywhere. Nobody stamps the receipt date. Nobody sends an automatic acknowledgement. The request sits until someone opens the inbox. If that takes a week — completely normal for a low-traffic shared inbox — you've already used 23% of your response window without touching the request. Legal review, data location, and drafting will eat most of what's left.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Classification inconsistency&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;"Please delete my data" is an erasure request. "I never gave you permission to use my data" is a grievance. "I want to update my phone number" is a correction request. "Can you send me everything you have on me" is an access request. A trained compliance professional can distinguish these consistently. Your Monday-morning on-call engineer who reads the shared inbox probably cannot — especially for requests written in Hindi, Bengali, or Tamil.&lt;/p&gt;

&lt;p&gt;When requests are misclassified, they get routed to the wrong person, get the wrong response template, and sometimes get the wrong legal treatment. An erasure request handled as a grievance will likely produce a response that doesn't fulfil the legal obligation under Section 12(b), even if it sounds polite.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Evidence that can't survive audit&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;An auditor asks: "On what date did you receive and process this erasure request?" If your answer is "let me check the email thread," you have a problem. Email is mutable, searchable by keyword but not by event type, and has no integrity guarantees. An auditor looking for "REQUEST_CREATED at timestamp T" followed by "REPLY_SENT at timestamp T+22 days" needs a structured log, not an inbox.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 3: The Role of AI in DPDP Compliance
&lt;/h2&gt;

&lt;p&gt;When I was designing DPDP Copilot, the central question was: &lt;strong&gt;where does AI actually add value, and where does it introduce risk?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;DPDP compliance has two types of tasks: tasks that require human judgment about legal gray areas, and tasks that require consistent application of known rules to varied inputs. AI is well-suited to the second category and badly suited to the first.&lt;/p&gt;

&lt;p&gt;Deciding whether your company has a legal obligation to retain data for a pending investigation? That's human judgment. Classifying an incoming message as an Access request vs. an Erasure request? That's pattern recognition on natural language — exactly what a well-prompted LLM is built for.&lt;/p&gt;

&lt;h3&gt;
  
  
  Classification: Where LLMs Outperform Rules
&lt;/h3&gt;

&lt;p&gt;Naive rule-based classification for DPDP requests fails quickly. "Please delete my account" is an erasure request. "I want my data removed from your marketing list" is also an erasure request but uses entirely different vocabulary. "Remove me" submitted in a support ticket might be an erasure request or might just be asking to be unsubscribed from emails — context determines which.&lt;/p&gt;

&lt;p&gt;A rules-based system that catches "delete my data" literally will miss most real-world submissions. People write in fragments, in their native language, with emotional context, in ways that don't follow a template.&lt;/p&gt;

&lt;p&gt;An LLM with a well-structured prompt classifies these correctly without needing exhaustive keyword lists. The DPDP Copilot classification prompt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;Classify&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;this&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;message&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;into&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;exactly&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;one&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;of:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Grievance,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Access,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Rectification,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Deletion.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;Respond&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;as&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;classification&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;Message:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="err"&gt;text&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The system prompt establishes the legal framework — "You are a DPDP compliance assistant classifying data principal requests under India's DPDP Act 2023." The model maps the message to the correct legal category.&lt;/p&gt;

&lt;p&gt;The output is constrained to a JSON object with a single key. The application validates that &lt;code&gt;type&lt;/code&gt; is one of the four legal categories. If the model returns something outside those four values, it's rejected and retried — the system never persists a classification it can't validate.&lt;/p&gt;

&lt;h3&gt;
  
  
  Multilingual Reply Drafting: Where AI Eliminates Weeks of Work
&lt;/h3&gt;

&lt;p&gt;This is where AI creates the most leverage in the Indian compliance context.&lt;/p&gt;

&lt;p&gt;India has 22 scheduled languages. The DPDP Act creates a right to grievance redressal — and for that mechanism to be genuinely accessible (which the Rules 2025 require), you need to respond in a language the person can understand.&lt;/p&gt;

&lt;p&gt;Without AI, producing compliant response templates in Hindi, Bengali, Tamil, and Marathi means hiring translators, reviewing legal language, maintaining version parity across languages, and updating all templates whenever requirements change. That's a significant operational cost — one that most companies defer indefinitely, defaulting to English-only responses that disadvantage non-English speakers.&lt;/p&gt;

&lt;p&gt;With a well-prompted LLM, drafting happens at response time. The model understands DPDP legal obligations and drafts a response that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Acknowledges the specific request type (not a generic "thank you for reaching out")&lt;/li&gt;
&lt;li&gt;Confirms receipt and logging with a reference number&lt;/li&gt;
&lt;li&gt;States the applicable response timeline&lt;/li&gt;
&lt;li&gt;Explains the next step the data principal should expect&lt;/li&gt;
&lt;li&gt;Is written in the language they chose&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The system prompt for drafting:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;You are a DPDP compliance officer drafting replies to data principal requests under 
India's Digital Personal Data Protection Act 2023. Write professional, empathetic 
replies that: acknowledge the request type, confirm receipt and logging, state the 
applicable response timeline, and explain the next step. Keep the tone formal but accessible.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The user message to the model specifies the request type and target language:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Draft a DPDP-compliant reply in ${language} for a ${type} request.

Customer message:
${text}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These are &lt;strong&gt;suggested&lt;/strong&gt; replies — an operator reviews them before sending. The human stays in the loop for all final communications.&lt;/p&gt;

&lt;h3&gt;
  
  
  Prompt Caching: Making AI Cost-Efficient at Scale
&lt;/h3&gt;

&lt;p&gt;The system prompts for both classification and drafting use &lt;code&gt;cache_control: { type: 'ephemeral' }&lt;/code&gt; via the Anthropic SDK, enabling prompt caching.&lt;/p&gt;

&lt;p&gt;If you're processing dozens of data principal requests per day, the system prompt — which is identical for every request — gets cached by Anthropic's API after the first call. Subsequent calls are billed at a fraction of the full input token cost. At scale, prompt caching reduces API costs by 50–80% for the classification and drafting steps.&lt;/p&gt;

&lt;p&gt;This is a small architectural detail that has no effect on the first request and compounding positive effect on the hundredth. If you're building compliance tooling that processes high volumes, prompt caching is the difference between a sustainable per-request cost and one that makes the tool impractical at production scale.&lt;/p&gt;

&lt;h3&gt;
  
  
  Retry Logic: Resilience Against Transient Failures
&lt;/h3&gt;

&lt;p&gt;The LLM calls use exponential backoff retry logic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;callWithRetry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;MAX_RETRIES&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;attempt&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isRetryable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
        &lt;span class="nx"&gt;err&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nx"&gt;Anthropic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;RateLimitError&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
        &lt;span class="nx"&gt;err&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nx"&gt;Anthropic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;InternalServerError&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;isRetryable&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;MAX_RETRIES&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Only rate limit errors and server errors trigger retries — not client errors (bad API key, invalid request format). The delay doubles with each attempt: 1 second, then 2, then 4. Three attempts total. A transient API hiccup doesn't fail the entire processing pipeline for a data principal's submission.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 4: DPDP Copilot — The Tool in Detail
&lt;/h2&gt;

&lt;p&gt;With the legal and AI context established, here's how DPDP Copilot works end to end.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Public Request Form
&lt;/h3&gt;

&lt;p&gt;The entry point for data principals is &lt;code&gt;/grievance&lt;/code&gt; — no login required. Requiring a login to submit a data rights request is a barrier that conflicts with the spirit of the Act. If someone can't easily submit an erasure request, the mechanism isn't truly accessible.&lt;/p&gt;

&lt;p&gt;The form collects:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The request message (free text — people write what they mean in their own words)&lt;/li&gt;
&lt;li&gt;Preferred response language (English, Hindi, Bengali, Tamil, Marathi)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There's no account creation, no verification code, no CAPTCHA wall. Data principals submit and receive an acknowledgement. The contact information is embedded in the message body — a known limitation of the current implementation, and a deliberate choice for the initial version: forcing a structured contact field requires more UI complexity and doesn't add meaningful compliance value until outbound email delivery is implemented.&lt;/p&gt;

&lt;h3&gt;
  
  
  What Happens in the Background on Submission
&lt;/h3&gt;

&lt;p&gt;When the form is submitted, a single API call to &lt;code&gt;POST /api/public/requests&lt;/code&gt; triggers a multi-step synchronous pipeline:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Request creation&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The system creates a database record with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A UUID as the request ID&lt;/li&gt;
&lt;li&gt;The raw message text&lt;/li&gt;
&lt;li&gt;The chosen language&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;type: 'PENDING'&lt;/code&gt; — not yet classified&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;sla_due_at: now() + 7 days&lt;/code&gt; — the internal SLA clock starts at submission. This 7-day default is configurable via &lt;code&gt;orgs.sla_days&lt;/code&gt; and is intentionally conservative relative to the 30-day legal window.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;org_id&lt;/code&gt; from the active organisation configuration&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Step 2: Evidence logging — REQUEST_CREATED&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;An &lt;code&gt;evidence_events&lt;/code&gt; record is written immediately after creation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"event_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"REQUEST_CREATED"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"event_data"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"public_form"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"language"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Hindi"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"created_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2025-05-25T10:00:00.000Z"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the legal timestamp of receipt. The moment the request hits the database, it's on record. The evidence log is append-only at the application level — there are no delete or update operations on &lt;code&gt;evidence_events&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3: AI classification&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The message text goes to Claude for classification. The model returns a JSON object. The application parses it and validates that &lt;code&gt;type&lt;/code&gt; is one of &lt;code&gt;{ Grievance, Access, Rectification, Deletion }&lt;/code&gt;. Any other value throws an error. The request record is updated with the validated type.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4: Evidence logging — REQUEST_CLASSIFIED&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"event_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"REQUEST_CLASSIFIED"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"event_data"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Deletion"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"created_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2025-05-25T10:00:01.342Z"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The classification result and timestamp are immutable facts in the evidence record from this point forward.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 5: AI reply drafting&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Claude drafts a response in the data principal's chosen language, using the classified request type and the original message as context.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 6: Evidence logging — REPLY_SUGGESTED&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"event_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"REPLY_SUGGESTED"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"event_data"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"language"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Hindi"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"model"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"claude-sonnet-4-6"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"created_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2025-05-25T10:00:02.891Z"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The entire pipeline — creation, classification, drafting — runs in under 5 seconds for a typical request. By the time an operator opens the inbox, the request is already classified, a draft reply exists, and the SLA clock has been running since submission.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Operator Inbox
&lt;/h3&gt;

&lt;p&gt;The inbox at &lt;code&gt;/&lt;/code&gt; is protected by authentication. It shows all requests for the active organisation, each with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Request type (Grievance, Access, Rectification, Deletion, or PENDING if classification failed)&lt;/li&gt;
&lt;li&gt;Message preview&lt;/li&gt;
&lt;li&gt;Live SLA status (Within SLA / Due Soon / Overdue)&lt;/li&gt;
&lt;li&gt;Creation timestamp&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The SLA status is computed at read time — not stored as a cached value. The &lt;code&gt;computeSlaStatus&lt;/code&gt; function runs on every page load:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;computeSlaStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;slaDueAt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;due&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;slaDueAt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;diffHours&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;due&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;diffHours&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;OVERDUE&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;diffHours&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;DUE_SOON&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;WITHIN_SLA&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The status shown in the inbox reflects the current moment — not the status at the last time the record was updated. A request that was &lt;code&gt;WITHIN_SLA&lt;/code&gt; yesterday is automatically &lt;code&gt;DUE_SOON&lt;/code&gt; or &lt;code&gt;OVERDUE&lt;/code&gt; today without any scheduled job or background worker.&lt;/p&gt;

&lt;p&gt;The inbox is sorted by SLA urgency by default, so operators see the most at-risk requests first.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Request Detail Page
&lt;/h3&gt;

&lt;p&gt;Clicking into any request shows everything an operator needs to review, respond, and close:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The original message&lt;/strong&gt; — verbatim, exactly as submitted. No interpretation layer between the operator and what the data principal actually wrote.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The AI-drafted reply&lt;/strong&gt; — pre-populated with DPDP-compliant language in the data principal's chosen language. The operator can read it, edit it in the text area, and send it. The draft is a starting point, not a cage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The resolution checklist&lt;/strong&gt; — structured prompts for the operator to work through before closing the request:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Has the relevant data been located?&lt;/li&gt;
&lt;li&gt;Has the requested action (access/correction/deletion) been taken?&lt;/li&gt;
&lt;li&gt;Has the data principal been notified?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The evidence timeline&lt;/strong&gt; — every event in chronological order with timestamps, event types, and metadata.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The export controls&lt;/strong&gt; — one click to download the full evidence trail as PDF or CSV.&lt;/p&gt;

&lt;h3&gt;
  
  
  Marking a Reply as Sent
&lt;/h3&gt;

&lt;p&gt;When an operator sends the response (currently: manually via email or another channel, then clicks "Mark as Sent" in the tool), the system:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Updates the request &lt;code&gt;status&lt;/code&gt; to &lt;code&gt;CLOSED&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Logs &lt;code&gt;REPLY_SENT&lt;/code&gt; to the evidence table:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
     &lt;/span&gt;&lt;span class="nl"&gt;"event_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"REPLY_SENT"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
     &lt;/span&gt;&lt;span class="nl"&gt;"event_data"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"operator"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"admin"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"channel"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"manual"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
     &lt;/span&gt;&lt;span class="nl"&gt;"created_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2025-05-27T14:22:00.000Z"&lt;/span&gt;&lt;span class="w"&gt;
   &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The gap between &lt;code&gt;REQUEST_CREATED&lt;/code&gt; and &lt;code&gt;REPLY_SENT&lt;/code&gt; timestamps is the documented response time. If an auditor asks "how long did you take to respond to this erasure request?" — the answer is computable from the evidence log to the second.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 5: The Evidence Architecture
&lt;/h2&gt;

&lt;p&gt;The evidence design is the most important part of DPDP Copilot from a compliance standpoint. Everything else is workflow tooling. The evidence log is what you use when the Data Protection Board comes calling.&lt;/p&gt;

&lt;h3&gt;
  
  
  Append-Only by Design
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;evidence_events&lt;/code&gt; table has no update or delete paths in the application. Once an event is written, it stays. There's no "edit evidence" API, no admin panel for removing events, no soft-delete flag.&lt;/p&gt;

&lt;p&gt;Audit evidence that can be modified isn't evidence; it's a story you're telling. An append-only log where every event has a database-generated timestamp (not an application-provided one) is as close to tamper-evident as you can get in a PostgreSQL-backed application.&lt;/p&gt;

&lt;p&gt;The schema:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;evidence_events&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt;          &lt;span class="n"&gt;uuid&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;request_id&lt;/span&gt;  &lt;span class="n"&gt;uuid&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;event_type&lt;/span&gt;  &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;event_data&lt;/span&gt;  &lt;span class="n"&gt;jsonb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt;  &lt;span class="n"&gt;timestamptz&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;org_id&lt;/span&gt;      &lt;span class="n"&gt;uuid&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;orgs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;created_at&lt;/code&gt; field uses &lt;code&gt;DEFAULT now()&lt;/code&gt; — the database server's timestamp, not the application's &lt;code&gt;Date.now()&lt;/code&gt;. Database server clocks in a managed PostgreSQL instance are NTP-synchronized and authoritative. Application clocks can drift.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Four Event Types
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;REQUEST_CREATED&lt;/strong&gt; — logged at the moment of database insertion, before any processing. This is the legal timestamp of receipt.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;REQUEST_CLASSIFIED&lt;/strong&gt; — logged immediately after the AI classification succeeds and the type is validated. Contains the classified type in &lt;code&gt;event_data&lt;/code&gt;. If classification fails and retries are exhausted, this event is not logged — the absence of this event tells you classification failed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;REPLY_SUGGESTED&lt;/strong&gt; — logged when the AI draft is written to the request record. Contains the language and model used.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;REPLY_SENT&lt;/strong&gt; — logged when an operator marks the reply as sent. Contains the operator identity and channel. This closes the request lifecycle in the evidence log.&lt;/p&gt;

&lt;p&gt;The presence of all four events, in order, within the applicable window means the request was handled correctly from intake to response. An auditor reviewing the CSV export can verify this in seconds.&lt;/p&gt;

&lt;h3&gt;
  
  
  Organisation Scoping
&lt;/h3&gt;

&lt;p&gt;Every evidence event carries an &lt;code&gt;org_id&lt;/code&gt;. Every query on &lt;code&gt;evidence_events&lt;/code&gt; is scoped to the active organisation. A single deployment can serve multiple organisations, and their evidence trails are strictly isolated.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;org_id&lt;/code&gt; in evidence events is written by the application using the resolved organisation context — not passed in by the caller. A data principal submitting a request cannot specify or forge the organisation context; it's resolved server-side from the environment configuration.&lt;/p&gt;

&lt;h3&gt;
  
  
  What the Export Looks Like
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;CSV export&lt;/strong&gt; for a complete request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csvs"&gt;&lt;code&gt;&lt;span class="k"&gt;event&lt;/span&gt;&lt;span class="err"&gt;_&lt;/span&gt;&lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;created&lt;/span&gt;&lt;span class="err"&gt;_&lt;/span&gt;&lt;span class="k"&gt;at&lt;/span&gt;
&lt;span class="k"&gt;REQUEST&lt;/span&gt;&lt;span class="err"&gt;_&lt;/span&gt;&lt;span class="k"&gt;CREATED&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="ld"&gt;2025-05-25T10:00:00&lt;/span&gt;&lt;span class="mf"&gt;.000&lt;/span&gt;&lt;span class="k"&gt;Z&lt;/span&gt;
&lt;span class="k"&gt;REQUEST&lt;/span&gt;&lt;span class="err"&gt;_&lt;/span&gt;&lt;span class="k"&gt;CLASSIFIED&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="ld"&gt;2025-05-25T10:00:01&lt;/span&gt;&lt;span class="mf"&gt;.342&lt;/span&gt;&lt;span class="k"&gt;Z&lt;/span&gt;
&lt;span class="k"&gt;REPLY&lt;/span&gt;&lt;span class="err"&gt;_&lt;/span&gt;&lt;span class="k"&gt;SUGGESTED&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="ld"&gt;2025-05-25T10:00:02&lt;/span&gt;&lt;span class="mf"&gt;.891&lt;/span&gt;&lt;span class="k"&gt;Z&lt;/span&gt;
&lt;span class="k"&gt;REPLY&lt;/span&gt;&lt;span class="err"&gt;_&lt;/span&gt;&lt;span class="k"&gt;SENT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="ld"&gt;2025-05-27T14:22:00&lt;/span&gt;&lt;span class="mf"&gt;.000&lt;/span&gt;&lt;span class="k"&gt;Z&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four rows. Auditor reads it: request received Sunday 10:00 AM, responded Tuesday 2:22 PM — 52 hours, well within any reasonable response window.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PDF export&lt;/strong&gt; includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Organisation name and request ID&lt;/li&gt;
&lt;li&gt;Request type and creation timestamp&lt;/li&gt;
&lt;li&gt;Original message (verbatim)&lt;/li&gt;
&lt;li&gt;Suggested reply (the draft that was reviewed and sent)&lt;/li&gt;
&lt;li&gt;Full evidence timeline&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The PDF is generated server-side using Puppeteer with Chromium. The HTML template is a known XSS risk in the current implementation (user-provided message text is interpolated directly into HTML) — the fix is explicit HTML escaping before interpolation, which is on the roadmap.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 6: SLA Architecture — The Compliance Clock
&lt;/h2&gt;

&lt;p&gt;SLA management is where most compliance tools fail. They either track SLA status as a static database field (which becomes stale the moment the clock ticks past the deadline) or they rely on background jobs (which can fail silently and leave the status indicator wrong).&lt;/p&gt;

&lt;p&gt;DPDP Copilot takes a third approach: &lt;strong&gt;compute SLA status at read time, every time.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  How the Internal SLA Clock Works
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;sla_due_at&lt;/code&gt; timestamp is written once, at request creation: &lt;code&gt;now() + sla_days&lt;/code&gt;. The default is 7 days — more conservative than the 30-day legal window, so normal review and approval cycles don't consume the entire legal budget. That's the only mutation to this field — it never changes after the request is created.&lt;/p&gt;

&lt;p&gt;On every inbox load, every request detail page load, the &lt;code&gt;computeSlaStatus(slaDueAt)&lt;/code&gt; function runs in the API layer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;diffHours&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;slaDueAt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;diffHours&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;OVERDUE&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;diffHours&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;DUE_SOON&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
                     &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;WITHIN_SLA&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No database update. No background worker. No scheduled job. The status shown to the operator is always accurate as of the current server time.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;DUE_SOON&lt;/code&gt; triggers at 24 hours remaining — a one-day warning before the internal deadline. This gives operators a meaningful heads-up without creating false urgency days in advance.&lt;/p&gt;

&lt;h3&gt;
  
  
  Setting the Right Internal SLA for Your Organisation
&lt;/h3&gt;

&lt;p&gt;The DPDP Rules 2025 set a 30-day legal maximum for access/correction/erasure responses. How you set your internal target depends on your process:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A startup where one person handles requests end-to-end: 7–10 days is achievable and leaves buffer&lt;/li&gt;
&lt;li&gt;A mid-size company where requests go through legal review and data lookup across multiple systems: 14–21 days as the internal target, with the legal 30-day window as the backstop&lt;/li&gt;
&lt;li&gt;A large enterprise with formal approval workflows: set the SLA to match your internal SLA policy; use the evidence log to track compliance with your own commitments&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The configurable &lt;code&gt;orgs.sla_days&lt;/code&gt; field in the database — not yet wired to request creation in the current version, but in the roadmap — will let each organisation set its own target without changing code.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Status vs. SLA Distinction
&lt;/h3&gt;

&lt;p&gt;Early versions of DPDP Copilot conflated two concepts in a single field: the workflow status of the request (open, closed?) and the computed SLA urgency (within deadline?). The second database migration separates these:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- migration 002_split_status_from_sla.sql&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt; &lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="s1"&gt;'OPEN'&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;CASE&lt;/span&gt;
  &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;sla_status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'CLOSED'&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'CLOSED'&lt;/span&gt;
  &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="s1"&gt;'OPEN'&lt;/span&gt;
&lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After this migration:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;status&lt;/code&gt; is the workflow state: &lt;code&gt;OPEN&lt;/code&gt; or &lt;code&gt;CLOSED&lt;/code&gt;. Closed means a reply was sent and the request is resolved.&lt;/li&gt;
&lt;li&gt;The live SLA urgency is always computed by &lt;code&gt;computeSlaStatus&lt;/code&gt; at read time.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This matters for reporting. You want to answer: "Of all requests that were open during the last month, what percentage were responded to within the internal SLA?" That question requires separating workflow state from deadline state.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 7: Multilingual Compliance at Scale
&lt;/h2&gt;

&lt;p&gt;The multilingual capability deserves more attention than it typically gets in discussions of DPDP tooling.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why Language Matters for DPDP
&lt;/h3&gt;

&lt;p&gt;India's 2011 census (the most recent with detailed language data) recorded 19,569 raw mother tongue entries from respondents — often cited as "over 19,500 languages spoken in some capacity" — which consolidate into 121 languages with more than 10,000 speakers each. The DPDP Act and Rules 2025 require that grievance mechanisms be accessible, which practically means: if your users write to you in Hindi, a response mechanism that only understands English is not accessible.&lt;/p&gt;

&lt;p&gt;DPDP Copilot supports five languages in the current version:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;English&lt;/strong&gt; — the default, always available&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hindi&lt;/strong&gt; — 528 million speakers (2011 census)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bengali&lt;/strong&gt; — 97 million speakers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tamil&lt;/strong&gt; — 69 million speakers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Marathi&lt;/strong&gt; — 83 million speakers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The public request form shows these as radio button options. The selection flows into the API request, through the drafting call, and into the AI prompt.&lt;/p&gt;

&lt;h3&gt;
  
  
  What the AI Draft Looks Like in Practice
&lt;/h3&gt;

&lt;p&gt;Here's the same erasure request processed in two languages.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input (English):&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"I never gave consent for you to sell my data. Please delete everything you have about me immediately."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Suggested reply in English:&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Dear Data Principal,&lt;/p&gt;

&lt;p&gt;Thank you for your request submitted on 25 May 2025. We have received and logged your request for erasure of personal data under Section 12(b) of the Digital Personal Data Protection Act, 2023.&lt;/p&gt;

&lt;p&gt;Your request has been assigned reference number [REF-ID]. Our compliance team will review your request, locate the relevant data, and initiate the erasure process in accordance with applicable legal requirements. We will respond with the outcome within the timeframe required under the DPDP Act.&lt;/p&gt;

&lt;p&gt;Please retain this acknowledgement for your records.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Suggested reply in Hindi:&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;प्रिय डेटा प्रिंसिपल,&lt;/p&gt;

&lt;p&gt;25 मई 2025 को प्रस्तुत आपके अनुरोध के लिए धन्यवाद। हमने डिजिटल व्यक्तिगत डेटा संरक्षण अधिनियम, 2023 की धारा 12(ख) के अंतर्गत आपके व्यक्तिगत डेटा के विलोपन के अनुरोध को प्राप्त कर दर्ज किया है।&lt;/p&gt;

&lt;p&gt;आपके अनुरोध को संदर्भ संख्या [REF-ID] दी गई है। हमारी अनुपालन टीम आपके अनुरोध की समीक्षा करेगी, संबंधित डेटा का पता लगाएगी और लागू कानूनी आवश्यकताओं के अनुसार विलोपन प्रक्रिया शुरू करेगी। हम डीपीडीपी अधिनियम के तहत निर्धारित समय-सीमा के भीतर आपको परिणाम की सूचना देंगे।&lt;/p&gt;

&lt;p&gt;कृपया इस पावती को अपने रिकॉर्ड के लिए सुरक्षित रखें।&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The structure is identical. The legal references are consistent. The tone is professional but accessible. An operator who reviews the Hindi draft can run it through a translation tool to verify quality before sending — the AI draft is a starting point, not a blindly trusted final output.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why Separate Prompts Per Language Matter
&lt;/h3&gt;

&lt;p&gt;A naive approach would translate a fixed English template into other languages once, then serve those static translations. This works for simple acknowledgements but fails for personalised responses that need to reference the specific request content.&lt;/p&gt;

&lt;p&gt;Because DPDP Copilot drafts replies by passing the original message to the model, the suggested reply can acknowledge specific details the data principal mentioned — not just their request type. If someone writes "I asked you to stop sending me SMS messages three months ago and you're still doing it," a good response acknowledges that history. A static template can't.&lt;/p&gt;

&lt;p&gt;The LLM approach generates a response that's contextually appropriate in the data principal's language — which is a qualitatively different outcome from translation.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 8: The Data Architecture
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Schema Design for Compliance
&lt;/h3&gt;

&lt;p&gt;The database schema is designed around compliance requirements first, application convenience second.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Three tables, three responsibilities&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;orgs&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt;         &lt;span class="n"&gt;uuid&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;name&lt;/span&gt;       &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="n"&gt;timestamptz&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="n"&gt;sla_days&lt;/span&gt;   &lt;span class="nb"&gt;integer&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt;              &lt;span class="n"&gt;uuid&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;message&lt;/span&gt;         &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;type&lt;/span&gt;            &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;status&lt;/span&gt;          &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="s1"&gt;'OPEN'&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;suggested_reply&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;sla_due_at&lt;/span&gt;      &lt;span class="n"&gt;timestamptz&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;org_id&lt;/span&gt;          &lt;span class="n"&gt;uuid&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;orgs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt;      &lt;span class="n"&gt;timestamptz&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;evidence_events&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt;          &lt;span class="n"&gt;uuid&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;request_id&lt;/span&gt;  &lt;span class="n"&gt;uuid&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;event_type&lt;/span&gt;  &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;event_data&lt;/span&gt;  &lt;span class="n"&gt;jsonb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt;  &lt;span class="n"&gt;timestamptz&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;org_id&lt;/span&gt;      &lt;span class="n"&gt;uuid&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;orgs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;orgs.sla_days&lt;/code&gt; field exists and is populated but not yet wired to request creation — the 7-day hardcode is the current implementation. When that field is connected, different organisations can run different internal SLA targets. The schema is ready for that; the application code isn't yet.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;evidence_events.event_data&lt;/code&gt; field is &lt;code&gt;jsonb&lt;/code&gt; — flexible enough to store different metadata per event type without schema changes. As the tool evolves (new event types, operator attribution, channel tracking), existing rows aren't invalidated.&lt;/p&gt;

&lt;h3&gt;
  
  
  Index Strategy
&lt;/h3&gt;

&lt;p&gt;Two composite indexes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;requests_org_created_idx&lt;/span&gt;
  &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;org_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;evidence_events_request_org_created_idx&lt;/span&gt;
  &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;evidence_events&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;org_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first index supports the inbox query: "give me all requests for this org, sorted by most recent." The second supports the request detail query: "give me all evidence events for this request in this org, in chronological order."&lt;/p&gt;

&lt;p&gt;Both indexes include &lt;code&gt;org_id&lt;/code&gt; as the leading column because every query in the application is org-scoped. An index that starts with &lt;code&gt;org_id&lt;/code&gt; is used by the query planner even for queries that also filter by &lt;code&gt;request_id&lt;/code&gt; — the org scope eliminates most of the table before the planner looks at other columns.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 9: Deployment Architecture
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Self-Hosted by Design
&lt;/h3&gt;

&lt;p&gt;DPDP Copilot is self-hosted. That's a deliberate product decision, not an oversight.&lt;/p&gt;

&lt;p&gt;DPDP requests often contain sensitive personal data — names, contact details, account information, and sometimes sensitive categories of data like health information or financial details. The organisation processing these requests is the data fiduciary. Routing that data through a third-party SaaS for classification and storage creates its own compliance risk: you're a data processor, processing data principal requests by sending them to another data processor, with all the consent and data transfer implications that entails.&lt;/p&gt;

&lt;p&gt;Running the tool in your own infrastructure — whether on-premises or in a cloud account you control — keeps the data principal's message in your trust boundary. The only data that leaves your environment is the message text sent to Anthropic's API for classification and drafting. That's a single, scoped, auditable data transfer that you control.&lt;/p&gt;

&lt;h3&gt;
  
  
  Docker Compose Quickstart
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Clone and configure&lt;/span&gt;
git clone https://github.com/swapnanil/dpdp-copilot
&lt;span class="nb"&gt;cd &lt;/span&gt;dpdp-copilot
&lt;span class="nb"&gt;cp&lt;/span&gt; .env.example .env

&lt;span class="c"&gt;# .env minimum required:&lt;/span&gt;
&lt;span class="c"&gt;# ANTHROPIC_API_KEY=sk-ant-...&lt;/span&gt;
&lt;span class="c"&gt;# DATABASE_URL=postgresql://user:pass@db:5432/dpdp&lt;/span&gt;
&lt;span class="c"&gt;# ADMIN_USER=compliance_admin&lt;/span&gt;
&lt;span class="c"&gt;# ADMIN_PASS=your_secure_password&lt;/span&gt;
&lt;span class="c"&gt;# DEFAULT_ORG_ID=          # fill after running seed&lt;/span&gt;
&lt;span class="c"&gt;# ADMIN_SESSION_SECRET=    # openssl rand -hex 32&lt;/span&gt;

&lt;span class="c"&gt;# Start database&lt;/span&gt;
docker compose up db &lt;span class="nt"&gt;-d&lt;/span&gt;

&lt;span class="c"&gt;# Run migrations&lt;/span&gt;
docker compose run &lt;span class="nt"&gt;--rm&lt;/span&gt; migrate

&lt;span class="c"&gt;# Seed initial org (note the UUID it prints)&lt;/span&gt;
docker compose run &lt;span class="nt"&gt;--rm&lt;/span&gt; seed

&lt;span class="c"&gt;# Start the application&lt;/span&gt;
docker compose up app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open &lt;code&gt;http://localhost:3000&lt;/code&gt; for the operator inbox.&lt;br&gt;
Open &lt;code&gt;http://localhost:3000/grievance&lt;/code&gt; for the public form.&lt;/p&gt;
&lt;h3&gt;
  
  
  Environment Configuration Reference
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Variable&lt;/th&gt;
&lt;th&gt;Required&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ANTHROPIC_API_KEY&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Your Anthropic API key for Claude&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;DATABASE_URL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;PostgreSQL connection string&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ADMIN_USER&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Operator login username&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ADMIN_PASS&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Operator login password&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;DEFAULT_ORG_ID&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;UUID of the active organisation (from seed)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ADMIN_SESSION_SECRET&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Production&lt;/td&gt;
&lt;td&gt;Signs session cookies — &lt;code&gt;openssl rand -hex 32&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;MODEL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Claude model (default: &lt;code&gt;claude-sonnet-4-6&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;MAX_TOKENS&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Reply draft length (default: 1024)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PUPPETEER_EXECUTABLE_PATH&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Docker&lt;/td&gt;
&lt;td&gt;Chromium path — set automatically in Docker&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h3&gt;
  
  
  Production Considerations
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Session signing&lt;/strong&gt;: Generate &lt;code&gt;ADMIN_SESSION_SECRET&lt;/code&gt; with &lt;code&gt;openssl rand -hex 32&lt;/code&gt;. In development you can skip this; in production the session cookie must be signed or it's trivially forgeable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Database&lt;/strong&gt;: The Docker Compose setup runs Postgres in a container. For production, use a managed database (AWS RDS, Google Cloud SQL, Supabase) with automated backups. The evidence table is your legal record — you want it on infrastructure with point-in-time recovery.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;HTTPS&lt;/strong&gt;: Run behind a reverse proxy (nginx, Caddy) that terminates TLS. Session cookies should have &lt;code&gt;Secure&lt;/code&gt; and &lt;code&gt;SameSite=Strict&lt;/code&gt; — these aren't set in the current implementation but are straightforward to add in a production nginx config.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rate limiting&lt;/strong&gt;: The public &lt;code&gt;/grievance&lt;/code&gt; form has no rate limiting in the current version. A reverse proxy rate limit on the public intake endpoint prevents abuse without touching the application code.&lt;/p&gt;


&lt;h2&gt;
  
  
  Part 10: Known Limitations and What's Next
&lt;/h2&gt;

&lt;p&gt;Honesty about limitations is part of useful tooling documentation. Here's what DPDP Copilot currently doesn't do and what the roadmap looks like.&lt;/p&gt;
&lt;h3&gt;
  
  
  Current Limitations
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;No outbound delivery&lt;/strong&gt;: The "send reply" workflow doesn't actually send anything. It marks the reply as sent in the evidence log and sets the request to &lt;code&gt;CLOSED&lt;/code&gt;. The operator is responsible for actually sending the drafted reply via their existing channel (email, portal, etc.). This is a limitation of the MVP, not the design — real outbound email delivery is the obvious next step.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Single-admin authentication&lt;/strong&gt;: The current auth model is a single username/password pair from environment variables. There's no user table, no role model, no per-operator audit trail. Multiple operators can't be tracked individually. This is fine for a team of one; it's a problem for a compliance team of five.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Static org configuration&lt;/strong&gt;: The active organisation is selected via &lt;code&gt;DEFAULT_ORG_ID&lt;/code&gt; in the environment. There's no UI for switching organisations or a multi-tenant router. The database schema supports multiple orgs; the application routing doesn't.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No structured contact data&lt;/strong&gt;: Contact information is embedded in the free-form message. There's no &lt;code&gt;contact_email&lt;/code&gt; or &lt;code&gt;contact_phone&lt;/code&gt; field. This means there's no reliable way to programmatically address the data principal in the reply or route the response to them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PDF XSS risk&lt;/strong&gt;: The PDF template interpolates user-provided text directly into HTML without escaping. A malicious actor could potentially inject HTML into the generated PDF. This is a known issue and is the highest-priority security fix.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No notifications&lt;/strong&gt;: Operators have no way to be alerted when a new request comes in or when a request is approaching its internal SLA deadline. Checking the inbox manually is the only current mechanism.&lt;/p&gt;
&lt;h3&gt;
  
  
  The Roadmap
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Outbound reply delivery&lt;/strong&gt;: Send the drafted reply via email (SendGrid, AWS SES, or SMTP) directly from the tool. Logs the delivery event to the evidence table. The operator reviews the draft, edits if needed, and clicks Send — not "Copy this and email it manually."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SLA alerts&lt;/strong&gt;: Email or Slack notification when a request enters &lt;code&gt;DUE_SOON&lt;/code&gt; status. Optional daily digest of all open requests with their current SLA status.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Multi-operator support&lt;/strong&gt;: A &lt;code&gt;users&lt;/code&gt; table, per-operator login, and role assignment (reviewer vs. approver). Evidence events attributed to specific operators. Audit trail for who touched what.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Structured contact fields&lt;/strong&gt;: Separate &lt;code&gt;contact_email&lt;/code&gt; from the message body at intake. Validate format. Apply retention controls — contact data should be deletable when the request is closed without deleting the evidence trail.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Configurable SLA&lt;/strong&gt;: Wire &lt;code&gt;orgs.sla_days&lt;/code&gt; to request creation. Different organisations have different internal SLA commitments — the schema already supports this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Approval workflow&lt;/strong&gt;: A draft reply requires supervisor approval before it can be sent. The evidence log records who approved and when. This is an operational pattern for organisations where a junior compliance analyst drafts but a senior officer approves.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Analytics dashboard&lt;/strong&gt;: How many requests per week? What types? Average response time? What percentage are within the internal SLA? This is a reporting requirement for any compliance programme worth its name.&lt;/p&gt;


&lt;h2&gt;
  
  
  Part 11: How DPDP Copilot Fits Into a Broader Compliance Programme
&lt;/h2&gt;

&lt;p&gt;DPDP Copilot handles the data principal rights workflow. That's one piece of a complete DPDP compliance programme. Here's how it fits:&lt;/p&gt;
&lt;h3&gt;
  
  
  What DPDP Copilot Covers
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Receiving data principal requests (Access, Rectification, Deletion, Grievance)&lt;/li&gt;
&lt;li&gt;Classifying them correctly and consistently&lt;/li&gt;
&lt;li&gt;Drafting multilingual responses&lt;/li&gt;
&lt;li&gt;Tracking internal SLA deadlines&lt;/li&gt;
&lt;li&gt;Generating the audit evidence trail&lt;/li&gt;
&lt;li&gt;Exporting evidence for regulatory review&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  What It Doesn't Cover
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Data discovery&lt;/strong&gt;: Finding where a person's data actually lives across your systems. DPDP Copilot receives and tracks the request but doesn't automate the underlying data lookup. That's a data catalogue problem.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Consent management&lt;/strong&gt;: Recording and tracking what data was collected under what consent. That's a separate consent registry.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Privacy notices&lt;/strong&gt;: Generating or maintaining the notice required under Section 5 of the Act. That's a legal document workflow.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data breach notification&lt;/strong&gt;: Section 8(6) requires prompt notification of significant breaches to the Data Protection Board and affected persons. That's a separate incident response workflow.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-border transfer compliance&lt;/strong&gt;: The Act restricts transfers of personal data to certain countries. That's a data governance and infrastructure question.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A full DPDP compliance programme needs all of these. DPDP Copilot handles the rights management piece — the part that creates the most immediate operational urgency because it has a hard deadline on individual transactions and a direct escalation path to the Data Protection Board.&lt;/p&gt;
&lt;h3&gt;
  
  
  The Risk Reduction Calculation
&lt;/h3&gt;

&lt;p&gt;Before DPDP Copilot:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Time to acknowledge a request: hours to days (depends on inbox monitoring)&lt;/li&gt;
&lt;li&gt;Time to classify a request: manual, inconsistent, language-dependent&lt;/li&gt;
&lt;li&gt;Time to draft a response: hours (finding a template, adapting it, translating it)&lt;/li&gt;
&lt;li&gt;Deadline tracking: none — someone has to remember&lt;/li&gt;
&lt;li&gt;Evidence: none — email threads that can be deleted&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After DPDP Copilot:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Time to acknowledge: seconds (the evidence log records receipt immediately on submission)&lt;/li&gt;
&lt;li&gt;Time to classify: 1–2 seconds (LLM call)&lt;/li&gt;
&lt;li&gt;Time to draft a response: 2–3 seconds (LLM call)&lt;/li&gt;
&lt;li&gt;Deadline tracking: automatic, live-computed, visible in the operator inbox&lt;/li&gt;
&lt;li&gt;Evidence: append-only database log, exportable as PDF or CSV in one click&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The reduction in time-to-first-action is the most important improvement. The legal clock starts when the request is submitted — not when someone reads it. DPDP Copilot ensures that classification and drafting are done before any human even opens the inbox. The operator's job is review and send, not receive-classify-draft-send.&lt;/p&gt;


&lt;h2&gt;
  
  
  Part 12: Who Should Use This
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Compliance and legal teams at Indian companies&lt;/strong&gt; processing personal data of Indian residents under the DPDP Act. If you're a data fiduciary — collecting or processing personal data — you have obligations under this Act. If you don't have a structured process for handling data principal requests, you need one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Engineering teams building privacy infrastructure&lt;/strong&gt; who need a reference implementation of DPDP request handling. The codebase is open-source. The data model, the API structure, the evidence logging pattern, the SLA computation logic — all of it is readable, runnable, and adaptable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Startups at the early compliance stage&lt;/strong&gt; who don't yet have a dedicated compliance team. The tool runs on a single machine. Configuration is a &lt;code&gt;.env&lt;/code&gt; file. The public form can be linked from your privacy policy. You don't need a compliance department to run it — you need someone who checks the inbox.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Organisations handling multilingual Indian user bases&lt;/strong&gt; where an English-only inbox isn't accessible to all the people it's supposed to serve. If your users write to you in Hindi and Tamil, they deserve responses in Hindi and Tamil — and the time cost of manual translation has historically made that impractical. It isn't anymore.&lt;/p&gt;


&lt;h2&gt;
  
  
  A Complete Example Walkthrough
&lt;/h2&gt;

&lt;p&gt;Let me walk through a real scenario end-to-end, using the tool as a data principal and then as an operator.&lt;/p&gt;
&lt;h3&gt;
  
  
  As the Data Principal
&lt;/h3&gt;

&lt;p&gt;You purchased something from a company. You're now getting SMS marketing messages you didn't opt in to. You want to file an erasure request and a grievance.&lt;/p&gt;

&lt;p&gt;You go to &lt;code&gt;https://yourcompany.com/grievance&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;You write:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"I never gave you permission to send me SMS promotions. I want you to delete my phone number and all data you hold about me. I also want to formally complain about this."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You select &lt;strong&gt;Hindi&lt;/strong&gt; as your preferred language and submit.&lt;/p&gt;

&lt;p&gt;You receive an acknowledgement: "Your request has been received and logged. Reference: [UUID]. Our compliance team will be in touch with the outcome."&lt;/p&gt;
&lt;h3&gt;
  
  
  As the Compliance Operator
&lt;/h3&gt;

&lt;p&gt;You open the operator inbox the next morning. You see a new request, classified as &lt;strong&gt;Grievance&lt;/strong&gt; (the model detected the formal complaint language alongside the deletion request), with &lt;code&gt;WITHIN_SLA&lt;/code&gt; status.&lt;/p&gt;

&lt;p&gt;You click into the request. You read the original message. The suggested reply in Hindi is already drafted. You read it — it acknowledges the complaint, confirms the erasure request has been noted, and explains next steps in Hindi.&lt;/p&gt;

&lt;p&gt;You make a small edit to reference your company's specific erasure process. You click "Send Reply" — which in the current version means you copy the draft, send it via your email system, and then click "Mark as Sent" in the tool.&lt;/p&gt;

&lt;p&gt;The evidence timeline now shows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;REQUEST_CREATED     2025-05-25 10:00:00
REQUEST_CLASSIFIED  2025-05-25 10:00:01  (Grievance)
REPLY_SUGGESTED     2025-05-25 10:00:02
REPLY_SENT          2025-05-26 09:15:00
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Total response time: 23 hours. Well within any reasonable SLA window. The CSV export documents this. If the data principal escalates to the Data Protection Board, you have a timestamped, exportable record of the complete interaction.&lt;/p&gt;




&lt;h2&gt;
  
  
  Quick Reference
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Public form&lt;/strong&gt;: &lt;code&gt;GET /grievance&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;API Endpoints&lt;/strong&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Path&lt;/th&gt;
&lt;th&gt;Auth&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;POST&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/api/public/requests&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Submit a data principal request&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;POST&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/api/login&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Operator login&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;POST&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/api/logout&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Operator logout&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GET&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/api/requests&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Operator&lt;/td&gt;
&lt;td&gt;List all requests with live SLA status&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GET&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/api/requests/:id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Operator&lt;/td&gt;
&lt;td&gt;Request detail + evidence timeline&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;POST&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/api/requests/:id/send-reply&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Operator&lt;/td&gt;
&lt;td&gt;Mark reply sent, close request&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GET&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/api/requests/:id/export/pdf&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Operator&lt;/td&gt;
&lt;td&gt;Download PDF evidence report&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GET&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/api/requests/:id/export/csv&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Operator&lt;/td&gt;
&lt;td&gt;Download CSV evidence export&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Request lifecycle&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Public form submission
  → Internal SLA clock starts (configurable, default 7 days)
  → REQUEST_CREATED logged
  → AI classification (Grievance / Access / Rectification / Deletion)
  → REQUEST_CLASSIFIED logged
  → AI reply drafted in chosen language
  → REPLY_SUGGESTED logged
  → Operator reviews in inbox
  → Operator marks reply sent
  → REPLY_SENT logged
  → Request status: CLOSED
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Legal response windows under DPDP Rules 2025&lt;/strong&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Request type&lt;/th&gt;
&lt;th&gt;Section&lt;/th&gt;
&lt;th&gt;Legal window&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Access&lt;/td&gt;
&lt;td&gt;Section 11&lt;/td&gt;
&lt;td&gt;30 days&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Correction / Erasure&lt;/td&gt;
&lt;td&gt;Section 12&lt;/td&gt;
&lt;td&gt;30 days&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Grievance&lt;/td&gt;
&lt;td&gt;Section 13&lt;/td&gt;
&lt;td&gt;90 days&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Final Thought
&lt;/h2&gt;

&lt;p&gt;The DPDP Act's data principal rights framework isn't complicated. Four rights, two response windows, one evidence requirement. The complexity is operational — handling a high-variance stream of natural language requests, in multiple languages, against a hard time constraint, with an audit trail that has to survive regulatory scrutiny.&lt;/p&gt;

&lt;p&gt;Manual processes fail under those conditions not because of negligence but because the requirements are genuinely hard to satisfy with shared inboxes and email chains.&lt;/p&gt;

&lt;p&gt;DPDP Copilot automates the classification and drafting — the two tasks that are the most time-consuming and the most error-prone. It makes the internal SLA clock visible before it expires. It generates the audit evidence as a byproduct of normal operation, not as a separate reporting task.&lt;/p&gt;

&lt;p&gt;The tool is open-source, self-hosted, and runs on a single Docker Compose command. If you're an Indian company with DPDP obligations and no structured data rights workflow, this is where to start.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://swapnanilsaha.com/tools/dpdp-copilot/" rel="noopener noreferrer"&gt;→ View the full tool page, docs, live demo, and GitHub repo&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built by Swapnanil Saha — &lt;a href="https://swapnanilsaha.com" rel="noopener noreferrer"&gt;swapnanilsaha.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>dpdp</category>
      <category>compliance</category>
      <category>dataprotection</category>
      <category>ai</category>
    </item>
    <item>
      <title>How to Stop Evaluating LLM Outputs by Gut Feel</title>
      <dc:creator>Swapnanil Saha</dc:creator>
      <pubDate>Thu, 21 May 2026 05:25:31 +0000</pubDate>
      <link>https://forem.com/swapnanilsaha/how-to-stop-evaluating-llm-outputs-by-gut-feel-ml9</link>
      <guid>https://forem.com/swapnanilsaha/how-to-stop-evaluating-llm-outputs-by-gut-feel-ml9</guid>
      <description>&lt;p&gt;The standard workflow for evaluating LLM output quality goes something like this: someone reads Response A, reads Response B, and says "I think A is better." Everyone nods. The prompt ships.&lt;/p&gt;

&lt;p&gt;This is a problem for three reasons:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;It doesn't scale.&lt;/strong&gt; You can't manually review 500 eval pairs after every prompt change.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's inconsistent.&lt;/strong&gt; The same person evaluating the same pair on different days produces different results.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It doesn't tell you why.&lt;/strong&gt; "Response A is better" doesn't tell you what to fix when Response B becomes the baseline.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I built &lt;strong&gt;LLM Eval Suite&lt;/strong&gt; to replace gut feel with structured, evidence-backed scoring — for any task type, with CI integration.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://swapnanilsaha.com/tools/llm-eval-suite/" rel="noopener noreferrer"&gt;→ Full tool page&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Core Insight: Evidence, Not Opinion
&lt;/h2&gt;

&lt;p&gt;Every score in LLM Eval Suite is accompanied by a verbatim quote from the response being evaluated. Not "this response has poor faithfulness" — but:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Faithfulness: 1.0/10
Quote: "30-day return policy, no questions asked"
Reasoning: "Source document specifies 14 days. This is a clear hallucination, not an interpretation."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This changes what you can do with the output. You can show it to a stakeholder. You can track it over time. You can build a regression test from it. You can tell the model what specifically went wrong.&lt;/p&gt;




&lt;h2&gt;
  
  
  Six Evaluation Capabilities
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Multi-Dimensional Scoring
&lt;/h3&gt;

&lt;p&gt;Ten task presets — QA, summarisation, RAG, code generation, creative writing, classification, translation, and more. Each preset activates the dimensions that matter for that task:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task Type&lt;/th&gt;
&lt;th&gt;Key Dimensions&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;qa&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Faithfulness, Completeness, Conciseness, Relevance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;summarisation&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Coverage, Compression, Accuracy, Readability&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;rag&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Faithfulness, Answer Relevancy, Context Precision, Context Recall&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;code&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Correctness, Efficiency, Readability, Security&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Every dimension score comes with verbatim evidence from the response text.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker-compose run cli &lt;span class="nb"&gt;eval&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--file&lt;/span&gt; examples/eval_qa.json &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--mode&lt;/span&gt; compare &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--format&lt;/span&gt; markdown
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Regression Testing
&lt;/h3&gt;

&lt;p&gt;Save any eval report as a named baseline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker-compose run cli regression save results.json &lt;span class="nt"&gt;--id&lt;/span&gt; prod-baseline
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run future evals against it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker-compose run cli regression run results.json &lt;span class="nt"&gt;--id&lt;/span&gt; prod-baseline &lt;span class="nt"&gt;--format&lt;/span&gt; markdown
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Per-dimension deltas are compared against configurable thresholds. &lt;strong&gt;Exit code 1 when scores drop below your floor.&lt;/strong&gt; This is the feature that makes the tool useful in CI.&lt;/p&gt;

&lt;h3&gt;
  
  
  GitHub Actions Integration
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run LLM eval&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;docker-compose run cli eval \&lt;/span&gt;
      &lt;span class="s"&gt;--file evals/suite.json \&lt;/span&gt;
      &lt;span class="s"&gt;--mode rank \&lt;/span&gt;
      &lt;span class="s"&gt;--format junit \&lt;/span&gt;
      &lt;span class="s"&gt;--output results.xml&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mikepenz/action-junit-report@v3&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;report_paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;results.xml&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Regression check&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;docker-compose run cli regression run \&lt;/span&gt;
      &lt;span class="s"&gt;results.json --id prod-baseline&lt;/span&gt;
    &lt;span class="s"&gt;# exits 1 if any dimension drops beyond threshold&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gates model upgrades, prompt changes, and fine-tune releases automatically. The JUnit XML output integrates with any CI system that understands test reports.&lt;/p&gt;

&lt;h3&gt;
  
  
  Hallucination Detection
&lt;/h3&gt;

&lt;p&gt;Claim-level analysis against a source document. Each claim in the response is classified as supported or unsupported — binary, not "mostly faithful."&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker-compose run cli hallucination &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--response&lt;/span&gt; output.txt &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--source&lt;/span&gt; source.txt &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--format&lt;/span&gt; markdown
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Risk levels: none / low / moderate / high / critical, with a &lt;code&gt;safe_to_use&lt;/code&gt; boolean for downstream gating. This is what you run before using LLM output in a production pipeline where accuracy matters.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;hallucination_risk&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;high&lt;/span&gt;
&lt;span class="na"&gt;safe_to_use&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;

&lt;span class="na"&gt;Claim&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;30-day&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;return&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;policy"&lt;/span&gt;
  &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unsupported&lt;/span&gt;
  &lt;span class="na"&gt;evidence&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Source&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;specifies&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;14&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;days"&lt;/span&gt;
  &lt;span class="na"&gt;severity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;critical&lt;/span&gt;

&lt;span class="na"&gt;Claim&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;no&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;questions&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;asked"&lt;/span&gt;
  &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unsupported&lt;/span&gt;
  &lt;span class="na"&gt;evidence&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Source&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;makes&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;no&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;mention&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;of&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;return&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;conditions"&lt;/span&gt;
  &lt;span class="na"&gt;severity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;high&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Prompt Sensitivity Analysis
&lt;/h3&gt;

&lt;p&gt;Test 2–5 prompt variants against a fixed response. Per-dimension variance tells you which dimensions are fragile across phrasings and which are stable.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker-compose run cli sensitivity &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--file&lt;/span&gt; examples/prompt_variants.json &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--format&lt;/span&gt; markdown
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Know which prompt phrasings shift your scores before you deploy. High-variance dimensions across prompts signal that your evaluation isn't measuring the response — it's measuring the prompt wording.&lt;/p&gt;

&lt;h3&gt;
  
  
  Panel Evaluation
&lt;/h3&gt;

&lt;p&gt;Run N independent judge passes on the same evaluation. Mean and variance per dimension expose where judges agree and where they disagree.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker-compose run cli panel &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--file&lt;/span&gt; examples/eval_qa.json &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--judges&lt;/span&gt; 5 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--format&lt;/span&gt; markdown
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;High-variance dimensions are flagged for human review automatically. The panel mode is the right choice when you're evaluating subjective tasks like creative writing where a single judge's opinion is insufficient signal.&lt;/p&gt;

&lt;h3&gt;
  
  
  RAGAS-Compatible RAG Preset
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;rag&lt;/code&gt; task type maps the four RAGAS metrics — &lt;strong&gt;faithfulness, answer relevancy, context precision, context recall&lt;/strong&gt; — as first-class evaluation dimensions with equal weighting. The output is compatible with RAGAS reporting conventions, so you can integrate this into existing RAGAS workflows or use it as a drop-in alternative.&lt;/p&gt;




&lt;h2&gt;
  
  
  Example: Two Responses In, Clear Winner Out
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Input:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"task_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"qa"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"eval_mode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"compare"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Refunds are accepted within 14 days if the item is unused."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"responses"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"label"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Response A"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"You can get a refund within 14 days if the item hasn't been used."&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"label"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Response B"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Our 30-day return policy means no questions asked."&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;winner&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Response A&lt;/span&gt;
&lt;span class="na"&gt;margin&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;clear&lt;/span&gt;

&lt;span class="s"&gt;Response B — Faithfulness&lt;/span&gt;
  &lt;span class="s"&gt;score&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1.0/10&lt;/span&gt;
  &lt;span class="s"&gt;quote&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;30-day&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;return&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;policy,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;no&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;questions&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;asked"&lt;/span&gt;
  &lt;span class="na"&gt;reasoning&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Source&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;specifies&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;14&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;days.&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;'No&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;questions&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;asked'&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;is&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;not&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;in&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;the&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;source.&lt;/span&gt;
              &lt;span class="s"&gt;Two&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;distinct&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;hallucinations&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;in&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;one&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;sentence."&lt;/span&gt;

&lt;span class="s"&gt;Response A — Faithfulness&lt;/span&gt;
  &lt;span class="s"&gt;score&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;9.5/10&lt;/span&gt;
  &lt;span class="s"&gt;quote&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;within&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;14&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;days&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;if&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;the&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;item&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;hasn't&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;been&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;used"&lt;/span&gt;
  &lt;span class="na"&gt;reasoning&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Accurately&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;paraphrases&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;the&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;source&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;with&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;no&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;additions."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Why This Matters in Production
&lt;/h2&gt;

&lt;p&gt;LLM evaluation is usually treated as a one-time concern — you evaluate before you ship. But models change, prompts drift, data distributions shift, and retrieval quality fluctuates. A system that was 90% faithful in January may be 75% faithful in April because the upstream data changed.&lt;/p&gt;

&lt;p&gt;The regression testing and CI integration in LLM Eval Suite are designed for this reality. You run evals continuously, not just at release time. The baseline is the floor — if you drop below it, the pipeline stops.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://swapnanilsaha.com/tools/llm-eval-suite/" rel="noopener noreferrer"&gt;→ View the full tool page, docs, and GitHub repo&lt;/a&gt;&lt;/p&gt;

</description>
      <category>llm</category>
      <category>ai</category>
      <category>testing</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Stop Getting 'It Depends' Answers About RAG Architecture</title>
      <dc:creator>Swapnanil Saha</dc:creator>
      <pubDate>Thu, 21 May 2026 05:09:30 +0000</pubDate>
      <link>https://forem.com/swapnanilsaha/stop-getting-it-depends-answers-about-rag-architecture-1em7</link>
      <guid>https://forem.com/swapnanilsaha/stop-getting-it-depends-answers-about-rag-architecture-1em7</guid>
      <description>&lt;p&gt;Ask five AI engineers which vector database to use for your RAG system. You'll get five different answers, and they'll all start with "it depends."&lt;/p&gt;

&lt;p&gt;It depends on your data volume. It depends on your query patterns. It depends on whether you need GDPR compliance. It depends on your team's infra maturity. It depends on your budget. It depends on whether you're doing hybrid search.&lt;/p&gt;

&lt;p&gt;The "it depends" answer is technically correct and operationally useless. It turns an architecture decision into an unbounded research project.&lt;/p&gt;

&lt;p&gt;I built &lt;strong&gt;RAG Readiness&lt;/strong&gt; to make one specific recommendation per component — and explain why.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://swapnanilsaha.com/tools/rag-readiness/" rel="noopener noreferrer"&gt;→ Full tool page&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Design Principle: Opinions, Not Options
&lt;/h2&gt;

&lt;p&gt;Most RAG tooling and documentation presents you with a comparison table. Pinecone vs. Weaviate vs. Qdrant vs. Chroma. BM25 vs. dense vs. hybrid. ada-002 vs. text-embedding-3-large.&lt;/p&gt;

&lt;p&gt;Comparison tables are useful if you already know which dimensions matter for your use case. They're paralyzing if you don't.&lt;/p&gt;

&lt;p&gt;RAG Readiness is opinionated by design. You describe your use case, your data, your constraints. The tool returns &lt;strong&gt;one choice per component&lt;/strong&gt; — with full reasoning.&lt;/p&gt;

&lt;p&gt;If GDPR applies, managed cloud vector databases are eliminated from consideration before the LLM is even called. That's a rule, not an LLM judgment. The recommendation you receive is already constraint-filtered.&lt;/p&gt;




&lt;h2&gt;
  
  
  Six Modes, One Tool
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Architecture Recommendation
&lt;/h3&gt;

&lt;p&gt;The core mode. Answer a structured set of questions about your use case — document types, query patterns, scale, compliance requirements, team capabilities. Get back:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Vector database&lt;/strong&gt;: one specific choice with rationale&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Embedding model&lt;/strong&gt;: one specific choice&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chunking strategy&lt;/strong&gt;: one specific approach with parameters&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retrieval method&lt;/strong&gt;: dense / BM25 / hybrid — one answer&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reranker&lt;/strong&gt;: whether you need one and which
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python main.py audit &lt;span class="nt"&gt;--interactive&lt;/span&gt;
&lt;span class="c"&gt;# or from file:&lt;/span&gt;
python main.py audit &lt;span class="nt"&gt;--file&lt;/span&gt; examples/usecase_legal_contracts.json &lt;span class="nt"&gt;--with-cost&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Architecture Diagnosis
&lt;/h3&gt;

&lt;p&gt;You already have a RAG system. It's not working. This mode takes your existing architecture and the problems you're seeing, and returns a root-cause analysis per component with severity levels and one specific fix.&lt;/p&gt;

&lt;p&gt;Not "improve your chunking" — "switch from fixed 512-token chunks to parent-child hierarchical chunking with 512-token child nodes. Your documents have multi-clause structure that fixed chunks split mid-sentence."&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python main.py diagnose &lt;span class="nt"&gt;--file&lt;/span&gt; examples/diagnosis_pinecone_fixed.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;overall_severity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;critical&lt;/span&gt;

&lt;span class="s"&gt;chunking_strategy — critical&lt;/span&gt;
  &lt;span class="s"&gt;"Fixed 512-token chunks split mid-clause in long legal documents"&lt;/span&gt;
  &lt;span class="s"&gt;Fix&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Parent-child hierarchical chunking, 512-token child nodes&lt;/span&gt;

&lt;span class="s"&gt;retrieval_method — high&lt;/span&gt;
  &lt;span class="s"&gt;"Dense-only misses exact terms like dollar amounts and clause references"&lt;/span&gt;
  &lt;span class="s"&gt;Fix&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Hybrid BM25 + dense with RRF fusion&lt;/span&gt;

&lt;span class="na"&gt;quick_fix&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Enable 10% token overlap today. Takes 20 minutes, reduces&lt;/span&gt;
           &lt;span class="s"&gt;the worst failures while you implement the full fix.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Multi-Use-Case Session
&lt;/h3&gt;

&lt;p&gt;Run up to 5 parallel audits in a single request — useful when you're scoping a RAG platform that needs to serve multiple internal teams.&lt;/p&gt;

&lt;p&gt;The output includes cross-cutting insights: which components can be shared across use cases, where requirements conflict (the legal team needs GDPR-compliant storage; the sales team wants managed cloud), and which use case to build first for the highest return on the shared infrastructure investment.&lt;/p&gt;

&lt;h3&gt;
  
  
  Implementation Bundle
&lt;/h3&gt;

&lt;p&gt;Once you have an architecture you trust, generate a complete implementation starter kit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python main.py bundle &amp;lt;session-id&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output: a &lt;code&gt;requirements.txt&lt;/code&gt;, &lt;code&gt;docker-compose.yml&lt;/code&gt;, &lt;code&gt;.env.example&lt;/code&gt;, and migration guide tailored to the recommended architecture. If you have an existing stack, you get ordered migration steps with rollback notes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cost Estimation
&lt;/h3&gt;

&lt;p&gt;Rule-based monthly cost breakdown per component — &lt;strong&gt;no LLM call&lt;/strong&gt;. Lookup tables for vector DB pricing tiers, embedding API costs, reranker inference, and LLM costs at your estimated query volume.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python main.py cost &amp;lt;session-id&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Returns a line-item breakdown, optimization tips (e.g., "switching to a self-hosted embedding model saves ~$800/month at this query volume"), and a hosting model classification (managed vs. self-hosted trade-off at your scale).&lt;/p&gt;

&lt;h3&gt;
  
  
  RAGAS Eval Dataset Generation
&lt;/h3&gt;

&lt;p&gt;Generate evaluation questions grounded in your actual use case and query patterns — not generic retrieval questions.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python main.py eval-dataset &amp;lt;session-id&amp;gt; &lt;span class="nt"&gt;--num-questions&lt;/span&gt; 20
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output includes easy/medium/hard distribution, RAGAS metric mapping (which questions test faithfulness vs. answer relevancy vs. context precision), an annotation guide, and a time estimate for human review.&lt;/p&gt;




&lt;h2&gt;
  
  
  Session Persistence and Refinement
&lt;/h2&gt;

&lt;p&gt;Every audit persists to SQLite. You can refine against new constraints:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python main.py refine &amp;lt;session-id&amp;gt; &lt;span class="nt"&gt;--feedback&lt;/span&gt; &lt;span class="s2"&gt;"Qdrant was too heavy for our infra team"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The tool re-runs with the feedback as an additional constraint. Refinement history is tracked — you can see how the recommendation evolved across iterations.&lt;/p&gt;




&lt;h2&gt;
  
  
  A Complete Quickstart
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/swapnanil/rag-readiness
&lt;span class="nb"&gt;cd &lt;/span&gt;rag-readiness
&lt;span class="nb"&gt;cp&lt;/span&gt; .env.example .env  &lt;span class="c"&gt;# add your ANTHROPIC_API_KEY&lt;/span&gt;
docker-compose up api

&lt;span class="c"&gt;# New architecture audit (interactive)&lt;/span&gt;
python main.py audit &lt;span class="nt"&gt;--interactive&lt;/span&gt;

&lt;span class="c"&gt;# Diagnose a broken stack&lt;/span&gt;
python main.py diagnose &lt;span class="nt"&gt;--interactive&lt;/span&gt;

&lt;span class="c"&gt;# Multi-use-case session&lt;/span&gt;
python main.py multi-audit examples/multi_usecase_lexvault.json

&lt;span class="c"&gt;# List sessions and refine&lt;/span&gt;
python main.py sessions
python main.py refine &amp;lt;session-id&amp;gt; &lt;span class="nt"&gt;--feedback&lt;/span&gt; &lt;span class="s2"&gt;"need self-hosted only"&lt;/span&gt;

&lt;span class="c"&gt;# Cost breakdown and eval dataset&lt;/span&gt;
python main.py cost &amp;lt;session-id&amp;gt;
python main.py eval-dataset &amp;lt;session-id&amp;gt; &lt;span class="nt"&gt;--num-questions&lt;/span&gt; 20
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  The Pre-Scoring Layer
&lt;/h2&gt;

&lt;p&gt;Before any LLM call, a rule-based pre-scorer computes a complexity score (1–10) from the use case inputs. This has two effects:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;It calibrates the LLM prompt — a complexity-1 use case gets a simpler, more direct recommendation; a complexity-9 use case gets a recommendation with more explicit trade-off reasoning.&lt;/li&gt;
&lt;li&gt;It runs conflict detection — if your inputs contain contradictory constraints (e.g., "GDPR compliant" + "use Pinecone"), the conflict is flagged before the LLM is called, not discovered in the output.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Who This Is For
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;AI engineers&lt;/strong&gt; starting a new RAG project who want a structured starting point rather than a blank page&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Engineering leads&lt;/strong&gt; who need to scope a RAG system for a business use case and justify the architecture choices to non-technical stakeholders&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Teams with an existing RAG system&lt;/strong&gt; that isn't performing as expected and need a systematic diagnosis, not a hunch&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The tool is open-source, runs locally, and persists everything to SQLite. Your use case details don't leave your environment beyond the single LLM API call per audit.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://swapnanilsaha.com/tools/rag-readiness/" rel="noopener noreferrer"&gt;→ View the full tool page, docs, and GitHub repo&lt;/a&gt;&lt;/p&gt;

</description>
      <category>rag</category>
      <category>llm</category>
      <category>ai</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Building Distributed Systems, Backend Infrastructure &amp; AI Platforms — My Engineering Journey</title>
      <dc:creator>Swapnanil Saha</dc:creator>
      <pubDate>Tue, 19 May 2026 09:34:58 +0000</pubDate>
      <link>https://forem.com/swapnanilsaha/building-distributed-systems-backend-infrastructure-ai-platforms-my-engineering-journey-54p3</link>
      <guid>https://forem.com/swapnanilsaha/building-distributed-systems-backend-infrastructure-ai-platforms-my-engineering-journey-54p3</guid>
      <description>&lt;p&gt;Hey everyone 👋&lt;/p&gt;

&lt;p&gt;I’m &lt;a href="https://swapnanilsaha.com" rel="noopener noreferrer"&gt;Swapnanil Saha&lt;/a&gt;, a backend and distributed systems engineer from Mumbai, India with 9+ years of experience building high-performance infrastructure systems, backend platforms, optimization pipelines, and AI-driven architectures.&lt;/p&gt;

&lt;p&gt;🌐 Website: &lt;a href="https://swapnanilsaha.com" rel="noopener noreferrer"&gt;swapnanilsaha.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;💻 GitHub: &lt;a href="https://github.com/swapnanil" rel="noopener noreferrer"&gt;github.com/swapnanil&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;🔗 LinkedIn: &lt;a href="https://www.linkedin.com/in/swapnanil/" rel="noopener noreferrer"&gt;linkedin.com/in/swapnanil&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Work On
&lt;/h2&gt;

&lt;p&gt;Over the years, I’ve worked on systems involving:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Large-scale distributed backend systems&lt;/li&gt;
&lt;li&gt;Low-latency request processing&lt;/li&gt;
&lt;li&gt;High-QPS APIs&lt;/li&gt;
&lt;li&gt;Real-time optimization systems&lt;/li&gt;
&lt;li&gt;ML/CVR prediction pipelines&lt;/li&gt;
&lt;li&gt;AI infrastructure and automation tooling&lt;/li&gt;
&lt;li&gt;Performance engineering and infra optimization&lt;/li&gt;
&lt;li&gt;Data-intensive backend architectures&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most of my work has focused on solving problems where:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;scalability matters,&lt;/li&gt;
&lt;li&gt;latency matters,&lt;/li&gt;
&lt;li&gt;reliability matters,&lt;/li&gt;
&lt;li&gt;and infrastructure efficiency matters.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I enjoy working on systems that operate under real production constraints and require practical engineering tradeoffs at scale.&lt;/p&gt;




&lt;h1&gt;
  
  
  Areas I’m Currently Exploring
&lt;/h1&gt;

&lt;p&gt;Lately, I’ve been spending more time exploring the intersection of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AI systems&lt;/li&gt;
&lt;li&gt;distributed infrastructure&lt;/li&gt;
&lt;li&gt;backend optimization&lt;/li&gt;
&lt;li&gt;and production-scale ML platforms.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Some areas I’m currently interested in:&lt;/p&gt;

&lt;h2&gt;
  
  
  AI Infrastructure
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;inference systems&lt;/li&gt;
&lt;li&gt;orchestration&lt;/li&gt;
&lt;li&gt;model serving&lt;/li&gt;
&lt;li&gt;prompt pipelines&lt;/li&gt;
&lt;li&gt;scalable AI tooling&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  ML &amp;amp; Prediction Systems
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;CVR prediction systems&lt;/li&gt;
&lt;li&gt;feature pipelines&lt;/li&gt;
&lt;li&gt;production inference flows&lt;/li&gt;
&lt;li&gt;experimentation frameworks&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Backend &amp;amp; Distributed Systems
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;concurrency&lt;/li&gt;
&lt;li&gt;caching systems&lt;/li&gt;
&lt;li&gt;event-driven architectures&lt;/li&gt;
&lt;li&gt;reliability engineering&lt;/li&gt;
&lt;li&gt;observability&lt;/li&gt;
&lt;li&gt;scaling strategies&lt;/li&gt;
&lt;/ul&gt;




&lt;h1&gt;
  
  
  Tech Stack
&lt;/h1&gt;

&lt;p&gt;Some technologies I frequently work with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Python&lt;/li&gt;
&lt;li&gt;Java&lt;/li&gt;
&lt;li&gt;Redis&lt;/li&gt;
&lt;li&gt;SQL&lt;/li&gt;
&lt;li&gt;Linux systems&lt;/li&gt;
&lt;li&gt;REST/gRPC architectures&lt;/li&gt;
&lt;li&gt;Distributed caching systems&lt;/li&gt;
&lt;li&gt;Cloud infrastructure&lt;/li&gt;
&lt;li&gt;Performance optimization tooling&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Currently spending more time improving my:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Go&lt;/li&gt;
&lt;li&gt;Rust&lt;/li&gt;
&lt;li&gt;AI systems engineering skills&lt;/li&gt;
&lt;/ul&gt;




&lt;h1&gt;
  
  
  Why I Built My Portfolio Website
&lt;/h1&gt;

&lt;p&gt;I recently launched my personal website:&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://swapnanilsaha.com" rel="noopener noreferrer"&gt;https://swapnanilsaha.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The goal is to create a central place for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;projects&lt;/li&gt;
&lt;li&gt;engineering writeups&lt;/li&gt;
&lt;li&gt;architecture ideas&lt;/li&gt;
&lt;li&gt;experiments&lt;/li&gt;
&lt;li&gt;future AI infrastructure work&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I also wanted a clean place to document lessons from building production systems and exploring modern AI infrastructure.&lt;/p&gt;




&lt;h1&gt;
  
  
  Topics I’ll Be Writing About
&lt;/h1&gt;

&lt;p&gt;Going forward, I plan to write about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;scaling backend systems&lt;/li&gt;
&lt;li&gt;low-latency engineering&lt;/li&gt;
&lt;li&gt;distributed systems architecture&lt;/li&gt;
&lt;li&gt;AI infra tooling&lt;/li&gt;
&lt;li&gt;production ML systems&lt;/li&gt;
&lt;li&gt;performance optimization&lt;/li&gt;
&lt;li&gt;backend engineering patterns&lt;/li&gt;
&lt;li&gt;practical engineering lessons from real systems&lt;/li&gt;
&lt;/ul&gt;




&lt;h1&gt;
  
  
  Open to Connecting
&lt;/h1&gt;

&lt;p&gt;I enjoy discussing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;distributed systems&lt;/li&gt;
&lt;li&gt;backend architecture&lt;/li&gt;
&lt;li&gt;AI infrastructure&lt;/li&gt;
&lt;li&gt;performance engineering&lt;/li&gt;
&lt;li&gt;scalable systems design&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Feel free to connect through my website or social profiles.&lt;/p&gt;

&lt;p&gt;🌐 Website: &lt;a href="https://swapnanilsaha.com" rel="noopener noreferrer"&gt;swapnanilsaha.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;💻 GitHub: &lt;a href="https://github.com/swapnanil" rel="noopener noreferrer"&gt;github.com/swapnanil&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;🔗 LinkedIn: &lt;a href="https://www.linkedin.com/in/swapnanil/" rel="noopener noreferrer"&gt;linkedin.com/in/swapnanil&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Thanks for reading 🚀&lt;/p&gt;

</description>
      <category>ai</category>
      <category>backend</category>
      <category>distributedsystems</category>
      <category>infrastructure</category>
    </item>
  </channel>
</rss>
