<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: jc pong</title>
    <description>The latest articles on DEV Community by jc pong (@jc_pong_6e35d218054d901f2).</description>
    <link>https://dev.to/jc_pong_6e35d218054d901f2</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4180835%2F3899a242-29c1-4643-9bb9-73fbc8d8a9c1.png</url>
      <title>DEV Community: jc pong</title>
      <link>https://dev.to/jc_pong_6e35d218054d901f2</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jc_pong_6e35d218054d901f2"/>
    <language>en</language>
    <item>
      <title>OnCall Brain: Finding the Right Person, the Current Rule, and the Evidence</title>
      <dc:creator>jc pong</dc:creator>
      <pubDate>Sat, 17 Oct 2026 07:58:54 +0000</pubDate>
      <link>https://dev.to/jc_pong_6e35d218054d901f2/oncall-brain-finding-the-right-person-the-current-rule-and-the-evidence-3jc5</link>
      <guid>https://dev.to/jc_pong_6e35d218054d901f2/oncall-brain-finding-the-right-person-the-current-rule-and-the-evidence-3jc5</guid>
      <description>&lt;p&gt;&lt;em&gt;An on-call knowledge graph that keeps document curation, technical responsibility, and policy history explicit.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A new on-call engineer can retrieve the right document and still make the wrong decision. The name beside a runbook might belong to its editor. A timeout setting might have been replaced. A confident chat message might contradict the operating procedure.&lt;/p&gt;

&lt;p&gt;Those are the problems behind OnCall Brain, a project for FalkorDB Graph Hacks. It joins operations documents, organisation records, chats, and support tickets in a knowledge graph, then answers questions with source-line evidence.&lt;/p&gt;

&lt;p&gt;All examples below come from the project's synthetic corpus for the fictional company AetherPulse. They are reproducible test cases, not reports from a production deployment.&lt;/p&gt;

&lt;p&gt;The engineering question is specific. Can we make the relationships needed for an operational decision explicit enough that an answer can be inspected?&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the data comes from
&lt;/h2&gt;

&lt;p&gt;Picture a platform operations team. Users report problems through a ticketing system, here called Sentinel Desk: each ticket records the module, the first-line engineer it was assigned to, and how it was resolved. The real work happens elsewhere, in a dedicated channel on the company's internal instant-messaging tool, where first-line and Tier 2 engineers ask each other questions, claim tickets, hand them over, and announce rule changes. Around both sit formal documents (SOPs, a module registry, an escalation policy) and an organisation roster.&lt;/p&gt;

&lt;p&gt;The synthetic corpus mirrors that setup: 10 formal documents, 18 days of channel logs, 95 tickets, and the org records.&lt;/p&gt;

&lt;p&gt;Chat and tickets meet on the ticket ID. The ticket system says TCK-1014 was assigned to Alan Cox. The channel log adds two lines: Dominic Shaw writes, "I am transferring TCK-1014 to Colin on Sentinel Desk right now", and later Colin Marsh writes, "Verified TCK-1014 closure". Extraction turns those lines into &lt;code&gt;HANDLED_BY&lt;/code&gt; edges on the same Ticket node, each citing its chat line and marked &lt;code&gt;observation&lt;/code&gt;, next to the ticket system's own record. Across the corpus, the ticket system contributes 95 handler edges and chat adds 26 more; 21 tickets end up with handlers from both sources. That is what lets the graph answer "who has actually fixed this?" with people the ticket system alone would miss.&lt;/p&gt;

&lt;p&gt;This join depends on someone typing the ticket ID. In practice, people often discuss a problem without naming the ticket, describing a symptom that resembles one ("the certificate handshake is timing out again"). OnCall Brain does not link those today: entities align only on identical IDs, so a resemblance never becomes a graph fact. At query time, keyword search over all evidence lines, ticket records included, can still surface the similar ticket and the chat thread side by side, but only as retrieved context, not as an edge, and not in the "who has fixed it" count. The natural extension is a similarity pass that proposes candidate &lt;code&gt;SIMILAR_TO&lt;/code&gt; edges between chat threads and tickets, kept out of answers until a person approves them.&lt;/p&gt;

