<?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: Richard Rodger</title>
    <description>The latest articles on DEV Community by Richard Rodger (@rjrodger).</description>
    <link>https://dev.to/rjrodger</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%2F690343%2Fdcb25037-125a-485d-9e84-4505ac0d2cb5.jpeg</url>
      <title>DEV Community: Richard Rodger</title>
      <link>https://dev.to/rjrodger</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/rjrodger"/>
    <language>en</language>
    <item>
      <title>The Voxgig Podcast Chatbot: Triggering Ingestion, and some Debugging DX</title>
      <dc:creator>Richard Rodger</dc:creator>
      <pubDate>Fri, 22 Mar 2024 14:45:10 +0000</pubDate>
      <link>https://dev.to/rjrodger/the-voxgig-podcast-chatbot-triggering-ingestion-and-some-debugging-dx-3fja</link>
      <guid>https://dev.to/rjrodger/the-voxgig-podcast-chatbot-triggering-ingestion-and-some-debugging-dx-3fja</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;This is the third post in a series I'm writing about a new &lt;a href="https://www.voxgig.com/podcast"&gt;Minimal Viable Product&lt;/a&gt; we've released at Voxgig that turns your podcast into a chatbot. Your visitors can now ask your guests questions directly! The first post is here: &lt;a href="https://www.richardrodger.com/2024/02/20/building-a-podcast-chatbot-for-voxgig/"&gt;Building a Podcast Chatbot for Voxgig&lt;/a&gt; and you find the full list at the end of this post.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The problem with podcasts is that they tend to have new episodes. This is annoying because you can't just ingest all the episodes once and be done with it. You have to set up a regular process (once a day is sufficient) to download the podcast RSS feed, check for new episodes and changes to old episodes, and run part of the data ingestion process again.&lt;/p&gt;

&lt;p&gt;To model this business logic, the system uses two separate messages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;aim:ingest,subscribe:podcast&lt;/code&gt;: subscribe to a podcast, called only once to setup the podcast in the system&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;aim:ingest:ingest:podcast&lt;/code&gt;: actually process each episode and ingest the content&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;code&gt;ingest:podcast&lt;/code&gt; message is the one we will run on a daily basis. For the moment, let's ignore the devops side of this system (it's still under development at the time of writing), and focus on processing the episodes of a podcast.&lt;/p&gt;

&lt;p&gt;The podcast RSS gives us the list of episodes, so we need to download the RSS to get the latest version. This does mean that on the initial subscription, in our system, the RSS gets downloaded twice. We can easily solve this problem by adding a parameter to the &lt;code&gt;ingest:podcast&lt;/code&gt; message (and we may yet do that in future), but for now, we are deliberately not going to solve this problem. The other more important parts of the codebase to work on. Downloading the same RSS twice to complete a one-time business process is not exactly a fundamental issue. Let's invoke &lt;a href="https://wiki.c2.com/?PrematureOptimization"&gt;"premature optimization is the root of all evil"&lt;/a&gt; and leave it at that for the moment.&lt;/p&gt;

&lt;p&gt;This little decision lets us keep the &lt;code&gt;ingest:podcast&lt;/code&gt; message implementation simpler for now. Let's look at the code, but this time also think about error handling. This message assumes we have already subscribed to a podcast in our system. That, for the moment, just means we saved an entry in the &lt;code&gt;pdm/podcast&lt;/code&gt; table.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Since we have abstracted away the database (see the &lt;a href="https://www.richardrodger.com/2024/02/28/the-voxgig-podcast-chatbot-first-subscribe/"&gt;previous post&lt;/a&gt; in this series), we'll use the term &lt;em&gt;table&lt;/em&gt; to refer to a generic idea of an entity store that can persist records that look like JSON documents, and can query these records, at least using to level properties. That's pretty much all we need. We use the convention &lt;code&gt;entity-base/entity-name&lt;/code&gt; to describe these entity stores and give them some namespacing.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The podcast entry must exist, so we need to know its ID. This must be a parameter to the message action, let's call it &lt;code&gt;podcast_id&lt;/code&gt;. We try to load this podcast and need to respond with an error if it does not exist.&lt;/p&gt;

&lt;p&gt;Is this error a system error, one that is in some way fatal to the process, or would leave to corrupted data? Nope. This is an ordinary everyday business logic bug. Somehow a podcast ID got sent to us that isn't correct. We just reject it, but there's no need to panic. Thus, we don't throw an exception, or return a bad HTTP error, or do anything out of the ordinary. We just respond with a JSON message that indicates, by a convention of its schema, that there was a business problem.&lt;/p&gt;

&lt;p&gt;The general schema that we use for message responses (when there is one!) is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// If true, message was accepted and processed successfully&lt;/span&gt;
  &lt;span class="nx"&gt;why&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// If ok is false, optionally supply a free-form debugging code useful to humans&lt;/span&gt;
  &lt;span class="c1"&gt;// Other properties, perhaps well-defined, providing additional response data&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's put all this together in code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;ingest_podcast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&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;seneca&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;debug&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;seneca&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;shared&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;debug&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;action&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;out&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;why&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&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;podcast_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;podcast_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;podcast_id&lt;/span&gt;

  &lt;span class="nx"&gt;debug&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nf"&gt;debug&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;START&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;podcast_id&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;podcastEnt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;seneca&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pdm/podcast&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;load&lt;/span&gt;&lt;span class="nf"&gt;$&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;podcast_id&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="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nx"&gt;podcastEnt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;why&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;podcast-not-found&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
    &lt;span class="nx"&gt;debug&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nf"&gt;debug&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;FAIL-PODCAST-ENTITY&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;podcast_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;out&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Process episodes here...&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;out&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's useful to have a "debug" mode for your microservices, that can produce additional log entries for debugging, both locally and when deployed. Some log entries might involve additional work to generate, so you want to avoid that when running normally.&lt;/p&gt;

&lt;p&gt;Also, it's tedious to keep adding context information to each log entry, such as the name of the microservice, the name of the message action function, and so on. Thus we make use of a shared utility that provides a debug function: &lt;code&gt;seneca.shared.debug&lt;/code&gt;, and we pass in the current message details (the &lt;code&gt;meta&lt;/code&gt; parameter) so it can generate a full log entry.&lt;/p&gt;

&lt;p&gt;If debug mode is not enabled, &lt;code&gt;seneca.shared.debug&lt;/code&gt; will return &lt;code&gt;null&lt;/code&gt;, so we can use that to short circuit any costly log entry code, using the idiom:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;debug &amp;amp;&amp;amp; debug(...)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We'll cover shared utilities in a later post, but if you want to look at the code, review &lt;a href="https://github.com/voxgig/podmind/blob/main/backend/src/srv/ingest/ingest-prepare.ts"&gt;ingest-prepare.ts&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Our code tries to load the podcast, and if it is not found, bails immediately by returning the response:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  ok: false,
  podcast_id: '...',
  why: 'podcast-not-found'
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We do a few things here to make debugging easier. When deployed, debugging becomes harder, so the more information you can give yourself, the better.&lt;/p&gt;

&lt;p&gt;As well as the expected &lt;code&gt;ok&lt;/code&gt; and &lt;code&gt;why&lt;/code&gt; properties, we also include the &lt;code&gt;podcast_id&lt;/code&gt; parameter in the response. In a system under high load, you might only see the body of the response in the current section of the log that you're looking at, so give yourself as much context as possible.&lt;/p&gt;

&lt;p&gt;For debug mode, we also emit a log entry, and add a test prefix that is easier to identify or search for: &lt;code&gt;"FAIL-PODCAST-ENTITY"&lt;/code&gt;. Ultimately you'll want to look at the line of code that caused the problem, but having a unique string is a great help when isolating the flow of the error. It also helps a great deal with working remotely with colleagues. This is a poor substitute for proper error codes for sure, but is a classic &lt;a href="https://en.wikipedia.org/wiki/Pareto_principle"&gt;80/20 solution&lt;/a&gt; that can be improved as needed.&lt;/p&gt;

&lt;p&gt;You'll also notice some defensive pessimistic code here. We assume the message will fail, and initialize &lt;code&gt;ok: false&lt;/code&gt;. This also helps us be lazy developers, as we only have to set the &lt;code&gt;why&lt;/code&gt; property when &lt;a href="https://gomakethings.com/the-early-return-pattern-in-javascript/"&gt;returning early&lt;/a&gt; due to an error.&lt;/p&gt;

&lt;p&gt;Most error conditions in this codebase are handled in the same way, and you can see them in the &lt;a href="https://github.com/voxgig/podmind/tree/main/backend"&gt;source code&lt;/a&gt;. We'll omit them in most cases it the rest of this series to keep the example code shorter.&lt;/p&gt;

&lt;p&gt;Let's proceed to the next step: download and save the RSS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;batch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;batch&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;podcastEnt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;seneca&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pdm/podcast&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;load&lt;/span&gt;&lt;span class="nf"&gt;$&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;podcast_id&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;feed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;podcastEnt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;feed&lt;/span&gt;

  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;rssRes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;seneca&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;shared&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getRSS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;debug&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;feed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;podcast_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;mark&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;rss&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;rssRes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rss&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;feedname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;encodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;feed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^https&lt;/span&gt;&lt;span class="se"&gt;?&lt;/span&gt;&lt;span class="sr"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\/\/&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;seneca&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pdm/rss&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;save&lt;/span&gt;&lt;span class="nf"&gt;$&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rss01/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;feedname&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
      &lt;span class="nx"&gt;podcastEnt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;batch&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.rss&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;rss&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We introduce a new parameter, &lt;code&gt;batch&lt;/code&gt;, which is a string identifying the current podcast ingestion process. This is a classic "batch" job run once a day, so we create a unique identifier to help track this job (and you thought using LLMs and AI models was sexy! No, production coding is the same old boring thing it's always been since the 1950's - batch jobs!).&lt;/p&gt;