&lt;p&gt;Chat also carries rules. The change of the default step timeout from 120 to 60 minutes, used below, was announced in the channel, not in a document, so it enters the graph as a &lt;code&gt;SUPERSEDES&lt;/code&gt; edge with chat evidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  The runbook editor is the wrong escalation target
&lt;/h2&gt;

&lt;p&gt;One test asks whether Wesley Tate should log into a backend server and terminate a zombie worker when a Pipeline runner deadlocks.&lt;/p&gt;

&lt;p&gt;Wesley is a plausible search result. He reviews Knowledge Hub articles and coordinates submissions. His name belongs near Pipeline documentation. But the module's technical lead is Colin Marsh.&lt;/p&gt;

&lt;p&gt;The distinction is encoded directly in the graph. A Resource points to its curator through &lt;code&gt;MAINTAINED_BY&lt;/code&gt;. A Person points to a Module through &lt;code&gt;TECH_LEAD_OF&lt;/code&gt;. The ontology explicitly forbids deriving technical responsibility from document maintenance. Unregistered predicates are rejected, and each assertion's domain and range are validated.&lt;/p&gt;

&lt;p&gt;The answer should correct the question's premise, direct the ticket to the Pipeline queue, and identify Colin. The walkthrough demonstrates that correction; the frozen evaluation also records this trap as a pass.&lt;/p&gt;

&lt;p&gt;A text or vector search can retrieve the supporting passages. Similarity alone does not establish what Wesley's relationship to Pipeline permits him to do. A model could reason correctly from those passages, but it would have to reconstruct the responsibility distinction during generation. Here, that distinction is part of the retrieval contract.&lt;/p&gt;

&lt;p&gt;Ticket history adds a second view. Someone who handled a module's tickets may offer practical help without being its official owner. OnCall Brain joins tickets to modules and handlers, then presents that history alongside ownership. It does not treat past handling as proof of current availability or authority.&lt;/p&gt;

&lt;h2&gt;
  
  
  The graph behind those distinctions
&lt;/h2&gt;

&lt;p&gt;The relationships are not invented on top of the data; they are read off the structure the sources already have. The module registry is a table with a header note, "Review Cadence: Weekly review by Wesley Tate", and rows such as:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Module ID&lt;/th&gt;
&lt;th&gt;Module Name&lt;/th&gt;
&lt;th&gt;Tier 2 Lead&lt;/th&gt;
&lt;th&gt;Content Platform&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;MOD-01&lt;/td&gt;
&lt;td&gt;Pipeline Orchestration Engine&lt;/td&gt;
&lt;td&gt;Colin Marsh&lt;/td&gt;
&lt;td&gt;Knowledge Hub&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A deterministic parser, with no LLM involved, maps one column to one relationship. The Tier 2 Lead column, defined in the same file as the technical owner accountable for runbook accuracy, becomes &lt;code&gt;Colin Marsh -TECH_LEAD_OF-&amp;gt; MOD-01&lt;/code&gt;. The platform column becomes &lt;code&gt;MOD-01 -HAS_RESOURCE-&amp;gt; Knowledge Hub&lt;/code&gt;. The review note becomes &lt;code&gt;Knowledge Hub -MAINTAINED_BY-&amp;gt; Wesley Tate&lt;/code&gt;. Rosters and team tables are handled the same way: a Team Lead column becomes a &lt;code&gt;HOLDS_ROLE&lt;/code&gt; edge to a role node.&lt;/p&gt;

&lt;p&gt;What about a title the schema does not know, such as "Support Lead"? Nothing is inferred from it. A new column in a structured source is ignored until someone decides which relationship it means. In free text, the model must pick from the nineteen registered relationships, so "Ava is the support lead for Pipeline" can at most become a role assignment, and a role never implies &lt;code&gt;TECH_LEAD_OF&lt;/code&gt;. Adding a relationship type is a schema decision with an explicit domain, range, and meaning. That is deliberate: "support lead" means escalation owner in one company and shift coordinator in another.&lt;/p&gt;

&lt;p&gt;The free-text path has a limit worth stating. Quote validation proves that the cited line exists and says what the assertion quotes; it does not prove the model chose the right relationship type. That choice is guided by the predicate definitions in the extraction prompt, not checked by code.&lt;/p&gt;

&lt;p&gt;This abbreviated model uses concrete source-line locators on its domain edges. &lt;code&gt;registry&lt;/code&gt; means &lt;code&gt;08-module-knowledge-base-registry.md&lt;/code&gt;; chat and ticket locators name their files directly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Colin Marsh (Person) --TECH_LEAD_OF [registry L17]--&amp;gt; MOD-01 (Module)
MOD-01 (Module) --HAS_RESOURCE [registry L17]--&amp;gt; Knowledge Hub (Resource)
Knowledge Hub (Resource) --MAINTAINED_BY [registry L9]--&amp;gt; Wesley Tate (Person)

TCK-1017 (Ticket) --ABOUT [tickets.jsonl L17]--&amp;gt; MOD-01 (Module)
TCK-1017 (Ticket) --HANDLED_BY [tickets.jsonl L17]--&amp;gt; Robert Coleman (Person)

Notice: 60-minute timeout --SUPERSEDES [chat_2026-08-20.md L28]--&amp;gt; Rule: 120-minute timeout
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Evidence is not drawn above because it is not wired into the business graph. Each Evidence node stores a source file, line, and quoted text; each relationship stores the IDs of its Evidence nodes in an &lt;code&gt;evidence&lt;/code&gt; property, and citations are resolved through those IDs. Business nodes carry &lt;code&gt;Entity&lt;/code&gt;; Evidence nodes do not, keeping evidence records out of business traversals.&lt;/p&gt;

&lt;p&gt;Every assertion carries an ID, authority, validity dates, state, and evidence references. Answer citations pair assertion IDs with &lt;code&gt;file:line&lt;/code&gt;. That makes the unit of inspection a supported relationship, rather than an unexplained connection between names.&lt;/p&gt;

&lt;h2&gt;
  
  
  Yesterday's correct timeout can be today's wrong answer
&lt;/h2&gt;

&lt;p&gt;The original corpus contains a change in the default Pipeline step timeout from 120 minutes to 60 minutes, effective 2026-08-20. Both values remain useful evidence. Only one belongs in the current default answer.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;SUPERSEDES&lt;/code&gt; preserves that history. A visible replacement excludes its target from current answers while retaining the old rule, replacement, and effective date for inspection.&lt;/p&gt;

&lt;p&gt;The freshness demo extends the example in an isolated graph. It copies the loaded graph, injects a notice replacing the 60-minute timeout with 30 minutes, and repeats the same read. The old card becomes superseded; the new notice and evidence appear. The resulting chain is 120 → 60 → 30.&lt;/p&gt;

&lt;p&gt;The 30-minute rule is an injected demonstration, not an original AetherPulse policy. The documented verification covers graph reads and reset; it does not establish that generated answers or browser layout were tested. The demo page reads the graph without an LLM.&lt;/p&gt;

&lt;p&gt;Retrieval by similarity can return every version of a timeout policy. Ranking the newest passage can help, but it still leaves questions about replacement scope and effective dates. A replacement edge records which rule changed. Future notices remain invisible until their effective date.&lt;/p&gt;

&lt;p&gt;Visibility is enforced server-side at every traversal hop. A relationship must be approved, its start date must have arrived, and its end date must be absent or still ahead of the query time. A matching sentence cannot bypass those conditions.&lt;/p&gt;

&lt;h2&gt;
  
  
  A chat message confuses claiming with diagnosis
&lt;/h2&gt;

&lt;p&gt;Another test starts with Luke Fischer's belief that a P3 ticket can remain unclaimed for four hours. The formal workflow training guide penalizes tickets left unassigned beyond 15 minutes. Sophia Evans's chat correction separates the deadlines explicitly. Claim within 15 minutes, provide an initial technical response within 30 minutes, and understand four hours as the initial diagnostic window.&lt;/p&gt;