&lt;p&gt;The table &lt;code&gt;pdm/rss&lt;/code&gt; is special - it is a folder in an &lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/Welcome.html"&gt;S3 bucket&lt;/a&gt; where we dump the RSS files. It is not special within our code, however, and looks just like any other database table. We do specify the data entity &lt;code&gt;id&lt;/code&gt; directly, as this will become the file name in the S3 bucket.&lt;/p&gt;

&lt;p&gt;As we discussed in a previous post, using the same abstraction for different persistence mechanisms makes our code much cleaner and easier to refactor. Changing cloud provider, or even just deciding to use an actual database table in future, won't require any changes to this code. But more importantly, we can unit test our business logic locally without even setting up fake S3 docker containers or other nonsense.&lt;/p&gt;

&lt;p&gt;We store the RSS because we will need it for debugging, and we might want to use it in other ways later. Also, it is good to have a record of the state of a given podcast RSS at a given date and time so we can track changes.&lt;/p&gt;

&lt;p&gt;We get the individual episode details from the &lt;code&gt;items&lt;/code&gt; property of the RSS (we parse it using the &lt;a href="https://github.com/rbren/rss-parser"&gt;&lt;code&gt;rss-parser&lt;/code&gt; package&lt;/a&gt;) to handle RSS variants. We loop over each episode and emit a &lt;code&gt;process:episode&lt;/code&gt; event for each one.&lt;/p&gt;

&lt;p&gt;We're not bothering to check for changes to episodes here. That logic should live in &lt;code&gt;process:episode&lt;/code&gt; as that message "understands" episodes.&lt;/p&gt;

&lt;p&gt;We also include the episode data in each message. This is a calculated risk. In general, you should keep your messages small, and reference data using an id. But in this case, we can safely assume the RSS content is "reasonable". If the content is too large, we'll just let this message fail and then think about whether we even want to solve the problem later.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;episodes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;rss&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;

  &lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;episodes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;episodes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;
  &lt;span class="nx"&gt;episodeEnd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;episodes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&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;epI&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;episodeStart&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;epI&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;episodeEnd&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;epI&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="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;episode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;episodes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;epI&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;handleEpisode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;episode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;epI&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handleEpisode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;episode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;epI&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;seneca&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;aim:ingest,process:episode&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;episode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;podcast_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;batch&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="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;out&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When deployed, the &lt;code&gt;process:episode&lt;/code&gt; message is triggered asynchronously, but we still wait for the message queue to accept it (hence the &lt;code&gt;await&lt;/code&gt;). Locally it is just a normal message and we wait for the actual business logic to complete.&lt;/p&gt;

&lt;p&gt;Once we've sent all the &lt;code&gt;process:episode&lt;/code&gt; messages, we're done. We set &lt;code&gt;ok: true&lt;/code&gt; and return a response.&lt;/p&gt;

&lt;p&gt;In the next post in this series, we'll look at the processing of each episode, including (finally!) our first usage of an LLM, which we'll use to extract additional information from the episode description.&lt;/p&gt;

&lt;h2&gt;
  
  
  Other Posts in this Series
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.richardrodger.com/2024/02/20/building-a-podcast-chatbot-for-voxgig/"&gt;Building a Podcast Chatbot for Voxgig&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.richardrodger.com/2024/02/23/the-voxgig-podcast-chatbot-is-production-code/"&gt;The Voxgig Podcast Chatbot is Production Code&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.richardrodger.com/2024/02/28/the-voxgig-podcast-chatbot-first-subscribe/"&gt;The Voxgig Podcast Chatbot: First, Subscribe!&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;em&gt;This Post&lt;/em&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>devrel</category>
      <category>ai</category>
      <category>chatbot</category>
    </item>
    <item>
      <title>The Voxgig Podcast Chatbot: First, Subscribe!</title>
      <dc:creator>Richard Rodger</dc:creator>
      <pubDate>Wed, 28 Feb 2024 23:23:36 +0000</pubDate>
      <link>https://dev.to/rjrodger/the-voxgig-podcast-chatbot-first-subscribe-lj9</link>
      <guid>https://dev.to/rjrodger/the-voxgig-podcast-chatbot-first-subscribe-lj9</guid>
      <description>&lt;p&gt;This is the third post in a series I'm writing about a new &lt;a href="https://www.voxgig.com/podcast"&gt;Minimal&lt;br&gt;
Viable Product&lt;/a&gt; we've released at&lt;br&gt;
Voxgig that turns your podcast into a chatbot. Your visitors can now&lt;br&gt;
ask your guests questions directly!&lt;/p&gt;

&lt;p&gt;The first post is here: &lt;a href="https://www.richardrodger.com/2024/02/20/building-a-podcast-chatbot-for-voxgig/"&gt;Building a Podcast Chatbot for&lt;br&gt;
Voxgig&lt;/a&gt;&lt;br&gt;
and you find the full list at the end of this post.&lt;/p&gt;

&lt;p&gt;We want to ingest a podcast. The podcast episodes are described in an RSS feed that also contains the metadata about the podcast. We send a message to the microservices system describing what we want to happen:&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="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;aim&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ingest&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;// The "aim" of the message is the `ingest` service.&lt;/span&gt;
  &lt;span class="nx"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;podcast&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// The instruction to the `ingest` service.&lt;/span&gt;
  &lt;span class="nx"&gt;feed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://feeds.resonaterecordings.com/voxgig-fireside-podcast&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 system routes this message to our implementation code (we'll come back to how that happens later in this series). Since this is a &lt;a href="https://www.typescriptlang.org/"&gt;TypeScript&lt;/a&gt; code base, our implementation is a TypeScript function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;subscribe_podcast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;)&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;out&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;why&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;no-code-yet&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; 

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;out&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As a convention, our microservices accept messages that are JSON documents and also respond with messages that are JSON documents. There may not be a response (if the message is an "event"), but if there is, we use the property &lt;code&gt;ok: boolean&lt;/code&gt; to indicate the success or failure of the message, and use the &lt;code&gt;why: string&lt;/code&gt; property to provide an error reason to the sender.&lt;/p&gt;

&lt;p&gt;Why use this convention? You can't throw exceptions over the network (easily or naturally). But you also don't want to use exceptions for business logic failures. The system itself hasn't failed, just some business logic process.&lt;/p&gt;