&lt;p&gt;These passages share the same vocabulary while describing different stages. A retrieved “P3, four hours” snippet can therefore reinforce the wrong premise even when it is topically relevant.&lt;/p&gt;

&lt;p&gt;The graph's assertions retain authority and semantic slots. Its authority order is &lt;code&gt;formal_rule&lt;/code&gt;, then &lt;code&gt;practice&lt;/code&gt;, then &lt;code&gt;observation&lt;/code&gt;. Chat defaults to observation; an announced policy can be practice, but chat is never automatically promoted to a formal rule.&lt;/p&gt;

&lt;p&gt;Conflict checks require claims about the same subject and slot, mutually exclusive values, compatible conditions, and overlapping validity intervals. This matters because a claim deadline and a diagnostic deadline can coexist. The mistaken claim-window belief needs correction; the diagnostic window does not need deletion.&lt;/p&gt;

&lt;p&gt;The answer can lay out the disagreement and cite both the mistaken chat statement and the governing evidence. The frozen Q13 result records that behavior as a pass. This is a useful retrieval advantage, not proof that vector search could never answer the question.&lt;/p&gt;

&lt;h2&gt;
  
  
  The model selects tools; the server owns Cypher
&lt;/h2&gt;

&lt;p&gt;The model never writes executable Cypher. It selects tool arguments, registered predicates, direction, and hop count. Server-owned templates perform the queries.&lt;/p&gt;

&lt;p&gt;For example, the following is copied verbatim from the README's query for &lt;code&gt;oncall_brain/tools.py:module_expertise&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cypher"&gt;&lt;code&gt;&lt;span class="k"&gt;MATCH&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;k:&lt;/span&gt;&lt;span class="n"&gt;Entity&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="py"&gt;a:&lt;/span&gt;&lt;span class="n"&gt;ABOUT&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;m:&lt;/span&gt;&lt;span class="n"&gt;Entity&lt;/span&gt; &lt;span class="ss"&gt;{&lt;/span&gt;&lt;span class="py"&gt;id:&lt;/span&gt; &lt;span class="n"&gt;$id&lt;/span&gt;&lt;span class="ss"&gt;}),&lt;/span&gt; &lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="py"&gt;h:&lt;/span&gt;&lt;span class="n"&gt;HANDLED_BY&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;p:&lt;/span&gt;&lt;span class="n"&gt;Entity&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;a.state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'approved'&lt;/span&gt; &lt;span class="ow"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;a.valid_from&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;$now&lt;/span&gt; &lt;span class="n"&gt;AND&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a.valid_to&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="ow"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;a.valid_to&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;$now&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;h.state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'approved'&lt;/span&gt; &lt;span class="ow"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;h.valid_from&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;$now&lt;/span&gt; &lt;span class="n"&gt;AND&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;h.valid_to&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="ow"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;h.valid_to&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;$now&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="n"&gt;k.id&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k.kind&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k.name&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;m.kind&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;m.name&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p.id&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p.kind&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p.name&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;a.aid&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;a.authority&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;a.evidence&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;h.aid&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;h.authority&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;h.evidence&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both ticket relationships receive visibility checks, and both return evidence. My reason for preferring this boundary is reviewability. Predicate semantics and filtering can be checked in a fixed query rather than trusted to each generated query. The evaluation reports also record executed template names and tool arguments.&lt;/p&gt;

&lt;p&gt;Ingest has a similar boundary. Structured organisation and ticket records are parsed without an LLM. Free text goes through model proposal, schema and evidence validation, entity alignment, and semantic annotation. Accepted assertions begin as candidates. A quoted evidence line that does not match its source causes rejection.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the measurements actually support
&lt;/h2&gt;

&lt;p&gt;The frozen deep-mode evaluation passed &lt;strong&gt;15 of 17&lt;/strong&gt; gold questions. The walkthrough reports &lt;strong&gt;14 of 17&lt;/strong&gt; for fast mode; the frozen report is specifically a deep-agent run. These are project test results on synthetic data, not a production accuracy claim or a measured victory over vector retrieval.&lt;/p&gt;

&lt;p&gt;The failures are informative. Q1 lost required-point coverage. Q7 included all required points but returned &lt;code&gt;insufficient_context&lt;/code&gt; where the card expected &lt;code&gt;applicable&lt;/code&gt;. Retrieving facts and presenting the correct answer state remain separate problems.&lt;/p&gt;

&lt;p&gt;A cold-cache full ingest took about &lt;strong&gt;2 hours 10 minutes&lt;/strong&gt; in the documented clean-checkout test, including provider timeouts. Cached completed slices help resume extraction, but they do not remove cold-start cost.&lt;/p&gt;

&lt;p&gt;Conflict validation also has a limit. The Web check accepts a graph conflict record or at least two distinct source-line references in the Conflicts section. Counting locators does not independently prove that the passages contradict. Evidence makes an answer inspectable; it still needs interpretation.&lt;/p&gt;

&lt;p&gt;Try the ownership trap, inspect its citations, then follow a replaced rule through its history. Those checks show what this graph contributes to an on-call decision.&lt;/p&gt;

&lt;p&gt;Repository: &lt;a href="https://github.com/jsonpang/oncall-brain" rel="noopener noreferrer"&gt;OnCall Brain&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Demo video: &lt;a href="https://youtu.be/gifjF3u3s0c" rel="noopener noreferrer"&gt;https://youtu.be/gifjF3u3s0c&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;AI development assistance included Claude Code for implementation, review, and fixes; Codex for features and documentation; Antigravity CLI for frontend pages; and Hunyuan &lt;code&gt;hy4-preview&lt;/code&gt;, driven through Claude Code, for backend KB health and a conflict fix.&lt;br&gt;
1&lt;/p&gt;

</description>
      <category>falkordb</category>
      <category>knowledgegraph</category>
      <category>hackathon</category>
      <category>ai</category>
    </item>
    <item>
      <title>Update2Report: turning messy Friday updates into a client-ready weekly report with an open-weight model</title>
      <dc:creator>jc pong</dc:creator>
      <pubDate>Fri, 16 Oct 2026 07:08:32 +0000</pubDate>
      <link>https://dev.to/jc_pong_6e35d218054d901f2/update2report-turning-messy-friday-updates-into-a-client-ready-weekly-report-with-an-open-weight-21ec</link>
      <guid>https://dev.to/jc_pong_6e35d218054d901f2/update2report-turning-messy-friday-updates-into-a-client-ready-weekly-report-with-an-open-weight-21ec</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-week2-2026-10-12"&gt;Hacktoberfest Open-Source AI Challenge: Week 2 (Side Hustle)&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;Update2Report takes the messy updates a small team posts in chat during the week, plus an optional task CSV, and turns them into a weekly report you can send to a client: a short summary, a printable HTML page, and an Excel workbook.&lt;/p&gt;

&lt;p&gt;The person I built it for is the lead of a 5 to 20 person outsourcing or ops team. Every Friday they scroll back through stand-up messages, copy lines into a document, count what got done, and tidy the wording. That is about two hours of work that nobody enjoys and nobody bills for. I think that is a task someone would pay to stop doing.&lt;/p&gt;

&lt;p&gt;The thing I cared about most was trust. A report that sends a client "5 docs written" when the team wrote 4 is worse than no report. So the model does the language work, and plain Python does the counting and checking.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;Live demo: &lt;a href="http://8.138.157.149" rel="noopener noreferrer"&gt;http://8.138.157.149&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Paste updates like these (all fictional):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Alice: finished login page redesign, 2 bugs fixed, blocked on API spec.
Bob: migrated billing cron to new scheduler; load test pending.
Chen: wrote 5 onboarding docs; started FAQ, 40% done.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The generated summary kept the 2, the 5 and the 40%, and did not add any people, projects or numbers that were not in the input. You also get a statistics table with Project, Owner, Completed, In progress and Blockers.&lt;/p&gt;