&lt;p&gt;Our initial implementation does nothing except fail, but we can still test it by using the REPL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;boqr/pdm-local&amp;gt; aim:ingest,subscribe:podcast
&lt;span class="o"&gt;{&lt;/span&gt;
  ok: &lt;span class="nb"&gt;false&lt;/span&gt;,
  why: &lt;span class="s1"&gt;'no-code-yet'&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now we can start to write code to fill out the implementation, which will get hot-reloaded as we go, and we can keep using the REPL to test it. This is what they call &lt;a href="https://developerexperience.io/articles/good-developer-experience"&gt;&lt;em&gt;Developer Experience&lt;/em&gt;&lt;/a&gt; folks.&lt;/p&gt;

&lt;p&gt;Let's pause for a minute before writing any more code. We want our system to handle more than one podcast, and we know that we will need to process each podcast episode separately (download the audio, transcribe it, create a vector "embedding" etc.). So in this message action, all we should do is download the RSS, and then send another message to start the actual ingestion process. That way we separate obtaining the podcast RSS feed, from operating on that feed. This will make development and testing easier because once we have the feed, we can run ingestion multiple times without downloading the RSS each time. And trust me, when you build an AI chatbot, you need to rerun your pipeline &lt;em&gt;a lot&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Here is the basic functionality:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;RSS&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./ingest-types&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;subscribe_podcast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// The current seneca instance.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;seneca&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;

  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// RSS URL&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;feed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;feed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;feed&lt;/span&gt;

  &lt;span class="c1"&gt;// Processing controls&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;doUpdate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;doUpdate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;!!&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;doUpdate&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;doIngest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;doIngest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;!!&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;doIngest&lt;/span&gt;

  &lt;span class="c1"&gt;// Load the podcast by feed URL too see if we are already subscribed&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;podcastEnt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;seneca&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pdm/podcast&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;load&lt;/span&gt;&lt;span class="nf"&gt;$&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;feed&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="c1"&gt;// Download the RSS feed if new or updating&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nx"&gt;podcastEnt&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;doUpdate&lt;/span&gt;&lt;span class="p"&gt;)&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;rssRes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;seneca&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;shared&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getRSS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;feed&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;rss&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;rssRes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rss&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;RSS&lt;/span&gt;

    &lt;span class="nx"&gt;podcastEnt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;podcastEnt&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;seneca&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pdm/podcast&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;make&lt;/span&gt;&lt;span class="nf"&gt;$&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nx"&gt;podcastEnt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;podcastEnt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;save&lt;/span&gt;&lt;span class="nf"&gt;$&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="nx"&gt;feed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;rss&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;desc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;rss&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;description&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="nx"&gt;podcastEnt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;podcast&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;podcastEnt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="nf"&gt;$&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;doIngest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;seneca&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;aim:ingest,ingest:podcast&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;podcast_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;podcastEnt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;doUpdate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;doIngest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;why&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;podcast-not-found&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;out&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Typescript types: so there's a lot of &lt;code&gt;any&lt;/code&gt; types in this code. There will be a future refactoring to improve this situation. For now, remember that network messages are not function calls, and the messages are validated in other ways.&lt;/p&gt;

&lt;p&gt;Yoda conditions: (&lt;code&gt;null == foo&lt;/code&gt;) &lt;a href="https://en.wikipedia.org/wiki/Yoda_conditions"&gt;Safer, this is, young Padawan&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I've removed most of the debugging, tracing, and control code, but what you see above is the real implementation. Let's unpack what it does.&lt;/p&gt;

&lt;p&gt;This message action expects a message that gives us the feed URL in the &lt;code&gt;feed&lt;/code&gt; property. But it also looks for optional &lt;code&gt;doUpdate&lt;/code&gt; and &lt;code&gt;doIngest&lt;/code&gt; boolean properties. These are used to control how far we progress along the ingestion pipeline.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;doUpdate&lt;/code&gt; property must be &lt;code&gt;true&lt;/code&gt; to download the RSS feed and "update" the podcast.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;doIngest&lt;/code&gt; property must be &lt;code&gt;true&lt;/code&gt; to send the ingestion message to start ingesting individual episodes.&lt;/p&gt;

&lt;p&gt;You can use these properties in the REPL to switch off parts of the pipeline to concentrate on validating and developing the parts you want to work on.&lt;/p&gt;

&lt;p&gt;Note that we also add these properties to the &lt;code&gt;out&lt;/code&gt; variable and send them back with the response. This makes debugging easier, especially when looking at logs.&lt;/p&gt;

&lt;h2&gt;
  
  
  New or Existing Podcast?
&lt;/h2&gt;

&lt;p&gt;The first real work happens when we try to load the podcast from our database using the feed URL. If it already exists, we use the existing database row. The &lt;a href="https://senecajs.org/"&gt;Seneca&lt;/a&gt; framework has a fundamental design principle: &lt;strong&gt;everything is a message&lt;/strong&gt;. That includes database access (or is there a database at all? Who knows...).&lt;/p&gt;

&lt;p&gt;As a convenience, database messages are wrapped in a traditional &lt;a href="https://en.wikipedia.org/wiki/Object%E2%80%93relational_mapping"&gt;Object Relational Mapper&lt;/a&gt; interface. Seneca also supports &lt;a href="https://en.wikipedia.org/wiki/Namespace"&gt;name-spacing&lt;/a&gt; data entities. I'll explain more about Seneca's persistence system as we go, so you'll have to take things a little on faith at the start.&lt;/p&gt;

&lt;p&gt;Let's try to load a podcast from the database:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;podcastEnt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;seneca&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pdm/podcast&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;load&lt;/span&gt;&lt;span class="nf"&gt;$&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;feed&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is an asynchronous operation (we have to wait for the database), hence we have to &lt;code&gt;await&lt;/code&gt; a promise. The &lt;code&gt;entity&lt;/code&gt; method creates a new entity object for us, loading data from whatever table or data source is mapped to the &lt;code&gt;pdm/podcast&lt;/code&gt; entity. The Seneca entity ORM provides standard methods that all end in &lt;code&gt;$&lt;/code&gt; to avoid clashing with your own database column names. The &lt;code&gt;load$&lt;/code&gt; method takes a query object and returns the first entity that matches the field values in the query.&lt;/p&gt;

&lt;p&gt;In this system, the &lt;code&gt;pdm&lt;/code&gt; namespace is used for tables relating to our podcast chatbot business logic. We will also make use of Seneca plugins that provide standard functionality (such as user accounts) that use the &lt;code&gt;sys&lt;/code&gt; namespace. By using our own namespace, we ensure our own tables never conflict with any of the Seneca plugins we might want to use later. I did say this was production code. This is the sort of thing you have to worry about in production applications.&lt;/p&gt;

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

&lt;p&gt;If the podcast does not exist in our system, or if &lt;code&gt;doUpdate&lt;/code&gt; is &lt;code&gt;true&lt;/code&gt;, then we proceed to download the RSS feed, set some fields on the &lt;code&gt;pdm/podcast&lt;/code&gt; entity, and save it to the database.&lt;/p&gt;

&lt;p&gt;Cool. So now, if we do have a podcast (&lt;code&gt;null != podcastEnt&lt;/code&gt;), then we can proceed to ingest it if &lt;code&gt;doIngest&lt;/code&gt; is &lt;code&gt;true&lt;/code&gt;. We send a new message but do not expect a reply. We are emitting the message as an event. Locally, the message gets routed inside our single process monolith. When deployed to AWS, our message will get sent out over a &lt;a href="https://aws.amazon.com/sqs/"&gt;Simple Queue Service&lt;/a&gt; topic to whoever is interested in it for further processing. Either way, we do have to wait for the message to be posted.&lt;/p&gt;

&lt;p&gt;Notice that the mental model of "it's all just JSON messages all the way down" means we don't have to (in this code) think about message routing architectures, microservice design patterns, or what service SDK we need to use. We can concentrate on our own business logic.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv5kbscvftupkul99c4mx.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv5kbscvftupkul99c4mx.jpg" alt="Just send some JSON Shinji" width="701" height="499"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Other Posts in this Series
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="https://dev.to/rjrodger/building-a-podcast-chatbot-for-voxgig-28f8"&gt;Building a Podcast Chatbot for Voxgig&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/rjrodger/the-voxgig-podcast-chatbot-is-production-code-52lj"&gt;The Voxgig Podcast Chatbot is Production Code&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;This Post&lt;/em&gt;&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>ai</category>
      <category>chatbot</category>
      <category>microservices</category>
    </item>
    <item>
      <title>The Voxgig Podcast Chatbot is Production Code</title>
      <dc:creator>Richard Rodger</dc:creator>
      <pubDate>Fri, 23 Feb 2024 11:45:27 +0000</pubDate>
      <link>https://dev.to/rjrodger/the-voxgig-podcast-chatbot-is-production-code-52lj</link>
      <guid>https://dev.to/rjrodger/the-voxgig-podcast-chatbot-is-production-code-52lj</guid>
      <description>&lt;p&gt;This post is part of a series.&lt;/p&gt;

&lt;p&gt;This is the second post in a series I'm writing about a new &lt;a href="https://www.voxgig.com/podcast" rel="noopener noreferrer"&gt;Minimal Viable Product&lt;/a&gt; we've released at Voxgig that turns your podcast into a chatbot. Your visitors can now ask your guests questions directly!&lt;/p&gt;

&lt;p&gt;The first post is here: &lt;a href="https://www.richardrodger.com/2024/02/20/building-a-podcast-chatbot-for-voxgig/" rel="noopener noreferrer"&gt;Building a Podcast Chatbot for Voxgig&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In this series, I'll dive into the code that implements the chatbot. It's all open source so you can cut and paste to have your own one.&lt;/p&gt;

&lt;p&gt;Since it is production-grade code, not just an example for the sake of some "content", we'll navigate through the code in baby steps by focusing on specific tasks, rather than trying to go top-to-bottom. Production code has to do a lot of things, so rather than trying to start at the start, we'll start in a reasonable place so you can see useful code right away.&lt;/p&gt;

&lt;p&gt;We have a podcast RSS feed URL, and we want to trigger ingestion of the episodes into the chatbot. Where do we begin?&lt;/p&gt;

&lt;p&gt;Well, we have to download and parse the RSS feed. There's a great RSS parser package that we can use: &lt;a href="https://github.com/rbren/rss-parser" rel="noopener noreferrer"&gt;rss-parser&lt;/a&gt; - thank you to &lt;a href="https://rbren.io/" rel="noopener noreferrer"&gt;Robert Brennan&lt;/a&gt;! To get the RSS feed we need one line of code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;Parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parseURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;feed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That returns the contents of the feed (which is XML), as a JSON document so it's nice and easy to work with. We'll loop through all the episodes, download the audio, get it transcribed with a speech-to-text service (&lt;a href="//deepgram.com"&gt;Deepgram&lt;/a&gt; in the first version), and then sprinkle some &lt;a href="https://www.promptingguide.ai/techniques/rag" rel="noopener noreferrer"&gt;Retrieval Augmented Generation&lt;/a&gt; magic pixie dust over everything to make the chatbot work.&lt;/p&gt;

&lt;p&gt;But first, let's do some software architecture. This is not meant to be a toy. The system deploys to AWS, and uses Lambda Functions, DynamoDB, S3, SQS, and other fun AWS stuff.&lt;/p&gt;

&lt;p&gt;Also, the system is a microservice system when deployed, but a local monolith when developing locally. How that all works is something we'll come back to in later posts.&lt;/p&gt;

&lt;p&gt;Since we are downloading RSS feeds, we are effectively an RSS reader, and thus we can have the concept of "subscribing" to a feed in our system. That means we need to have a microservice that can accept an instruction to subscribe to a podcast.&lt;/p&gt;

&lt;p&gt;The microservice is called &lt;a href="https://github.com/voxgig/podmind/tree/main/backend/src/srv/ingest" rel="noopener noreferrer"&gt;&lt;code&gt;ingest&lt;/code&gt;&lt;/a&gt; and the message that we send to the microservice to subscribe to a podcast (and optionally trigger ingestion) looks like this:&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="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;aim&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ingest&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;// The "aim" of the message is the `ingest` service.&lt;/span&gt;
  &lt;span class="nx"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;podcast&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// The instruction to the `ingest` service.&lt;/span&gt;
  &lt;span class="nx"&gt;feed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://feeds.resonaterecordings.com/voxgig-fireside-podcast&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;There are more properties, but we'll come back to those later. Also, I'm a big fan of the &lt;a href="https://thethreevirtues.com/" rel="noopener noreferrer"&gt;Three Virtues&lt;/a&gt;: Laziness, Impatience, and Hubris. The string &lt;code&gt;"aim"&lt;/code&gt; is three characters and one syllable, whereas &lt;code&gt;"service"&lt;/code&gt; is seven characters and two syllables. Also &lt;code&gt;"service"&lt;/code&gt; is a term that is going to be horrendously overloaded and over-used in any codebase. Using &lt;a href="https://www.orwellfoundation.com/the-orwell-foundation/orwell/essays-and-other-works/politics-and-the-english-language/" rel="noopener noreferrer"&gt;short non-technical Anglo-Saxon words&lt;/a&gt; to stand for project-specific concepts is a great way to reduce overall confusion in a large code base that you will have to maintain for a long time. My favorite programming tool has always been a thesaurus.&lt;/p&gt;

&lt;p&gt;I'm completely ignoring, for now, all questions about how this message, which is a JSON document, gets routed to the &lt;code&gt;ingest&lt;/code&gt; microservice. But let's look at the two main ways that we can send this message.&lt;/p&gt;

&lt;p&gt;In code, you can send this message (perhaps as a result of a new user filling out a form with their RSS feed URL) by using the &lt;a href="https://senecajs.org/" rel="noopener noreferrer"&gt;Seneca Framework&lt;/a&gt; microservices library:&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;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;seneca&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;aim&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ingest&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;podcast&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;feed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://feeds.resonaterecordings.com/voxgig-fireside-podcast&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;Oh wait, we forgot to be lazy. Let's try that again:&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;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;seneca&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;aim:ingest,subscribe:podcast&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;feed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://feeds.resonaterecordings.com/voxgig-fireside-podcast&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;Notice that there is no indication of how this message is transported to the &lt;code&gt;ingest&lt;/code&gt; service. No HTTP calling code, no message topic, nothing. That's important. Because the shortest path to the dreaded &lt;a href="https://joaovasques.medium.com/your-distributed-monoliths-are-secretly-plotting-against-you-4c1b20324a31" rel="noopener noreferrer"&gt;distributed monolith&lt;/a&gt; is not to use a message abstraction layer.&lt;/p&gt;

&lt;p&gt;The other way to send this message, which you'll see quite a bit in this series, is to use a REPL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ npm run repl-dev

&amp;gt; @voxgig/podmind-backend@0.3.3 repl-dev
&amp;gt; seneca-repl aws://lambda/pdm01-backend01-dev-monitor?region=us-east-1

Connected to Seneca: {
  version: '3.34.1',
  id: '6ejf/monitor-pdm01-dev@0.3.3',
  when: 1708685110275
}
6ejf/monitor-pdm01-dev@0.3.3&amp;gt; aim:ingest,subscribe:podcast,doIngest:false,feed:'https://feeds.resonaterecordings.com/voxgig-fireside-podcast'
{
  ok: true,
  why: '',
  batch: 'B2024022310460874',
  mark: 'Ml032jm',
  feed: 'https://feeds.resonaterecordings.com/voxgig-fireside-podcast',
  doUpdate: false,
  doIngest: false,
  doAudio: false,
  doTranscribe: false,
  episodeStart: 0,
  episodeEnd: -1,
  chunkEnd: -1,
  podcast: Entity {
    'entity$': '-/pdm/podcast',
    feed: 'https://feeds.resonaterecordings.com/voxgig-fireside-podcast',
    t_mh: 2024020722371877,
    t_m: 1707345438776,
    t_ch: 2024012801013692,
    t_c: 1706403696926,
    id: 'pdfxc5',
    batch: 'B2024020722371839',
    title: 'Fireside with Voxgig',
    desc: '\n' +
      '            This DevRel focused podcast allows entrepreneur, author and coder Richard Rodger introduce you to interesting leaders and experienced professionals in the tech community. Richard and his guests chat not just about their current work or latest trend, but also about their experiences, good and bad, throughout their career. DevRel requires so many different skills and you can come to it from so many routes, that this podcast has featured conference creators, entrepreneurs, open source maintainers, developer advocates and community managers. Join us to learn about just how varied DevRel can be and get ideas to expand your work, impact and community.\n' +
      '        '
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I literally just REPL'd into my live system and sent that message so I could cut and paste that example for you.&lt;/p&gt;

&lt;p&gt;That &lt;code&gt;doIngest&lt;/code&gt; parameter lets me turn off ingestion. No point ingesting a podcast I'm already up-to-date with, just for the sake of an example. The message action, implemented in the &lt;code&gt;ingest&lt;/code&gt; service, is synchronous, so I get a response back with the details of the podcast stored in my database.&lt;/p&gt;

&lt;p&gt;I can trigger almost any behavior in my system, at any point in the ingestion pipeline, using the REPL. I can do this live on AWS, or I can do it locally (with hot-reloading), so the &lt;a href="https://www.richardrodger.com/2023/09/04/why-you-should-be-using-a-repl-all-the-way-to-production/" rel="noopener noreferrer"&gt;debugging experience is just lovely&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;In the next post, we'll look at the implementation of the &lt;code&gt;aim:ingest,subscribe:podcast&lt;/code&gt; message action in detail. That will set us up to understand the other messages that focus more on the RAG implementation.&lt;/p&gt;

&lt;p&gt;But you don't have to wait for me: &lt;a href="https://github.com/voxgig/podmind/tree/main/backend/src/srv/ingest" rel="noopener noreferrer"&gt;&lt;code&gt;ingest&lt;/code&gt; source code on github&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://blog.codinghorror.com/learn-to-read-the-source-luke/" rel="noopener noreferrer"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.richardrodger.com%2Fwp-content%2Fuploads%2F2024%2F02%2Fread-the-source-luke.jpg" alt="Read The Source Luke"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Other Posts in this Series
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="https://dev.to/rjrodger/building-a-podcast-chatbot-for-voxgig-28f8"&gt;Building a Podcast Chatbot for Voxgig&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;This post!&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/rjrodger/the-voxgig-podcast-chatbot-first-subscribe-lj9"&gt;The Voxgig Podcast Chatbot: First, Subscribe!&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>ai</category>
      <category>chatbot</category>
      <category>microservices</category>
    </item>
    <item>
      <title>Building a Podcast Chatbot for Voxgig</title>
      <dc:creator>Richard Rodger</dc:creator>
      <pubDate>Tue, 20 Feb 2024 15:25:20 +0000</pubDate>
      <link>https://dev.to/rjrodger/building-a-podcast-chatbot-for-voxgig-28f8</link>
      <guid>https://dev.to/rjrodger/building-a-podcast-chatbot-for-voxgig-28f8</guid>
      <description>&lt;p&gt;This post is part of a series.&lt;/p&gt;

&lt;p&gt;At Voxgig we have now recorded 140 &lt;a href="https://voxgog.com/podcast"&gt;podcast&lt;/a&gt; episodes (as of Feb 2024). That's a lot of chatting about developer relations.  It's great if you are a regular listener (thanks!), or have the time to listen to our back catalogue. But what is frustrating is that there is so much wonderful knowledge about developer relations locked up in an audio format that, while enjoyable to listen to, is not accessible or usable in an efficient manner.&lt;/p&gt;

&lt;p&gt;When you need to know something about developer relations, and you want to tap into the wisdom of one our guests, you are out of luck. Or at least, you were. We decided to turn our coding skills to this little problem and write our own AI chatbot to let you ask our guests your questions directly.&lt;/p&gt;

&lt;p&gt;If you go to the &lt;a href="https://voxgog.com/podcast"&gt;Voxgig podcast page&lt;/a&gt;, you can now see a little chatbot widget, and when you type in a question, you get a reasonably good answer!&lt;/p&gt;

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

&lt;p&gt;Now this is a very crude initial version, and we are actively improving it. The chatbot uses &lt;a href="https://www.promptingguide.ai/techniques/rag"&gt;Retrieval Augmented Generation&lt;/a&gt; (RAG) to ingest all our podcast episodes, and let you ask questions of our guests directly.&lt;/p&gt;

&lt;p&gt;We decided to open-source the entire project. Building a RAG ingestor and query engine is a natural fit for microservices (there are lots of little asynchronous tasks). So if you'd like your own podcast chatbot, just cut and paste our code: &lt;a href="https://github.com/voxgig/podmind"&gt;github.com/voxgig/podmind&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;We've had such an enthusiastic reaction to this fun little project we've decided to take it further and extend the chatbot to handle all sorts of developer content. We're also adding the ability to easily experiment with the prompt and the "chunker" (stay tuned if you want to know all about that stuff).&lt;/p&gt;

&lt;p&gt;We're going to "build in public" with this project. You'll be able to follow along at a source-code level as we make it more and more useful.&lt;/p&gt;

&lt;p&gt;To get started, here are the microservices and what each one does:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;audio&lt;/code&gt; - Transcribes audio to dialogue text (using &lt;a href="https://deepgram.com/"&gt;Deepgram&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;auth&lt;/code&gt; - User identity - for the user analytics dashboard.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;chat&lt;/code&gt; - Perform chat queries.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;chunk&lt;/code&gt; - The "chunker" that splits the transcript into chunks for the vector db.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;embed&lt;/code&gt; - Embed the chunks as vectors so you can do similarity searches.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;entity&lt;/code&gt; - General purpose persistent entity operations.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ingest&lt;/code&gt; - Ingestor to orchestrate the ingestion process.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;monitor&lt;/code&gt; - Monitoring and debugging of the system, provides a &lt;a href="https://www.richardrodger.com/2023/09/04/why-you-should-be-using-a-repl-all-the-way-to-production/"&gt;REPL&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;store&lt;/code&gt; - Store audio and RSS files.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;user&lt;/code&gt; - User management.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;widget&lt;/code&gt; - The API for the embeddable chat widget (which is a proper web component).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We run this system as a "local monolith", but deploy it as Lambdas on AWS.&lt;/p&gt;

&lt;p&gt;A lot of our development is done in a REPL with hot reloading, so we can alter business logic and experiment on the fly, even with our deployed Lambdas. We'll get into this in later posts, but here is an example of submitting a question to the live &lt;code&gt;chat&lt;/code&gt; service Lambda:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ seneca-repl aws://lambda/pdm01-backend01-dev-monitor?region=us-east-1

&amp;gt; @voxgig/podmind-backend@0.3.3 repl-dev
&amp;gt; seneca-repl aws://lambda/pdm01-backend01-dev-monitor?region=us-east-1

Connected to Seneca: {
  version: '3.34.1',
  id: 'axjz/monitor-pdm01-dev@0.3.3',
  when: 1708436725968
}
axjz/monitor-pdm01-dev@0.3.3&amp;gt; aim:chat,chat:query,query:'what is devrel?'
{
  ok: true,
  why: '',
  answer: "DevRel, short for Developer Relations, is a field within the tech industry that primarily focuses on building and maintaining relationships between companies and developers. DevRel professionals often act as intermediaries between developers and the companies they serve, bridging the gap between technical know-how and the business needs of a product. They help developers understand how a company's product works and are always on the lookout for opportunities to improve the overall developer experience.",
  context: {
    hits: [ ... ]
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We're a TypeScript/JavaScript shop, so that's what we're using right now. I would not be surprised if a Python microservice or two appears in the future, but for now, we'll stick to what we know best.  This is the start of a series - I'll add links to more posts as we go.&lt;/p&gt;

&lt;h2&gt;
  
  
  Other posts in this series
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;em&gt;This post!&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/rjrodger/the-voxgig-podcast-chatbot-is-production-code-52lj"&gt;The Voxgig Podcast Chatbot is Production Code&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/rjrodger/the-voxgig-podcast-chatbot-first-subscribe-lj9"&gt;The Voxgig Podcast Chatbot: First, Subscribe!&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>ai</category>
      <category>chatbot</category>
      <category>rag</category>
      <category>microservices</category>
    </item>
    <item>
      <title>Why you should be using a REPL all the way to production</title>
      <dc:creator>Richard Rodger</dc:creator>
      <pubDate>Mon, 04 Sep 2023 09:34:22 +0000</pubDate>
      <link>https://dev.to/rjrodger/why-you-should-be-using-a-repl-all-the-way-to-production-32id</link>
      <guid>https://dev.to/rjrodger/why-you-should-be-using-a-repl-all-the-way-to-production-32id</guid>
      <description>&lt;p&gt;Let’s talk about one of the most productive tools a coder can use: the &lt;a href="https://en.wikipedia.org/wiki/Read%E2%80%93eval%E2%80%93print_loop"&gt;REPL&lt;/a&gt;! The first half of this article gives you a short introduction to the subject if you’re not a coder. If you are a coder, skip ahead to the second half where I talk about using REPLs in production (“with great power comes great responsibility”).&lt;/p&gt;

&lt;p&gt;I recently released a long overdue update to the &lt;a href="https://senecajs.org/"&gt;Seneca&lt;/a&gt; REPL plugin (A &lt;a href="https://nodejs.org/"&gt;Node.js&lt;/a&gt; &lt;a href="https://martinfowler.com/microservices/"&gt;microservices&lt;/a&gt; framework). Since I’m also a fan of &lt;a href="https://fastify.dev/"&gt;fastify&lt;/a&gt; (a web framework for Node.js) I couldn’t resist submitting a plugin proposal for a fastify REPL too, since we’ve been having far too much fun with our Seneca REPL, and fastify users are missing out!&lt;/p&gt;

&lt;p&gt;Why this renewed focus on a feature I first built years ago? What’s so great about REPLs anyway?&lt;/p&gt;

&lt;p&gt;A REPL (Read-Execute-Print-Loop) is a coding tool that lets you interact directly with a running system. The browser console is easily the most used REPL, and essential to the development of any complex web application. You can type in code directly, and see results right away. Rather than waiting for your code to compile, deploy or load, you can make changes immediately. You get direct feedback with no delays that lets you stay focused on the work.&lt;/p&gt;

&lt;p&gt;Programming systems provide a REPL as an interactive text interface that lets you write code, usually in a more lenient or shorthand form. You don’t have to formally declare variables. You get access to predefined useful state objects (in the browser, you can access the &lt;code&gt;document&lt;/code&gt; object, among many others). The essential thing is that you are given access to the live running system, whether that is a web browser, or a local development server, or something else.&lt;/p&gt;

&lt;p&gt;Coders use a REPL to debug problems directly, to inspect data to figure out what is going on, to experiment with new code to see what works, to get a feel for a new API (Application Programming Interface), to manipulate data, and even to administer deployed systems.&lt;/p&gt;

&lt;p&gt;If a new system doesn’t yet have a REPL, coders will always build one for it. Why does this happen? In more ordinary software development, you write some code, save it, and then run the code, often as part of a test suite. If you’re lucky, your test suite is pretty fast, on the order of seconds. More usually, it can take minutes to run for a production system.&lt;/p&gt;

&lt;p&gt;There’s a huge amount of friction in writing or changing some code, then switching to another place to run the code, and then waiting for results. Programming is a lot like the spinning plates performance act. The coder has to spin up multiple mental states and keep them all in their head, so that they can tie the pieces together to solve the problem. Get interrupted while you’re waiting for your tests to complete? Good luck fixing that bug–now you have to rebuild your mental state again, which can easily take a few minutes of focused thought.&lt;/p&gt;

&lt;p&gt;As an aside, this is why “collaborative” open plan offices make your developers super unhappy and unproductive–they need to &lt;a href="https://www.linkedin.com/posts/ian-mcclean-46b7681_minimise-distraction-activity-7056953069778939904--b99"&gt;concentrate, not chit chat!&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The coding tool that makes unnecessary wait time go away is the REPL. You don’t have to context-switch half as much. The code is run and you see the results right away, so you can stay focused and get the solution. Often you’d be able to copy and paste the rough code out of the REPL into your editor, and fix it up to be production grade.&lt;/p&gt;

&lt;p&gt;Not all code is the same. REPLs work best with “business logic” code, and user interface code. When you’re writing code that’s more about computer science algorithms, or core library code, you spend much more time just thinking rather than smashing the keyboard. But when you have a documented business requirement, the work is more about gluing the parts together than inventing new algorithms. What counts is being able to ensure your data has the right structure, and you are calling the right APIs in the right way. A REPL makes this work much easier.&lt;/p&gt;

&lt;p&gt;Where (and when) does the idea of a REPL come from? Probably in 1958, from &lt;a href="https://en.wikipedia.org/wiki/John_McCarthy_(computer_scientist)"&gt;John McCarthy&lt;/a&gt;, the inventor of the Lisp language. Interactive evaluation of expressions was an essential feature of &lt;a href="https://twobithistory.org/2018/10/14/lisp.html"&gt;Lisp&lt;/a&gt; right from the start. If you had had to code using &lt;a href="https://www.computermuseumofamerica.org/2022/09/16/what-are-punch-cards-in-early-computers/"&gt;punch cards&lt;/a&gt; up to that point, you’d probably be pretty keen on having a REPL too. &lt;/p&gt;

&lt;p&gt;The &lt;a href="https://squeak.org/"&gt;Smalltalk&lt;/a&gt; programming environment took the idea of a REPL to the next level, by making the entire system “live”. You coded by manipulating the state of the live system using an editor that was basically a REPL. Saving your project was the same as serializing the current state.&lt;/p&gt;

&lt;p&gt;Although it didn’t really work out, the &lt;a href="https://renenyffenegger.ch/notes/development/databases/SQL/history/SEQUEL"&gt;database query language SQL&lt;/a&gt; was supposed to be a way to “talk” to databases using human-like language. But us programmers still get the benefit of an interactive language to manipulate data.&lt;/p&gt;

&lt;p&gt;The REPL environment that most non-coders would be familiar with is a spreadsheet. You edit formulas and you see immediate results. This is very gratifying! Programmers enjoy the same serotonin kick. And it gets the work done faster. &lt;a href="https://www.youtube.com/watch?v=kOO31qFmi9A"&gt;Enjoy the cheese!&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;With machine learning finally starting to deliver, many folks are getting exposed to another form of the REPL: the interactive notebook. First seen in the realm of statistical analytics and mathematics, with systems like &lt;a href="https://www.wolfram.com/mathematica/"&gt;Mathematica&lt;/a&gt;, notebooks such as &lt;a href="https://jupyter.org/"&gt;Jupyter&lt;/a&gt; now provide a comfortable interface to complex underlying systems.&lt;/p&gt;

&lt;p&gt;And we are not far off the holy grail of REPLs–the fully competent natural language interface provided by the new generation of artificial intelligence LLMs (&lt;a href="https://mark-riedl.medium.com/a-very-gentle-introduction-to-large-language-models-without-the-hype-5f67941fa59e"&gt;Large Language Models&lt;/a&gt;). Just playing around with a service like &lt;a href="https://chat.openai.com/"&gt;ChatGPT&lt;/a&gt; will give you an instant feel for why coders are so keen on having a REPL available.&lt;/p&gt;

&lt;p&gt;There’s even a &lt;a href="https://www.badunicorn.vc/"&gt;marked-for-unicorn&lt;/a&gt; status tech startup called &lt;a href="https://replit.com/"&gt;repl.it&lt;/a&gt; that provides software developers with, you guessed it, an online REPL-like environment for coding.&lt;/p&gt;

&lt;p&gt;OK, let’s get a bit more technical. If you’re a coder and have used REPLs occasionally but have not found them to be that useful, let’s dive into how you can use them to get some &lt;a href="https://www.youtube.com/watch?v=zgt-jNqbxF8"&gt;super powers&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;First, find a REPL! Here’s a list. The fact that almost every serious programming environment has one should tell you REPLs are a serious tool you need to master.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Python:

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.python.org/3/tutorial/interpreter.html"&gt;Default Python shell&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ipython.org/documentation.html"&gt;IPython&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://bpython-interpreter.org/"&gt;bpython&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;Ruby:

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.ruby-lang.org/en/2.7.0/IRB.html"&gt;irb (Interactive Ruby)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;JavaScript:

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://nodejs.org/api/repl.html"&gt;Node.js&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Tools/Web_Console"&gt;Browsers' Console&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;Lisp/Scheme/Clojure:

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.gnu.org/software/mit-scheme/documentation/mit-scheme-ref.html"&gt;GNU Clisp, Scheme, and other implementations&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://clojure.org/guides/repl/introduction"&gt;Clojure&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;Scala:

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.scala-lang.org/overviews/repl/overview.html"&gt;Scala REPL&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;Haskell:

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://downloads.haskell.org/~ghc/latest/docs/html/users_guide/ghci.html"&gt;ghci&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;Java:

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.oracle.com/javase/9/jshell/toc.htm"&gt;jshell&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;Groovy:

&lt;ul&gt;
&lt;li&gt;&lt;a href="http://groovy-lang.org/groovysh.html"&gt;groovysh&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;Erlang:

&lt;ul&gt;
&lt;li&gt;&lt;a href="http://erlang.org/doc/man/erl.html"&gt;erl&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;Elixir:

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://hexdocs.pm/iex/IEx.html"&gt;iex&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;R:

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://cran.r-project.org/doc/manuals/r-release/R-intro.html"&gt;R documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;Perl:

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://perldoc.perl.org/perldebug.html"&gt;perl debug mode&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://metacpan.org/pod/Reply"&gt;re.pl&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;Lua:

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.lua.org/manual/5.1/"&gt;Lua documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;Swift:

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.swift.org/swift-book/LanguageGuide/InteractivePlayground.html"&gt;Swift REPL&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;Go:

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/motemen/gore"&gt;gore (third-party)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;Julia:

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.julialang.org/en/v1/stdlib/REPL/"&gt;Julia REPL&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;OCaml:

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/ocaml-community/utop"&gt;utop&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://caml.inria.fr/pub/docs/manual-ocaml/toplevel.html"&gt;Default OCaml REPL&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;F#:

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.microsoft.com/en-us/dotnet/fsharp/tutorials/fsharp-interactive/"&gt;fsi&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;C#:

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.microsoft.com/en-us/visualstudio/csharp/csharp-interactive-repl-in-visual-studio"&gt;C# Interactive Window in Visual Studio&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/dotnet/roslyn/wiki/Interactive-Window"&gt;csi command-line tool&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;Kotlin:

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://kotlinlang.org/docs/whatsnew13.html#kotlin-repl"&gt;Kotlin REPL&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;Crystal:

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/crystal-lang-tools/icr"&gt;icr&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;Rust:

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/google/evcxr"&gt;evcxr (third-party)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;PHP:

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.php.net/manual/en/features.commandline.interactive.php"&gt;php -a&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;MATLAB:

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.mathworks.com/help/matlab/matlab_env/enter-statements-in-command-window.html"&gt;MATLAB documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;Shell scripting languages:

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.gnu.org/software/bash/manual/bash.html"&gt;Bash, Zsh, and other Unix shells&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;Racket:

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.racket-lang.org/drracket/interactive.html"&gt;Racket documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;Prolog:

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.swi-prolog.org/pldoc/doc_for?object=section(%27A.14%27)"&gt;SWI-Prolog and other implementations&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;


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

&lt;p&gt;Let’s talk about what you can do with REPLs and why you would want one. I’ll start with the ordinary stuff and work my way up to the &lt;a href="https://thedailywtf.com/articles/The-System"&gt;definitely controversial radioactive-spider use cases&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;You write some code. You compile it (for most languages). You run your unit tests. Or you run a small script to check things. Or you start up your system and then manually interact with it to check if your code works.&lt;/p&gt;

&lt;p&gt;This cycle takes time. You spend a lot of time just waiting for compilation to complete, or the unit testing framework to boot up and then run your tests. Each individual delay might take a few seconds, but it all adds up. Pretty soon you’re on Twitter or Youtube or Discord and you lose twenty minutes before you drag yourself back to work. There are decades worth of articles all over the web about the productivity horrors of “context switching” and how it kills your concentration. You work best as a developer when you can get into that &lt;a href="https://www.ted.com/talks/mihaly_csikszentmihalyi_flow_the_secret_to_happiness"&gt;magical “flow state” where the code just pours out of your brain&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;You know what makes flow state easy to maintain? A REPL! You write, you see results. &lt;a href="https://www.youtube.com/watch?v=ub747pprmJ8"&gt;Right here, right now&lt;/a&gt;. If that REPL is exposed by a local version of the system you’re working on, it gets even better. You stay in flow working on your system. Out pours the code!&lt;/p&gt;

&lt;p&gt;Now let’s take it to the next level. If you rig up some sort of hot-reloading into your system (there’s always a way to do this in your language of choice), you can debug in flow state without those restart wait times that break your concentration. Instant, instant feedback. &lt;a href="https://www.youtube.com/watch?v=uASnEyRG_uQ"&gt;That’s the drug we need&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;You run your code in the REPL. It’s broken. Alt-Tab back to your editor, make a change. Alt-Tab back to the REPL. Hit the up-arrow key, hit return, see results. Still broken? Repeat. Fixed? Alt-Tab, run the unit tests and &lt;a href="http://www.extremeprogramming.org/rules/unittests.html"&gt;bask in the glow of green&lt;/a&gt;. Go grab a coffee. &lt;a href="https://gwern.net/doc/cs/algorithm/2001-demarco-peopleware-whymeasureperformance.pdf"&gt;You’re a 10X dev&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The feeling of intellectual reward you get from not wasting any time waiting for the machine is not something you’ll ever want to give up on again once you’ve tasted it.&lt;/p&gt;

&lt;p&gt;And up another level. Take your REPL and start adding custom commands and shortcuts. Your command history will have lots of code that you keep using again and again. Turn that repeated code into shortcuts. Apart from the righteous satisfaction every dev gets from &lt;a href="https://www.cs.cmu.edu/~awb/linux.history.html"&gt;building their own tools&lt;/a&gt;, you just made yourself and your team a lot faster and a lot happier.&lt;/p&gt;

&lt;p&gt;At this point it’s time to invoke the &lt;a href="https://thethreevirtues.com/"&gt;eternal three virtues of programming&lt;/a&gt;:&lt;/p&gt;

&lt;p&gt;According to Larry Wall, the original author of the Perl programming language, there are &lt;strong&gt;three great virtues of a programmer&lt;/strong&gt;; Laziness, Impatience and Hubris&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Laziness&lt;/strong&gt;: The quality that makes you go to great effort to reduce overall energy expenditure. It makes you write labor-saving programs that other people will find useful and document what you wrote so you don't have to answer so many questions about it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Impatience&lt;/strong&gt;: The anger you feel when the computer is being lazy. This makes you write programs that don't just react to your needs, but actually anticipate them. Or at least pretend to.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hubris&lt;/strong&gt;: The quality that makes you write (and maintain) programs that other people won't want to say bad things about.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;[&lt;a href="https://www.oreilly.com/library/view/programming-perl-4th/9781449321451/"&gt;“Programming Perl"&lt;/a&gt;, 2nd Edition, O'Reilly &amp;amp; Associates, 1996]&lt;/p&gt;

&lt;p&gt;Nothing is more faithful to the Three Virtues than a REPL (or perhaps &lt;a href="https://www.perl.org/"&gt;Perl&lt;/a&gt;, or &lt;a href="https://www.raku.org/"&gt;Raku&lt;/a&gt; nowadays, I guess).&lt;/p&gt;

&lt;p&gt;Your shortcuts can go beyond mere abbreviations. All large systems define conceptual abstractions for the system itself. How do you represent data? Operations on that data? Components? User interactions? API requests? All these things and more can be “&lt;a href="https://ericnormand.me/podcast/what-is-to-reify-in-software"&gt;reified&lt;/a&gt;” in the REPL. Given form and substance, so that you can use them directly.&lt;/p&gt;

&lt;p&gt;Here’s an example. Let’s say you use an &lt;a href="https://opensource.com/article/17/11/django-orm"&gt;Object-Relation-Mapper&lt;/a&gt; to talk to your database. Maybe your code looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
let alice = new User({ name: “Alice”, role: “admin” })

alice.save()

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

&lt;/div&gt;



&lt;p&gt;You could type that into the REPL directly. Or you could lift up (“reify”) the “idea” of data entities into a system of shortcuts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
&amp;gt; save user {name:alice,role:admin}

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

&lt;/div&gt;



&lt;p&gt;Here’s another example. Let’s say you’re working on a REST API. Sure you’ve got &lt;a href="https://curl.se/"&gt;curl&lt;/a&gt; and &lt;a href="https://www.postman.com/"&gt;postman&lt;/a&gt; to help with testing and debugging, but again, they do require a context-switch. Not much, but it adds up. Instead, use the REPL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
&amp;gt; GET /api/users

[ …list of user objects… ]

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

&lt;/div&gt;



&lt;p&gt;Once you start to build up a set of shortcuts, they become a &lt;a href="https://tek-ritr.com/its-more-than-chaucer/"&gt;vernacular&lt;/a&gt; that makes you and your team very happy developers. And fast.&lt;/p&gt;

&lt;p&gt;Where can you go next? I’m sure you’ll be very familiar with &lt;a href="https://blog.codinghorror.com/the-works-on-my-machine-certification-program/"&gt;code working perfectly on your local machine&lt;/a&gt;, and breaking once deployed to a build server, or to staging or production. This is normal. No amount of testing will save you. Indeed the purpose of build or staging servers is to find exactly these fracture points where different environments break your code.&lt;/p&gt;

&lt;p&gt;That’s nice. What isn’t nice is the pain of debugging code that is broken on staging. Welcome to log file hell. We won’t wade into the Big Fight about &lt;a href="https://buttondown.email/geoffreylitt/archive/starting-this-newsletter-print-debugging-byoc/"&gt;print-versus-debugger&lt;/a&gt; debugging, but when it comes to staging servers, all you often get is a log file.&lt;/p&gt;

&lt;p&gt;So you add some print statements and you redeploy. Then you wait for the remote system to churn. Then you load up your log files (probably in a clunky web interface), and try to make sense of the output, most of which is utterly irrelevant. You thought flow state was hard to maintain for local dev work? There ain’t no flow state here. This work is grueling and slow and very not fun.&lt;/p&gt;

&lt;p&gt;Wouldn’t you like a REPL into staging? With a REPL you just look at things directly and muck about debugging right on the system, with the staging data. This is the way to debug! And if your REPL also includes some commands for data querying and modification, then you don’t even need to Alt-Tab over the database interface. Bugs and issues on staging can be resolved in nearly the same way, and nearly as fast as local issues. Happy happy developer!&lt;/p&gt;

&lt;p&gt;When you implement a REPL to give you access to staging, you may not always just be able to expose an open network port, which is the normal mode of connection to a REPL. For things like serverless functions, you’ll need to co-opt the cloud infrastructure to let you submit commands and get responses. Use things like AWS Lambda invocation to get your commands up into the cloud. What matters is the developer experience of an interactive terminal. What happens behind the curtain is an implementation detail. &lt;/p&gt;

&lt;p&gt;At this point you will need to start thinking more seriously about access control. Staging servers, build servers, and test servers are shared infrastructure. Who gets REPL access and what can they do? That has to be context dependent, but here’s some things that have worked well for me.&lt;/p&gt;

&lt;p&gt;If your system has a concept of user accounts with permissions, reuse that abstraction. Each REPL is operated as a user in the system. You’ll need to add some way to login or provide an access token. If you have an external API that provides this already, you’re nearly home and dry.&lt;/p&gt;

&lt;p&gt;If you don’t have a concept of a user, then you’ll need to at least provide a set of access levels, and restrictions for those levels. This is no different from other kinds of infrastructure work that you do to make your systems easy to monitor and maintain.&lt;/p&gt;

&lt;p&gt;You will also want to control access at a high level such as network layers and application domains. This is again very much context dependent, but things like &lt;a href="https://kubernetes.io/docs/reference/kubectl/kubectl/"&gt;kubectl&lt;/a&gt; take you most of the way there.&lt;/p&gt;

&lt;p&gt;And here’s the big one: TURN OFF THE REPL IN PRODUCTION. You really don’t want an active REPL to go live (well…unless you do–see below!). The safest way to do this is to treat the REPL as being under feature flag control, switched off by default. Add some failsafes by also checking general environment characteristics, like system variables. A REPL falls into the same bucket as system monitoring and control, so you can apply the same DevOps control principles. &lt;/p&gt;

&lt;p&gt;And finally, you can run a REPL in production. It’s not as crazy as it sounds. A REPL lets you resolve emergency production issues. It gives you a fine-grained administration interface. It lets you perform maintenance you have not yet automated. These are all massive time savers, especially in the early days of a startup, before you have full systems in place.&lt;/p&gt;

&lt;p&gt;Access to the production REPL should be very tightly controlled. And I like to add in additional layers of security, such as time-based one-time password verification. You could even start using SSL certs if you’re really paranoid.&lt;/p&gt;

&lt;p&gt;I have found that the operational benefits for power users are worth the risk.&lt;/p&gt;

&lt;p&gt;There is one important thing you’ll need to add to your REPL implementation if you go all the way to production. An audit log. You’ll need to keep a log of all the commands, and who ran them. And you’ll need to add some scrubbing logic to remove sensitive data. Your REPL is now a serious systems management tool, so will require more effort to maintain.&lt;/p&gt;

&lt;p&gt;I’ve been using REPLs in all these ways for more than a decade. A REPL makes me faster and happier as a developer. It will make you faster and happier too - use a REPL!&lt;/p&gt;

</description>
    </item>
    <item>
      <title>@seneca/repl Version 6 Released!</title>
      <dc:creator>Richard Rodger</dc:creator>
      <pubDate>Wed, 02 Aug 2023 17:04:09 +0000</pubDate>
      <link>https://dev.to/rjrodger/senecarepl-version-6-released-277g</link>
      <guid>https://dev.to/rjrodger/senecarepl-version-6-released-277g</guid>
      <description>&lt;p&gt;I've released a substantial update to the &lt;a href="https://github.com/senecajs/seneca-repl"&gt;@seneca/repl&lt;/a&gt; plugin!&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/senecajs/seneca-repl"&gt;@seneca/repl&lt;/a&gt; plugin provides a &lt;a href="https://en.wikipedia.org/wiki/Read%E2%80%93eval%E2%80%93print_loop"&gt;REPL&lt;/a&gt; for the Seneca microservices framework. As one of the earliest plugins, it has proven to be incredibly useful. A REPL (Read-Execute-Print-Loop) offers an interactive space to write code and execute it instantly. If you've ever used the browser console or run the command &lt;code&gt;node&lt;/code&gt; in Node.js, you've used a REPL.&lt;/p&gt;

&lt;p&gt;To learn more about the plan for this release, refer to my previous post: &lt;a href="http://www.richardrodger.com/2023/07/26/seneca-repl-version-2-x-plan/"&gt;@seneca/repl version 2.x plan&lt;/a&gt; (Yes, I did say 2.x - that was a brain glitch!). Read that post to understand what the Seneca REPL can do for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  New Feature: Entity Commands
&lt;/h2&gt;

&lt;p&gt;The Seneca REPL allows you to send messages directly to a running Seneca instance, just by entering the JSON source of the message. In fact, it's even simpler than that, as the REPL accepts a relaxed form of JSON called &lt;a href="https://github.com/jsonicjs/jsonic"&gt;Jsonic&lt;/a&gt;, which lets you avoid most strict JSON syntax rules.&lt;/p&gt;

&lt;p&gt;Here's an example of using the REPL to get the status of a Seneca instance:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;seneca-repl
&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; role:seneca,stats:true
&lt;span class="o"&gt;{&lt;/span&gt;
  start: &lt;span class="s1"&gt;'2023-08-01T19:23:23.274Z'&lt;/span&gt;,
  act: &lt;span class="o"&gt;{&lt;/span&gt; calls: 105, &lt;span class="k"&gt;done&lt;/span&gt;: 104, fails: 0, cache: 0 &lt;span class="o"&gt;}&lt;/span&gt;,
  actmap: undefined,
  now: &lt;span class="s1"&gt;'2023-08-01T19:29:00.108Z'&lt;/span&gt;,
  &lt;span class="nb"&gt;uptime&lt;/span&gt;: 336834
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this interaction, the full JSON message was submitted:&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;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"seneca"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"stats"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="kc"&gt;true&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 equivalent to the Seneca API call:&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="nx"&gt;seneca&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;act&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;role&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;seneca&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;stats&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Also, since Seneca accepts Jsonic too:&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="nx"&gt;seneca&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;act&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;role:seneca,stats:true&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;When working with Seneca data entities that provide a simple &lt;a href="https://en.wikipedia.org/wiki/Object%E2%80%93relational_mapping"&gt;ORM&lt;/a&gt; to access your database, you can interact with them using standard Seneca messages. For example:&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="nx"&gt;seneca&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;foo&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;list$&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="c1"&gt;// lists all rows in the table "foo"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the REPL, this would be the equivalent message:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; sys:entity,cmd:load,name:foo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The REPL itself also provides a Seneca instance, allowing you to write standard Seneca code.&lt;/p&gt;

&lt;p&gt;However, adhering to &lt;a href="https://thethreevirtues.com/"&gt;Larry Wall's programming virtues: Laziness, Impatience, and Hubris&lt;/a&gt;, I've introduced a REPL shortcut for data entities, as they are empirically the most common use case for the REPL.&lt;/p&gt;

&lt;p&gt;Each entity operation (&lt;code&gt;list$, load$, save$, remove$&lt;/code&gt;) gets its own REPL command, all following the same syntax:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;CMD&lt;span class="nv"&gt;$ &lt;/span&gt;CANON &lt;span class="o"&gt;[&lt;/span&gt;QUERY]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here are some examples:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; list&lt;span class="nv"&gt;$ &lt;/span&gt;sys/user
&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; list&lt;span class="nv"&gt;$ &lt;/span&gt;sys/user group:foo
&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; save&lt;span class="nv"&gt;$ &lt;/span&gt;sys/user &lt;span class="nb"&gt;id&lt;/span&gt;:aaa,group:bar
&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; load&lt;span class="nv"&gt;$ &lt;/span&gt;sys/user aaa
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The REPL commands provide various functions to manage data entities, and details and examples can be found in the article.&lt;/p&gt;

&lt;h2&gt;
  
  
  New Feature: Auto Reconnection
&lt;/h2&gt;

&lt;p&gt;To enhance developer experience, the REPL client now automatically reconnects to the server if disconnected, using a backoff mechanism. You can also hit the return key to reconnect instantly.&lt;/p&gt;

&lt;h2&gt;
  
  
  New Feature: REPL Tunnels
&lt;/h2&gt;

&lt;p&gt;Configuring REPL tunnels has been simplified, and it's now possible to drive the REPL using Seneca messages. We've introduced HTTP and AWS Lambda tunnels, and detailed instructions are provided in the article.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;WARNING:&lt;/strong&gt; This feature is a security risk! Don't expose your production systems without additional access controls.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  New Feature: Isolated History
&lt;/h2&gt;

&lt;p&gt;This release improves command history storage, now saved locally in a hidden &lt;code&gt;.seneca&lt;/code&gt; folder in your home folder. It keeps a separate history for each unique connection URL, and histories are not truncated.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;seneca-repl localhost?project&lt;span class="o"&gt;=&lt;/span&gt;foo &lt;span class="c"&gt;# unique command history for the "foo" project&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;seneca-repl localhost?project&lt;span class="o"&gt;=&lt;/span&gt;bar &lt;span class="c"&gt;# unique command history for the "bar" project&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Done!
&lt;/h2&gt;

&lt;p&gt;With this new, eagerly anticipated release of the @seneca/repl plugin, there are many features to explore and enjoy. As it's open-source, you can find it on &lt;a href="https://github.com/senecajs/seneca-repl"&gt;GitHub&lt;/a&gt;, where you're welcome to submit bugs, issues, and feature requests. Enjoy!&lt;/p&gt;

</description>
    </item>
    <item>
      <title>@seneca/repl version 2.x plan</title>
      <dc:creator>Richard Rodger</dc:creator>
      <pubDate>Mon, 24 Jul 2023 12:07:41 +0000</pubDate>
      <link>https://dev.to/rjrodger/senecarepl-version-2x-plan-10bi</link>
      <guid>https://dev.to/rjrodger/senecarepl-version-2x-plan-10bi</guid>
      <description>&lt;p&gt;I'm updating the &lt;a href="https://github.com/senecajs/seneca-repl"&gt;@seneca/repl&lt;/a&gt; plugin! Here is the basic plan.&lt;/p&gt;

&lt;p&gt;NOTE: There's a &lt;a href="https://github.com/orgs/senecajs/projects/2"&gt;&lt;code&gt;@seneca/repl dev&lt;/code&gt; Github Project&lt;/a&gt; to track this work.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/senecajs/seneca-repl"&gt;@seneca/repl&lt;/a&gt; plugin provides a &lt;a href="https://en.wikipedia.org/wiki/Read%E2%80%93eval%E2%80%93print_loop"&gt;REPL&lt;/a&gt; for the seneca microservices framework. It is one of the earliest plugins, and has proven to be one of the most useful. A REPL (Read-Execute-Print-Loop) is an interactive space to write code and get it executed right away. If you've used the browser console, you've used a REPL. With Node.js, you also get a REPL, just run the command &lt;code&gt;node&lt;/code&gt; by itself and off you go!&lt;/p&gt;

&lt;p&gt;The big thing about a REPL is the speed boost it gives your development process. You just type stuff in and it works right away. You can directly inspect objects to see what they are made of. Debugging is much easier.&lt;/p&gt;

&lt;p&gt;The Seneca REPL provides you with the standard Node.js REPL -- you can execute arbitrary JavaScript. But it also provides you with access to the root Seneca instance, and with shortcuts to post Seneca messages, and examine the running Seneca system.&lt;/p&gt;

&lt;p&gt;For example, if you have a message &lt;code&gt;foo:bar,zed:qaz&lt;/code&gt;, then you can post that message directly in the REPL:&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&amp;gt; foo:bar,zed:qaz&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;The REPL accepts any valid JSON (or the equivalent Jsonic form of abbreviated JSON) as an input and attempts to post the input as a message, printing the result. Combine this with hot-reloading from the &lt;a href="https://github.com/senecajs/seneca-reload"&gt;@seneca/reload&lt;/a&gt; plugin and you have a lovely little high speed local development environment for your microservice system.&lt;/p&gt;

&lt;p&gt;An update for &lt;a href="https://github.com/senecajs/seneca-repl"&gt;@seneca/repl&lt;/a&gt; is long overdue. I've created a Github project to track the main tasks. The most important new feature is the ability to interact with the REPL when your microservice is running in a serverless context.&lt;/p&gt;

&lt;p&gt;A traditional REPL exposes a network port and you cannot do that over the network. This is fine for local development, but it is not supported in a serverless context. However most serverless environments provide an invocation mechanism so you can send messages to your deployed function. I'm extending &lt;a href="https://github.com/senecajs/seneca-repl"&gt;@seneca/repl&lt;/a&gt; so that it can support this interaction pathway by providing a special message: &lt;code&gt;sys:repl,send:cmd&lt;/code&gt; that can run REPL commands and collect their output.&lt;/p&gt;

&lt;p&gt;To implement this some refactoring is required. The old codebase is pretty old. As in, callback-hell old. It also assumes a network socket. So this all has to be pulled apart and abstracted a little. The code is very much streams-based, and that also makes it fun, as the streams have to be marshalled to operate via a request/response interaction.&lt;/p&gt;

&lt;p&gt;One issue in the existing code is the lack of a delimiter for the command result. It all sort of works by accident! I'm going to use NUL as the delimiter to mark the end of command response text. This should also clear up some bizarro bugs.&lt;/p&gt;

&lt;p&gt;The other new feature is more direct support for Seneca entities with an extended set of special commands: &lt;code&gt;list$&lt;/code&gt;, &lt;code&gt;save$&lt;/code&gt;, &lt;code&gt;load$&lt;/code&gt;, etc. that mirror the Seneca entity operations. This is a pretty big use case and we've been putting up with the kludgy workaround of using entity messages directly for ... too many years, sigh.&lt;/p&gt;

&lt;p&gt;On the command-line side, the REPL client needs to be extended to perform serverless invocations. A stream overlay will be used for this to preserve streams as the basic abstraction for REPL communication.&lt;/p&gt;

&lt;p&gt;The other tasks are housekeeping to move to the new Seneca standard test runner, &lt;a href="https://jestjs.io/"&gt;Jest&lt;/a&gt;, and convert the codebase to &lt;a href="https://www.typescriptlang.org/"&gt;Typescript&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Once this release is out, the &lt;a href="https://github.com/senecajs/seneca-gateway"&gt;@seneca/gateway&lt;/a&gt; plugins will need to be updated to handle security more generally when exposing a REPL. At the moment we tend to use a &lt;code&gt;monitor&lt;/code&gt; serverless function that has no external exposure, and can only be called by invocation with appropriate credentials. This &lt;code&gt;monitor&lt;/code&gt; function also uses our little trick of embedding all the other microservices as a modular monolith so that you can use the REPL to post any message. While this is mostly sufficient in practice, it would be nice to also be able to invoke any function directly via the REPL.&lt;/p&gt;

</description>
      <category>seneca</category>
      <category>plugin</category>
      <category>repl</category>
      <category>planning</category>
    </item>
  </channel>
</rss>