&lt;p&gt;A few things about the demo itself:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;There are buttons for cached sample inputs. They cost nothing and always work.&lt;/li&gt;
&lt;li&gt;Live generation has a per-IP hourly cap and a daily cap, and I can switch it off with one setting. When it is off, or when the model endpoint is unreachable, the page says cached samples still work instead of showing a stack trace.&lt;/li&gt;
&lt;li&gt;Please do not paste anything sensitive into a public demo.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fza18n1w2l98vsrv4duy1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fza18n1w2l98vsrv4duy1.png" alt="Home page" width="800" height="538"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1pg4vv0c9ti0e1jr501c.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1pg4vv0c9ti0e1jr501c.png" alt="Report generated from a cached sample" width="800" height="741"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzufd8vnozs3wwda6qgdt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzufd8vnozs3wwda6qgdt.png" alt="The example above, generated live" width="800" height="401"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/jsonpang" rel="noopener noreferrer"&gt;
        jsonpang
      &lt;/a&gt; / &lt;a href="https://github.com/jsonpang/update2report" rel="noopener noreferrer"&gt;
        update2report
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
  &lt;tbody&gt;
  &lt;tr&gt;
    &lt;th&gt;title&lt;/th&gt;
    &lt;td&gt;Update2Report&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;th&gt;emoji&lt;/th&gt;
    &lt;td&gt;📋&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;th&gt;colorFrom&lt;/th&gt;
    &lt;td&gt;blue&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;th&gt;colorTo&lt;/th&gt;
    &lt;td&gt;indigo&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;th&gt;sdk&lt;/th&gt;
    &lt;td&gt;gradio&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;th&gt;sdk_version&lt;/th&gt;
    &lt;td&gt;5.50.0&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;th&gt;python_version&lt;/th&gt;
    &lt;td&gt;3.11&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;th&gt;app_file&lt;/th&gt;
    &lt;td&gt;app.py&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;th&gt;pinned&lt;/th&gt;
    &lt;td&gt;false&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;th&gt;license&lt;/th&gt;
    &lt;td&gt;mit&lt;/td&gt;
  &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Update2Report&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;Turn messy team notes into a printable weekly HTML report and an Excel workbook. Open-weight models are the core of Update2Report—they extract structured tasks from unstructured updates and draft natural summaries, while deterministic Python code computes trusted statistics and cross-checks every number in the summary against ground truth. The public demo runs DeepSeek's open-weight DeepSeek-V4.1-Flash (MIT) through an OpenAI-compatible gateway, with OpenAI's open-weight gpt-oss-120b (Apache 2.0) as the fallback model; point &lt;code&gt;LLM_BASE_URL&lt;/code&gt; at another provider or a local Ollama instance to use a different backend. All bundled Northwind Studio data is fictional.&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Run locally&lt;/h2&gt;
&lt;/div&gt;

&lt;div class="highlight highlight-source-powershell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;git clone https:&lt;span class="pl-k"&gt;//&lt;/span&gt;&lt;span class="pl-c1"&gt;github.com&lt;/span&gt;&lt;span class="pl-k"&gt;/&lt;/span&gt;jsonpang&lt;span class="pl-k"&gt;/&lt;/span&gt;update2report.git
cd update2report
python &lt;span class="pl-k"&gt;-&lt;/span&gt;m pip install &lt;span class="pl-k"&gt;-&lt;/span&gt;r requirements.txt
&lt;span class="pl-c1"&gt;Copy-Item&lt;/span&gt; .env.example .env
&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; Edit .env with your own endpoint, key and model, then start:&lt;/span&gt;
python&lt;/pre&gt;…
&lt;/div&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/jsonpang/update2report" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;



&lt;p&gt;MIT licensed. The Python package is in &lt;code&gt;update2report/&lt;/code&gt;, the Gradio app is &lt;code&gt;app.py&lt;/code&gt;, and there are 52 tests.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Stack.&lt;/strong&gt; Python, Gradio for the UI, and an OpenAI-compatible client for the model. The model is DeepSeek's open-weight DeepSeek-V4.1-Flash, licensed under MIT, with OpenAI's open-weight gpt-oss-120b (Apache 2.0) as the fallback. The model connection is three environment variables, &lt;code&gt;LLM_BASE_URL&lt;/code&gt;, &lt;code&gt;LLM_API_KEY&lt;/code&gt; and &lt;code&gt;LLM_MODEL&lt;/code&gt;, so swapping the model or the host means changing those and no code. The public demo has a few more settings of its own (rate limits and a switch to pause live generation). An optional &lt;code&gt;LLM_FALLBACK_MODEL&lt;/code&gt; retries once on a second model if the first is rate-limited or down.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Split of work.&lt;/strong&gt; The model does two jobs: pull structured tasks (project, owner, status, blockers) out of free text, and write the summary in either an internal or a client-facing tone. Everything that can be a fact is computed in code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Checks around the model.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;em&gt;Extraction validation.&lt;/em&gt; The extracted JSON is validated deterministically. If it is malformed, the model gets one correction attempt.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Number preservation.&lt;/em&gt; Every number and percentage in the source must appear in the summary. If one is missing, the summary is regenerated once. If it is still missing, the report appends a "Numbers from source:" list, so a figure is never silently dropped.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;No invention.&lt;/em&gt; Any number in the summary, digits or words like "three", must exist in the source, and capitalized names in the summary (people, projects) must appear in the source too. Otherwise the draft is rejected and rewritten once with the reason attached. The name check is a simple word match, so it catches made-up names, not every wrong attribution.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Statistics from code.&lt;/em&gt; The Project / Owner / Completed / In progress / Blockers table is computed in Python, not written by the model. If a CSV is supplied, chat items and CSV tasks are deduplicated by task name within each project, so the same task is not counted twice.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;These checks do not prove that extraction is semantically perfect. They make sure the numbers a client sees match what the team wrote, and when something is off, the failure is visible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AI assistance.&lt;/strong&gt; I used AI coding assistants (Codex and Claude) while writing the code and tests. I reviewed and ran the results myself, and the design above is mine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Does Open Innovation Matter?
&lt;/h2&gt;

&lt;p&gt;Privacy is the main reason, and cost is second.&lt;/p&gt;

&lt;p&gt;A weekly report contains client names, delivery status and sometimes complaints. A team lead may not be allowed to paste that into a hosted chatbot, and a client may not want them to. Because the model is open-weight and the app speaks to any OpenAI-compatible endpoint, you can point &lt;code&gt;LLM_BASE_URL&lt;/code&gt; at a local Ollama server and the notes never leave your machine. With a closed model that option does not exist.&lt;/p&gt;

&lt;p&gt;Cost is the other half. The task is small: a few thousand characters in, a few hundred out, once a week. A small open model on hardware you already own is enough, and there is no per-seat or per-token bill to explain to a client. I also did not have to build around one vendor's API, which matters for a tool I may want to sell or hand over.&lt;/p&gt;

&lt;p&gt;Where it did not do better: a larger hosted model writes smoother prose. That is why the checks exist. I would rather have a slightly plainer summary with correct numbers.&lt;/p&gt;

&lt;h2&gt;
  
  
  My First User
&lt;/h2&gt;

&lt;p&gt;I do not have a paying user yet. Before the deadline I plan to ask a few team leads to run it on a real week of their own notes, and I will update this post with what they say, including whether anyone would pay.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Test with local Ollama models of different sizes and record how often the number check has to fall back to the "Numbers from source" list.&lt;/li&gt;
&lt;li&gt;Support more input formats, such as exported chat logs.&lt;/li&gt;
&lt;li&gt;Offer a hosted version for people who do not want to run it themselves, and a self-hosted setup for people who care most about privacy.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Prize Categories
&lt;/h2&gt;

&lt;p&gt;None that I am aware of fit, so I am not listing any.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
