<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Marcus Weinberger - Developer Blog]]></title><description><![CDATA[Marcus Weinberger, ethical hacker and developer - blog.]]></description><link>https://blog.marcusj.org</link><image><url>https://cdn.hashnode.com/res/hashnode/image/upload/v1681784368663/_o5nDYSSk.png</url><title>Marcus Weinberger - Developer Blog</title><link>https://blog.marcusj.org</link></image><generator>RSS for Node</generator><lastBuildDate>Sat, 12 Sep 2026 23:29:24 GMT</lastBuildDate><atom:link href="https://blog.marcusj.org/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Custom Analytics for Hashnode - Part 2]]></title><description><![CDATA[This is a brief update, continuing from my previous post where I built a basic, custom analytics service to use on my Hashnode blog.
In my final thoughts, I said:

While I'm happy with what I created, there are still some things I'd like to work on. ...]]></description><link>https://blog.marcusj.org/custom-analytics-for-hashnode-part-2</link><guid isPermaLink="true">https://blog.marcusj.org/custom-analytics-for-hashnode-part-2</guid><category><![CDATA[Hashnode]]></category><category><![CDATA[analytics]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[Pocketbase ]]></category><dc:creator><![CDATA[Marcus Weinberger]]></dc:creator><pubDate>Tue, 09 May 2023 16:44:57 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1683648197612/f37551dc-a9bf-4b67-b2a6-3dd52f69192b.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em>This is a brief update, continuing from</em> <a target="_blank" href="https://marcus.hashnode.dev/creating-a-custom-analytics-engine-for-my-hashnode-blog"><em>my previous post</em></a> <em>where I built a basic, custom analytics service to use on</em> <a target="_blank" href="https://marcus.hashnode.dev"><em>my Hashnode blog</em></a><em>.</em></p>
<p>In my final thoughts, I said:</p>
<blockquote>
<p>While I'm happy with what I created, there are still some things I'd like to work on. Having more security rules for the PocketBase collection is number one, however, I want to add an option that will display total views in the iframe. Doing so would require better security, and possibly a backend server.</p>
</blockquote>
<p>Thankfully (for my budget) I was wrong about needing a backend server. <a target="_blank" href="https://pocketbase.io/">PocketBase</a>, alone, is enough to accomplish everything.</p>
<h3 id="heading-goals">Goals</h3>
<ul>
<li><p>Include an option (like <code>show=true</code>) in the URL that will display total views.</p>
</li>
<li><p>Make it so - for security - only the number of views (aka, entries in my PocketBase collection) can be queried, not any of the analytics data.</p>
</li>
</ul>
<p>After a rough start trying to experiment with PocketBase rules, I asked the community for help with my problem. Below, is the reply from <a target="_blank" href="https://github.com/ganigeorgiev">@ganigeorgiev</a> which saved me from resorting to creating a backend server.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://github.com/pocketbase/pocketbase/discussions/2449#discussioncomment-5848751">https://github.com/pocketbase/pocketbase/discussions/2449#discussioncomment-5848751</a></div>
<p> </p>
<h2 id="heading-updating-our-pocketbase">Updating our PocketBase</h2>
<p>Turns out, in order to only allow viewing the number of results and not the content of said results, another collection is needed. However, not the same type of collection. I didn't know about this, but PocketBase has a <a target="_blank" href="https://pocketbase.io/docs/collections#view-collection">"view" collection type</a> which is a collection that's content reflects another collection's (or multiple), with filters applied.</p>
<p>With a little modification to group counts by our <code>slug</code> (title of blog post), I ended up with this:</p>
<pre><code class="lang-sql"><span class="hljs-keyword">SELECT</span> <span class="hljs-keyword">id</span>, slug, <span class="hljs-keyword">COUNT</span>(*) <span class="hljs-keyword">as</span> total
<span class="hljs-keyword">FROM</span> <span class="hljs-string">`basic_analytics`</span>
<span class="hljs-keyword">GROUP</span> <span class="hljs-keyword">BY</span> slug
</code></pre>
<p>Your new collection settings in PocketBase should look something like this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1683649216812/86806f63-f150-4e44-8a16-cee1eaf6de6b.png" alt class="image--center mx-auto" /></p>
<p>But there's still one more change to make, in the <strong>API Rules</strong> section, unlock both methods. This will allow <strong>anyone with your PocketBase URL</strong> to view <strong><em>only</em></strong> the number of views for each post.</p>
<p>After saving the new collection, if you have records in your <code>basic_analytics</code> collection, your new collection will look like this (I've named mine <code>view_analytics</code>):</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1683649376558/2ac43722-2c24-4677-b4c2-df71ca1e9480.png" alt class="image--center mx-auto" /></p>
<p>Mine has some genuine, and some test data. It took me a while to work out all the issues.</p>
<h2 id="heading-updating-our-htmljavascript">Updating our HTML/JavaScript</h2>
<p>As our website will be embedded in Hashnode posts, I'm not going to focus on styling. <s>Also because I despise CSS</s>. So let's create a simple <code>div</code> , hidden at first, where we can display the number of views.</p>
<pre><code class="lang-xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">id</span>=<span class="hljs-string">'views'</span> <span class="hljs-attr">style</span>=<span class="hljs-string">'display: none'</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">h1</span>&gt;</span>Total Views: <span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">id</span>=<span class="hljs-string">'viewnum'</span>&gt;</span>...<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">h1</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
</code></pre>
<p>Next, we need a function to query the new collection and update the element. You may ask, <strong>why am I doing this in the same function?</strong> Well, the PocketBase SDK actually uses asynchronous functions. Previously, I didn't use <code>await</code> as our functions were synchronous - I just assume the promise will eventually resolve (which it seems to do so just fine).</p>
<p>This time, I'll write an <code>async</code> function, so we can get the value from PocketBase and then use it. However, the function still won't be awaited properly, and we will assume it resolves again. If someone would like to leave a comment with fully asynchronous code I would appreciate it, for now I think I'll stick to <em>"if it ain't broke, don't fix it"</em>.</p>
<h3 id="heading-getting-view-count-in-javascript">Getting view count in JavaScript</h3>
<pre><code class="lang-javascript"><span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getTotalViews</span>(<span class="hljs-params">span_id</span>) </span>{
    <span class="hljs-comment">// get matches for slug from URL params</span>
    <span class="hljs-keyword">const</span> count = <span class="hljs-keyword">await</span> pb.collection(<span class="hljs-string">'view_analytics'</span>).getFirstListItem(
        <span class="hljs-string">`slug="<span class="hljs-subst">${params.get(<span class="hljs-string">'slug'</span>)}</span>"`</span>
    );
    <span class="hljs-comment">// update the element with the view count</span>
    <span class="hljs-built_in">document</span>.getElementById(span_id).innerText = count.total;
}
</code></pre>
<p><strong>If you're confused</strong>, you really should <a target="_blank" href="https://blog.marcusj.tech/creating-a-custom-analytics-engine-for-my-hashnode-blog">read my first post</a>, which explains how we got to this point.</p>
<p>Next, let's update our main function - where we create the entry in our collection. Right after our <code>console.log</code> where we log the new record, and the data it contains, we'll add a simple <code>if</code> statement which will toggle the visibility of our view count <code>div</code>, and call our <code>getTotalViews</code> function.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">if</span> (params.get(<span class="hljs-string">'show'</span>)) {
    <span class="hljs-built_in">document</span>.getElementById(<span class="hljs-string">'views'</span>).style.display = <span class="hljs-string">'block'</span>;
    getTotalViews(<span class="hljs-string">'viewnum'</span>);
}
</code></pre>
<p>As I mentioned earlier, we can't use <code>await</code> as the function containing this code is synchronous. But, <strong>that's it!</strong></p>
<p>At the end of this post, I have inserted some HTML with Hasnode, to use our analytics and display the view count. Here is the code:</p>
<pre><code class="lang-xml"><span class="hljs-tag">&lt;<span class="hljs-name">iframe</span> <span class="hljs-attr">src</span>=<span class="hljs-string">'https://&lt;your-repl&gt;.&lt;your-replit-username&gt;.repl.co/?slug=custom_analytics_pt2&amp;show=true'</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">iframe</span>&gt;</span>
</code></pre>
<h3 id="heading-see-it-in-action">See it in action</h3>
<iframe src="https://basichashnodeanalytics.marcusweinberger.repl.co/?slug=custom_analytics_pt2&amp;show=true"></iframe>]]></content:encoded></item><item><title><![CDATA[Creating a custom Analytics engine for my Hashnode blog]]></title><description><![CDATA[View part 2 of this article here!
IntroductionHi, my name is Marcus Weinberger. I'm mainly a Python developer (due to my love for hacking), but I work with JS when I must. One of my passions is creating my own software for doing things - I really enj...]]></description><link>https://blog.marcusj.org/creating-a-custom-analytics-engine-for-my-hashnode-blog</link><guid isPermaLink="true">https://blog.marcusj.org/creating-a-custom-analytics-engine-for-my-hashnode-blog</guid><category><![CDATA[analytics]]></category><category><![CDATA[Hashnode]]></category><category><![CDATA[Python]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[Pocketbase ]]></category><dc:creator><![CDATA[Marcus Weinberger]]></dc:creator><pubDate>Tue, 09 May 2023 11:35:47 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1683628922910/6065fea9-76ef-4607-b44e-d74bed77b62f.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><a target="_blank" href="https://marcus.hashnode.dev/custom-analytics-for-hashnode-part-2"><em>View part 2 of this article here!</em></a></p>
<details><summary>Introduction</summary><div data-type="detailsContent">Hi, my name is <a target="_blank" href="https://marcusj.tech">Marcus Weinberger</a>. I'm mainly a Python developer (due to my love for hacking), but I work with JS when I must. One of my passions is creating my own software for doing things - I really enjoy being able to not rely on third parties. This doesn't always benefit me (<a target="_blank" href="https://blog.marcusj.tech/adding-a-list-of-my-hashnode-posts-to-my-personal-website#heading-why-the-update">check out the three links to blogs that I wrote from scratch</a> - moving to <a target="_blank" href="https://marcus.hashnode.dev">Hashnode</a> ended up being the best for me), but it is a good learning process nonetheless.</div></details>

<p>So, Hashnode has some very nice analytics. I like how you can see how much time people spend on your articles. But one feature that I'd like to see is more location data collected, I want to know where my readers are coming from. Why? Previously, I misjudged potential revenue due to not realizing <strong>over 60% of my userbase resided in Myanmar</strong> and were only using <a target="_blank" href="https://replit.com/@MarcusWeinberger/gptfree-telegram-bot?c=891113">my service</a> for its free nature.</p>
<h2 id="heading-initial-ideas">Initial ideas</h2>
<p>Initially, my plan was to create a Hashnode widget with some JavaScript that would collect some user data and then send it off to a database, however, Hashnode seems to disallow Javascript, oddly.</p>
<p>Iframes, on the other hand, are allowed to execute JavaScript (I assume so that pages can still function). To save costs on hosting, I decided to use an <a target="_blank" href="https://repl.new/html">HTML Repl</a>, and a <a target="_blank" href="https://pocketbase.io/">PocketBase</a> instance hosted on <a target="_blank" href="https://www.linode.com/">linode</a> for the database.</p>
<h1 id="heading-development">Development</h1>
<p>After creating a blank website on <a target="_blank" href="https://replit.com">replit</a>, I used <a target="_blank" href="https://cdnjs.com/">cdnjs</a> to find the libraries I need. For the PocketBase connection, I used their provided SDK. For analyzing browser information, I will use the <a target="_blank" href="https://github.com/bestiejs/platform.js/">platform.js</a> library.</p>
<pre><code class="lang-xml"><span class="hljs-comment">&lt;!-- pocketbase sdk --&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">script</span> <span class="hljs-attr">src</span>=<span class="hljs-string">"https://cdnjs.cloudflare.com/ajax/libs/pocketbase/0.8.0-rc1/pocketbase.umd.min.js"</span> <span class="hljs-attr">integrity</span>=<span class="hljs-string">"sha512-0NJSuVFhF9NPZ/UAp98rCmJTTLhvjYwn2Uu4HN5eXE3uYfT6xad6WV6XuTmoKMMnj43yPT+kEyaCix1/t+8Tkw=="</span> <span class="hljs-attr">crossorigin</span>=<span class="hljs-string">"anonymous"</span> <span class="hljs-attr">referrerpolicy</span>=<span class="hljs-string">"no-referrer"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
<span class="hljs-comment">&lt;!-- platform.js --&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">script</span> <span class="hljs-attr">src</span>=<span class="hljs-string">"https://cdnjs.cloudflare.com/ajax/libs/platform/1.3.6/platform.min.js"</span> <span class="hljs-attr">integrity</span>=<span class="hljs-string">"sha512-eYPrm8TgYWg3aa6tvSRZjN4v0Z9Qx69q3RhfSj+Mf89QqwOMqmwSlsVqfp4N8NVAcZe/YeUhh9x/nM2CAOp6cA=="</span> <span class="hljs-attr">crossorigin</span>=<span class="hljs-string">"anonymous"</span> <span class="hljs-attr">referrerpolicy</span>=<span class="hljs-string">"no-referrer"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
<span class="hljs-comment">&lt;!-- jquery --&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">script</span> <span class="hljs-attr">src</span>=<span class="hljs-string">'https://code.jquery.com/jquery.min/js'</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
</code></pre>
<p>Now let's set up the collection in PocketBase. Once you have a PocketBase instance set up following their instructions (make sure you remember the admin user password), you can create a new collection for the data. I called mine <code>basic_analytics</code>. Make sure to add all the fields you need, mine looks like this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1683630125339/21f99bb6-d8ee-45b0-bec1-503043295609.png" alt class="image--center mx-auto" /></p>
<p>For security, I'm not going to do too much. You may want to make some changes, but I'm fairly new to PocketBase, and what I made works for me. In the API Rules, keep everything on admins only mode, but unlock the <code>Create</code> rule and leave it blank (allowing anyone to create new records). It should look like this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1683630286848/98878415-0b2d-4058-ab2f-bc2e46e656f3.png" alt class="image--center mx-auto" /></p>
<p>Everything not shown above has been left as the default.</p>
<h2 id="heading-javascript">JavaScript</h2>
<p>Going back to your website, the HTML repl, let's create the connection to your PocketBase instance.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> pb = <span class="hljs-keyword">new</span> PocketBase(<span class="hljs-string">'https://&lt;your pocketbase server&gt;'</span>);
</code></pre>
<p>As we don't require authentication to create records, we don't need to worry about keys or anything.</p>
<p>Now let's add some JavaScript which will collect some analytics data, and add a new entry to our database. We will be using a URL parameter to let our script know which post we are collecting data for.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> agent = navigator.userAgent; <span class="hljs-comment">// get userAgent string</span>
<span class="hljs-keyword">var</span> info = platform.parse(agent); <span class="hljs-comment">// parse with platform.js</span>
<span class="hljs-keyword">var</span> params = <span class="hljs-keyword">new</span> URLSearchParams(<span class="hljs-built_in">window</span>.location.search); <span class="hljs-comment">// get req params</span>

<span class="hljs-comment">// ipinfo is a free service to get ip and location</span>
$.getJSON(<span class="hljs-string">'https://ipinfo.io'</span>, <span class="hljs-function">(<span class="hljs-params">resp</span>) =&gt;</span> {
    <span class="hljs-keyword">var</span> data = {
        <span class="hljs-string">'slug'</span>: params.get(<span class="hljs-string">'slug'</span>), <span class="hljs-comment">// blog post slug</span>

        <span class="hljs-string">'ip'</span>: resp.ip, 
        <span class="hljs-string">'country'</span>: resp.country <span class="hljs-comment">// country code</span>
        <span class="hljs-string">'postal'</span>: resp.postal, <span class="hljs-comment">// only the first couple characters</span>

        <span class="hljs-string">'browser'</span>: info.name, <span class="hljs-comment">// e.g., Chrome</span>
        <span class="hljs-string">'os'</span>: info.os.family <span class="hljs-comment">// e.g., Windows</span>
    }

    <span class="hljs-comment">// create record (basic_analytics is our collection name)</span>
    <span class="hljs-keyword">let</span> doc_p = pb.collection(<span class="hljs-string">'basic_analytics'</span>).create(data);
    <span class="hljs-comment">// log for debugging purposes</span>
    <span class="hljs-built_in">console</span>.log({
        doc_p,
        data
    });
})
</code></pre>
<p>Now, when we attempt to visit our website, we see a blank page, but a record is added to our database!</p>
<h3 id="heading-collecting-analytics">Collecting analytics</h3>
<p>In order to collect analytics for your blog post, use the new "Insert HTML" feature when writing, and link to your website like so:</p>
<pre><code class="lang-xml"><span class="hljs-tag">&lt;<span class="hljs-name">iframe</span> <span class="hljs-attr">src</span>=<span class="hljs-string">'https://&lt;repl-name&gt;.&lt;your-repl-username&gt;.repl.co/?slug=post_slug_or_identifier'</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">iframe</span>&gt;</span>
</code></pre>
<p>It's as simple as that!</p>
<h2 id="heading-viewing-analytics-with-python">Viewing analytics with Python</h2>
<p>We could just log into our PocketBase instance every time we desired to view our analytics, but that's not fun. So let's write a simple Python script that <strong>authenticates with PocketBase as an admin</strong> and shows us a <strong>pretty table</strong> of analytics.</p>
<p>Before we start, we need to ensure these libraries are installed (with <code>pip install library-name</code> ):</p>
<ul>
<li><p><a target="_blank" href="https://github.com/vaphes/pocketbase"><code>pocketbase</code></a> - python client for PocketBase</p>
</li>
<li><p><a target="_blank" href="https://github.com/Textualize/rich"><code>rich</code></a> - a library for pretty outputs (and creating a table in the console)</p>
</li>
</ul>
<p>Now let's start writing. Create a new Python file, call it whatever you want. First, we need to import everything that we will use.</p>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> argparse <span class="hljs-keyword">import</span> ArgumentParser <span class="hljs-comment"># to make a cli interface</span>
<span class="hljs-keyword">from</span> pocketbase <span class="hljs-keyword">import</span> PocketBase <span class="hljs-comment"># pocketbase client</span>
<span class="hljs-keyword">from</span> rich.table <span class="hljs-keyword">import</span> Table <span class="hljs-comment"># creating tables in the console</span>
<span class="hljs-keyword">from</span> getpass <span class="hljs-keyword">import</span> getpass <span class="hljs-comment"># get password input securely</span>
<span class="hljs-keyword">from</span> rich <span class="hljs-keyword">import</span> <span class="hljs-keyword">print</span> <span class="hljs-comment"># pretty printing</span>
</code></pre>
<p>Run the file to make sure everything is installed and working. Next, let's create the CLI.</p>
<pre><code class="lang-python">parser = ArgumentParser() <span class="hljs-comment"># new arg parser</span>
parser.add_argument(
    <span class="hljs-string">'-s'</span>, <span class="hljs-string">'--slug'</span>, <span class="hljs-comment"># name</span>
    help=<span class="hljs-string">'The slug of the blog post to get analytics for'</span> <span class="hljs-comment"># desc</span>
)
parser.add_argument(
    <span class="hljs-string">'-p'</span>, <span class="hljs-string">'--password'</span>, 
    help=<span class="hljs-string">'The password to use to authenticate with the API'</span>
)
args = parser.parse_args() <span class="hljs-comment"># read args from command line args</span>
</code></pre>
<p>Now we're ready to authenticate with PocketBase. We will add a fallback from the CLI that will prompt the user if a password was not given via argument.</p>
<pre><code class="lang-python">pb = PocketBase(<span class="hljs-string">'https://&lt;your-pb-server&gt;'</span>)
adminData = pb.admins.auth_with_password(
    <span class="hljs-string">'&lt;admin-email-addr&gt;'</span>,
    (args.password <span class="hljs-keyword">or</span> getpass()) <span class="hljs-comment"># use args, or fallback to prompt </span>
)
pb.auth_store.save(adminData.token) <span class="hljs-comment"># it took me 1 hour to figure this out</span>
</code></pre>
<p>Now that we are authenticated as an admin, we can access all the "locked" methods for our PocketBase collection. Let's create our query parameters and get the results from our database.</p>
<pre><code class="lang-python">query_params = {
    <span class="hljs-string">'per_page'</span>: <span class="hljs-number">100</span> <span class="hljs-comment"># up to 100 results returned</span>
}
<span class="hljs-keyword">if</span> args.slug: <span class="hljs-comment"># if not given, will just return all posts' analytics</span>
    query_params[<span class="hljs-string">'filter'</span>] = <span class="hljs-string">f'slug="<span class="hljs-subst">{args.slug}</span>"'</span>

records = pb.collection(<span class="hljs-string">'basic_analytics'</span>).get_full_list(query_params=query_params)
</code></pre>
<p>Now, using the <code>rich</code> library, let's show the results in a nice, readable way.</p>
<pre><code class="lang-python"><span class="hljs-comment"># create the table</span>
table = Table(
    title=<span class="hljs-string">f'Analytics for <span class="hljs-subst">{args.slug <span class="hljs-keyword">or</span> <span class="hljs-string">"all posts"</span>}</span>. Total: <span class="hljs-subst">{len(records)}</span>.'</span>
)
table.add_column(<span class="hljs-string">'Date'</span>)
table.add_column(<span class="hljs-string">'IP'</span>)
table.add_column(<span class="hljs-string">'Country'</span>)
table.add_column(<span class="hljs-string">'Postal'</span>)
table.add_column(<span class="hljs-string">'Browser'</span>)
table.add_column(<span class="hljs-string">'OS'</span>)
<span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> args.slug:
    table.add_column(<span class="hljs-string">'Slug'</span>)

<span class="hljs-comment"># populate</span>
<span class="hljs-keyword">for</span> record <span class="hljs-keyword">in</span> records: 
    data = record.collection_id <span class="hljs-comment"># contains entry data</span>
    row = [
        <span class="hljs-string">f"[green]<span class="hljs-subst">{data[<span class="hljs-string">'created'</span>]}</span>[/green]"</span>,
        data[<span class="hljs-string">'ip'</span>],
        <span class="hljs-string">f"[green]<span class="hljs-subst">{data.get(<span class="hljs-string">'country'</span>, <span class="hljs-string">'Unknown'</span>)}</span>[/green]"</span>,
        data.get(<span class="hljs-string">'postal'</span>, <span class="hljs-string">'Unknown'</span>),
        <span class="hljs-string">f"[green]<span class="hljs-subst">{data.get(<span class="hljs-string">'browser'</span>, <span class="hljs-string">'Unknown'</span>)}</span>[/green]"</span>,
        data.get(<span class="hljs-string">'os'</span>, <span class="hljs-string">'Unknown'</span>)
    ]
    <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> args.slug:
        row.append(<span class="hljs-string">f"[green]<span class="hljs-subst">{data[<span class="hljs-string">'slug'</span>]}</span>[/green]"</span>)

    table.add_row(*row) <span class="hljs-comment"># add row to table</span>

<span class="hljs-comment"># display table</span>
print(table) <span class="hljs-comment"># remember, this print is imported from rich</span>
</code></pre>
<p>I decided to add color to every other row to make the final result more readable.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1683632030149/cd77155b-f955-4e36-b11d-9cfe976c2ff3.png" alt class="image--center mx-auto" /></p>
<h1 id="heading-final-thoughts">Final thoughts</h1>
<p>While I'm happy with what I created, there are still some things I'd like to work on. Having more security rules for the PocketBase collection is number one, however, I want to add an option that will display total views in the iframe. Doing so would require better security, and possibly a backend server.</p>
<p>For now, I will be using it on all future posts.</p>
<iframe src="https://basichashnodeanalytics.marcusweinberger.repl.co/?slug=creating_custom_analytics&amp;show=true"></iframe>

<p><a target="_blank" href="https://marcus.hashnode.dev/custom-analytics-for-hashnode-part-2"><em>View part 2 of this article here!</em></a></p>
]]></content:encoded></item><item><title><![CDATA[Creating a basic split-panel console interface in Python (with i/o)]]></title><description><![CDATA[BackstoryHi, my name is Marcus Weinberger. My two biggest interests are cyber security and programming, which (paired with recent advancements in tech) led me to begin the development of 1337GPT - an AI penetration testing assistant. This is the stor...]]></description><link>https://blog.marcusj.org/creating-a-basic-split-panel-console-interface-in-python-with-io</link><guid isPermaLink="true">https://blog.marcusj.org/creating-a-basic-split-panel-console-interface-in-python-with-io</guid><category><![CDATA[Python]]></category><category><![CDATA[terminal]]></category><dc:creator><![CDATA[Marcus Weinberger]]></dc:creator><pubDate>Sat, 06 May 2023 16:40:23 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1683387629927/8787d7f1-6de9-443c-bcfd-9592f0b3cbf2.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<details><summary>Backstory</summary><div data-type="detailsContent">Hi, my name is <a target="_blank" href="https://marcusj.tech">Marcus Weinberger</a>. My two biggest interests are cyber security and programming, which (paired with recent advancements in tech) led me to begin the development of <a target="_blank" href="https://blog.marcusj.tech/1337gpt-yet-another-gpt-agent-for-penetration-testing">1337GPT</a> - an <strong>AI penetration testing assistant</strong>. This is the story of how I painstakingly created a terminal user interface for this project.</div></details>

<h1 id="heading-choosing-a-library">Choosing a library</h1>
<p>My requirements were simple; I wanted two verticle panels. One that displays information that updates occasionally, another that acts as a console.</p>
<p>Initially, I was drawn to <a target="_blank" href="https://textual.textualize.io/">Textual</a>. I had seen demos of what it could do not long ago, and it was impressive. They also provided an example of a split-pane layout. However, I could not figure out how to print to one of the panes. The documentation was confusing, and either I couldn't find the relevant section or it just didn't exist yet, so I gave up with that.</p>
<p>After contemplating writing one myself, I eventually stumbled upon talk of <a target="_blank" href="https://rich.readthedocs.io/en/stable/introduction.html">rich</a> (from the same organisation as Textual) being used to create a split screen. I had used rich before. It's simple. I like it. They had a new feature, called <code>Live</code>, that would run a loop displaying updated information. All I needed was a way to use all those <code>print</code> and <code>input</code> statements.</p>
<p>Turns out, rich wasn't exactly the most suited for such a task, either. But, thanks to the help of <a target="_blank" href="https://github.com/Textualize/rich/discussions/1791#discussioncomment-1908942">@zuhataslan on GitHub</a>, I had everything I needed to put it together.</p>
<h1 id="heading-building-the-framework">Building the framework</h1>
<p>I decided, to make things as simple as possible, I was going to achieve this split screen with a simple table. This seemed to be the easiest way of dividing the screen. So I created a basic class:</p>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> rich.live <span class="hljs-keyword">import</span> Live
<span class="hljs-keyword">from</span> rich.table <span class="hljs-keyword">import</span> Table

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">TUI</span>(<span class="hljs-params">object</span>):</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__init__</span>(<span class="hljs-params">self</span>):</span>
        self.table = Table(title=<span class="hljs-string">'1337GPT'</span>) <span class="hljs-comment"># title of my application</span>
        self.table.add_column(<span class="hljs-string">'Data'</span>) <span class="hljs-comment"># left pane</span>
        self.table.add_column(<span class="hljs-string">'Console'</span>) <span class="hljs-comment"># right pane</span>

<span class="hljs-comment"># ensure that it works</span>
<span class="hljs-keyword">import</span> time
tui = TUI()
<span class="hljs-keyword">with</span> Live(tui.table, refresh_per_second=<span class="hljs-number">4</span>):
    <span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> range(<span class="hljs-number">100</span>):
        tui.table.add_row(<span class="hljs-string">'b'</span> * i, <span class="hljs-string">'a'</span> * i)
        time.sleep(<span class="hljs-number">0.2</span>)
</code></pre>
<p>The last bit is just how I tested it to see how everything would look. Which, was something like this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1683388633689/9a28a4f1-c667-415b-b369-03bc053e1f96.png" alt class="image--center mx-auto" /></p>
<p>Alright. Good starting point. Next I wanted to expand the table so that it took up the whole terminal window. To do that, I used the <code>get_terminal_size</code> from the built-in <code>os</code> library. Since there's not much code so far, I'll just show you the whole updated file.</p>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> rich.live <span class="hljs-keyword">import</span> Live
<span class="hljs-keyword">from</span> rich.table <span class="hljs-keyword">import</span> Table
<span class="hljs-keyword">import</span> os

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">TUI</span>(<span class="hljs-params">object</span>):</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__init__</span>(<span class="hljs-params">self</span>):</span>
        <span class="hljs-comment"># give us some slack for the max line size</span>
        <span class="hljs-comment"># this accounts for the "|"s when formatting the table</span>
        self.max_lines = os.get_terminal_size().lines - <span class="hljs-number">7</span>
        self.total_cols = os.get_terminal_size().columns - <span class="hljs-number">4</span>

        <span class="hljs-comment"># if total_cols is odd, make it even</span>
        <span class="hljs-keyword">if</span> self.total_cols % <span class="hljs-number">2</span> == <span class="hljs-number">1</span>:
            self.total_cols -= <span class="hljs-number">1</span>

        <span class="hljs-comment"># length of characters in each pane</span>
        self.half_cols = self.total_cols // <span class="hljs-number">2</span>

        <span class="hljs-comment"># make the table</span>
        self.table = Table(title=<span class="hljs-string">'1337GPT'</span>)
        self.table.add_column(<span class="hljs-string">'Data'</span>)
        self.table.add_column(<span class="hljs-string">'Console'</span>)

        <span class="hljs-comment"># create some empty rows</span>
        <span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> range(os.get_terminal_size().lines - <span class="hljs-number">7</span>):
            self.table.add_row(<span class="hljs-string">' '</span> * self.half_cols, <span class="hljs-string">' '</span> * self.half_cols)

    <span class="hljs-comment"># this function will be useful later for padding text</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">pad</span>(<span class="hljs-params">self, text</span>):</span>
        <span class="hljs-keyword">return</span> text.ljust(self.half_cols)
</code></pre>
<p>Now, by experimenting in the Python console, I found out a little more about how rich's tables work. See, the only method that was shown in the documentation for adding data was <code>table.add_row</code>, which would update both "panes" at the same time. To work around this, I found out that once I had all those blank rows, I could simply replace a single cell using <code>table.columns[0]._cells</code> which contained a list of cells within the first column.</p>
<p>Next, I created a <code>Console</code> class which would allow me to interact with the right pane as if it were a console.</p>
<pre><code class="lang-python"><span class="hljs-comment"># update the second import</span>
<span class="hljs-keyword">from</span> rich.table <span class="hljs-keyword">import</span> Table, Row

<span class="hljs-comment"># ... other code above</span>

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Console</span>(<span class="hljs-params">object</span>):</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__init__</span>(<span class="hljs-params">self, tui: TUI</span>):</span>
        self.tui = tui <span class="hljs-comment"># to access the table contained within</span>
        self.history = [] <span class="hljs-comment"># for when we run out of space for lines</span>

    <span class="hljs-comment"># gets the current "lines" in console</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">getlines</span>(<span class="hljs-params">self</span>):</span>
        <span class="hljs-keyword">return</span> self.tui.table.columns[<span class="hljs-number">1</span>]._cells <span class="hljs-comment"># columns[1] is the Console</span>

    <span class="hljs-comment"># adds a line</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">add_line</span>(<span class="hljs-params">self, line: str</span>):</span>
        <span class="hljs-comment"># padding it ensures the width of the table remains the same</span>
        line = self.tui.pad(line)

        <span class="hljs-comment"># "scroll down" and add line</span>
        lines = self.getlines()
        self.history.append(lines.pop(<span class="hljs-number">0</span>)) <span class="hljs-comment"># preserve history</span>
        lines.append(line)
        self.tui.table.columns[<span class="hljs-number">1</span>]._cells = lines <span class="hljs-comment"># update table</span>
</code></pre>
<p>This method of adding a line to the "console" ensures that it will always have the same number of rows so that we can always see the title of the table (and application) within the current terminal view.</p>
<p>Now, we need an <code>input</code> method. This will be awkward to implement, as the rich <code>Live</code> changes how inputs behave. Anything printed (including input prompts) will be displayed <strong>above</strong> the table - which is not what I want. Here's how I added this functionality myself (the <code>...</code> represents code which was shown earlier).</p>
<pre><code class="lang-python"><span class="hljs-comment"># we need the getch function to get a single character during the Live loop</span>
<span class="hljs-keyword">if</span> os.name == <span class="hljs-string">'nt'</span>:
    <span class="hljs-comment"># windows</span>
    <span class="hljs-keyword">from</span> msvcrt <span class="hljs-keyword">import</span> getch
<span class="hljs-keyword">else</span>:
    <span class="hljs-comment"># linux based</span>
    <span class="hljs-keyword">from</span> getch <span class="hljs-keyword">import</span> getch

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Console</span>(<span class="hljs-params">object</span>):</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__init__</span>(<span class="hljs-params">self, tui: TUI</span>):</span>
        ...
        self._input = <span class="hljs-string">''</span>

    <span class="hljs-comment"># updates the text of the last line</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">update_last</span>(<span class="hljs-params">self, text: str</span>):</span>
        lines = self.getlines()
        lines[<span class="hljs-number">-1</span>] = self.tui.pad(text)
        self.tui.table.columns[<span class="hljs-number">1</span>]._cells = lines

    <span class="hljs-comment"># our print function</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">print</span>(<span class="hljs-params">self, *args</span>):</span>
        args_str = map(repr, args) <span class="hljs-comment"># convert to string representation</span>
        args_line = <span class="hljs-string">' '</span>.join(args_str) <span class="hljs-comment"># list to string</span>
        self.add_line(args_line) <span class="hljs-comment"># "print"</span>

    <span class="hljs-comment"># our input function</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">input</span>(<span class="hljs-params">self, prompt: str</span>):</span>
        <span class="hljs-comment"># print the prompt </span>
        self.add_line(prompt)

        <span class="hljs-comment"># loop until input is done</span>
        <span class="hljs-keyword">while</span> <span class="hljs-literal">True</span>:
            ch = getch().decode() <span class="hljs-comment"># gets a single character</span>
            <span class="hljs-keyword">if</span> ch == <span class="hljs-string">'\x03'</span>: <span class="hljs-comment"># Ctrl+c</span>
                <span class="hljs-keyword">raise</span> KeyboardInterrupt
            <span class="hljs-keyword">elif</span> ch <span class="hljs-keyword">in</span> (<span class="hljs-string">'\n'</span>, <span class="hljs-string">'\r'</span>): <span class="hljs-comment"># new line characters means we're done</span>
                inp = self._input
                self._input = <span class="hljs-string">''</span> <span class="hljs-comment"># clear it for next time</span>
                <span class="hljs-keyword">return</span> inp <span class="hljs-comment"># return the value - exiting the loop</span>
            <span class="hljs-keyword">elif</span> ord(ch) == <span class="hljs-number">127</span>: <span class="hljs-comment"># backspace</span>
                self._input = self._input[:<span class="hljs-number">-1</span>] <span class="hljs-comment"># remove last char</span>
                self.update_last(prompt + self._input) <span class="hljs-comment"># update line</span>
            <span class="hljs-keyword">else</span>: <span class="hljs-comment"># valid char</span>
                self._input += ch <span class="hljs-comment"># add it to the input</span>
                self.update_last(prompt + self._input) <span class="hljs-comment"># update line</span>
</code></pre>
<p>Surprisingly, this works! Here's an example of how you would use it all together:</p>
<pre><code class="lang-python"><span class="hljs-comment"># put this at the end of the file</span>
tui = TUI()
console = Console(tui)

<span class="hljs-keyword">with</span> Live(tui.table, refresh_per_second=<span class="hljs-number">10</span>): <span class="hljs-comment"># increased refresh for typing</span>
    <span class="hljs-keyword">while</span> <span class="hljs-literal">True</span>:
        cmd = console.input(<span class="hljs-string">'&gt;&gt;&gt; '</span>)

        <span class="hljs-keyword">if</span> cmd <span class="hljs-keyword">in</span> (<span class="hljs-string">'exit'</span>, <span class="hljs-string">'break'</span>, <span class="hljs-string">'exit()'</span>):
            <span class="hljs-keyword">break</span>

        <span class="hljs-keyword">elif</span> cmd:
            res = eval(cmd)
            console.print(cmd)
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1683391024474/679908d5-4ba6-477f-8d44-e025c41f4218.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-use-with-1337gpthttpsgithubcomageofmarcus1337gpt">Use with <a target="_blank" href="https://github.com/AgeOfMarcus/1337GPT"><code>1337GPT</code></a></h2>
<p>If you want to see how I integrated it with my <a target="_blank" href="https://blog.marcusj.tech/task-management-system-for-langchain">AI Task Manager (used in <code>1337GPT</code>)</a>, you can <a target="_blank" href="https://github.com/AgeOfMarcus/1337GPT/blob/main/tui.py">view the finished source code, here</a>!</p>
]]></content:encoded></item><item><title><![CDATA[1337GPT - Yet another GPT agent for Penetration Testing]]></title><description><![CDATA[1337GPT is my attempt at designing a GPT agent for pentesting. I built it on top of my previous AI Task Manager - a layer designed to be above a langchain agent that can complete complex tasks.
How does it work?
1337GPT starts off with a goal that th...]]></description><link>https://blog.marcusj.org/1337gpt-yet-another-gpt-agent-for-penetration-testing</link><guid isPermaLink="true">https://blog.marcusj.org/1337gpt-yet-another-gpt-agent-for-penetration-testing</guid><category><![CDATA[GPT 4]]></category><category><![CDATA[langchain]]></category><category><![CDATA[pentesting]]></category><category><![CDATA[AI]]></category><dc:creator><![CDATA[Marcus Weinberger]]></dc:creator><pubDate>Mon, 01 May 2023 17:00:09 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1682958536403/0819c41c-98c0-4505-a12a-8dbe1f73ad0a.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><a target="_blank" href="https://github.com/AgeOfMarcus/1337GPT"><strong>1337GPT</strong></a> is my attempt at designing a GPT agent for pentesting. I built it on top of my previous <a target="_blank" href="https://blog.marcusj.tech/task-management-system-for-langchain">AI Task Manager</a> - a layer designed to be above a <a target="_blank" href="https://python.langchain.com/en/latest/">langchain</a> agent that can complete complex tasks.</p>
<h2 id="heading-how-does-it-work">How does it work?</h2>
<p><strong>1337GPT</strong> starts off with a goal that the user sets. It will come up with a list of tasks that it thinks it will need to complete to achieve this goal. As it iterates through this list of tasks, it will give the task (along with some contextual information) to a langchain agent - equipped with tools. The result of this agent chain will be fed back into the task manager, where it will do a few things:</p>
<ul>
<li><p>Save information for later tasks</p>
</li>
<li><p>Save information for the final result (when the goal is complete)</p>
</li>
<li><p>Add any tasks (if it thinks necessary)</p>
</li>
</ul>
<p>And this <strong>refinement</strong> step is what makes the process so effective. When given a chance to ponder the results of an action <strong>1337GPT</strong> has taken, it can better select it's next steps.</p>
<h1 id="heading-cool-features">Cool features</h1>
<ul>
<li><p>Modify tasks before running, or skip them entirely</p>
</li>
<li><p>Persistent sessions - if something goes wrong, you can pick up from where you left <em>(also means you can edit data, as it is stored in JSON format)</em></p>
</li>
<li><p>Some <strong>custom tools</strong> I wrote to help <strong>1337GPT</strong> pentest effectively, listed below</p>
</li>
</ul>
<h3 id="heading-provided-tools">Provided tools</h3>
<p>These can be found in <code>tools/</code>, and are written using the <a target="_blank" href="https://python.langchain.com/en/latest/modules/agents/tools/custom_tools.html">langchain</a> library. But you can also write your own custom tools for the job!</p>
<ul>
<li><p>Execute shell commands (<strong>asks for user confirmation before executing for safety</strong>)</p>
</li>
<li><p>Search Google (uses <code>googlesearch.py</code> to avoid needing an API key)</p>
</li>
<li><p>Scrape text from websites (<strong>requires API key from</strong> <code>extractor</code>)</p>
</li>
<li><p>Search <a target="_blank" href="https://shodan.io">Shodan.io</a> using their API (<strong>requires API key)</strong></p>
</li>
<li><p>Read/write local files</p>
</li>
<li><p>Ask the user for input</p>
</li>
</ul>
<h1 id="heading-usage">Usage</h1>
<p><a target="_blank" href="https://github.com/AgeOfMarcus/1337GPT#usage">See the Usage section in my <code>README.md</code>.</a></p>
]]></content:encoded></item><item><title><![CDATA[Adding a list of my Hashnode posts to my personal website.]]></title><description><![CDATA[About meHey reader! My name is Marcus Weinberger, and I'm just getting started with Hashnode. While I'm mainly a Python developer, I work with HTML and such for my frontends. Click here to see more about me!

So, my personal website is a bit lame. Ac...]]></description><link>https://blog.marcusj.org/adding-a-list-of-my-hashnode-posts-to-my-personal-website</link><guid isPermaLink="true">https://blog.marcusj.org/adding-a-list-of-my-hashnode-posts-to-my-personal-website</guid><category><![CDATA[Hashnode]]></category><category><![CDATA[Python]]></category><category><![CDATA[GraphQL]]></category><dc:creator><![CDATA[Marcus Weinberger]]></dc:creator><pubDate>Tue, 18 Apr 2023 03:28:48 GMT</pubDate><content:encoded><![CDATA[<details><summary>About me</summary><div data-type="detailsContent">Hey reader! My name is Marcus Weinberger, and I'm just getting started with Hashnode. While I'm mainly a Python developer, I work with HTML and such for my frontends. <a target="_blank" href="https://marcus.hashnode.dev/about">Click here to see more about me!</a></div></details>

<p>So, <a target="_blank" href="https://marcusj.tech">my personal website</a> is a bit lame. Actually, I would say it's very lame. I don't really <strong>do</strong> front-end.</p>
<p><a target="_blank" href="https://web.archive.org/web/20220130211140/https://marcusj.tech/"><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1681787419136/b950c512-8efe-49c3-9e7c-7d567792a6c7.png" alt="An old snapshot of my personal website - marcusj.tech" class="image--center mx-auto" /></a></p>
<h2 id="heading-why-the-update">Why the update?</h2>
<p>After starting <a target="_blank" href="https://blog.marcusweinberger.repl.co/"><em>yet</em></a> <a target="_blank" href="https://replit.com/@MarcusWeinberger/blogold"><strong>another</strong></a> <a target="_blank" href="https://notes.marcusj.tech/link/blog"><s>additional</s></a> blog, I thought it would be a nice way to drive traffic to it if I added a feed of my posts to my personal website. On a sidenote, I don't know why I insisted on writing all my previous blogs from scratch. Moving to Hashnode is looking like a good idea.</p>
<p>Well, the <a target="_blank" href="https://marcusj.tech/__repl">back-end of my website</a> is built with Python, so let's use that to query the Hashnode API. I followed a tutorial which I will link below, and customized it a little, before making a little wrapper for it.</p>
<p><div class="embedded-content"><a class="embed-card" data-card-width="600px" data-card-key="2e4d628b39a64b99917c73956a16b477" data-card-controls="0" data-card-theme="light" href="https://catalins.tech/hashnode-api-how-to-display-your-blog-articles-on-your-portfolio-page"></a></div>]</p>
<h3 id="heading-python-code">Python code</h3>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> requests

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Hashnode</span>(<span class="hljs-params">object</span>):</span>
    _api_url = <span class="hljs-string">'https://api.hashnode.com'</span>

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__init__</span>(<span class="hljs-params">self, api_key: str</span>):</span>
        self.headers = {
            <span class="hljs-string">'Authorization'</span>: api_key
        }

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">get_user_posts</span>(<span class="hljs-params">self, username: str, page: int = <span class="hljs-number">0</span></span>) -&gt; (str, list):</span>
        <span class="hljs-string">"""
        Accepts a str username as input.
        Returns the blog domain (str), and a list of posts (dicts)
        """</span>
        query = <span class="hljs-string">'''{
  user(username: "&lt;username&gt;") {
    publication {
      posts(page: &lt;page&gt;) {
        title
        brief
        slug
        coverImage
      }
    }
    publicationDomain
  }
}'''</span>.replace(<span class="hljs-string">'&lt;username&gt;'</span>, username).replace(<span class="hljs-string">'&lt;page&gt;'</span>, str(page))
        req = requests.post(self._api_url, json={<span class="hljs-string">'query'</span>: query}, headers=self.headers)
        res = req.json()[<span class="hljs-string">'data'</span>][<span class="hljs-string">'user'</span>]
        <span class="hljs-keyword">return</span> res[<span class="hljs-string">'publicationDomain'</span>], res[<span class="hljs-string">'publication'</span>][<span class="hljs-string">'posts'</span>]
</code></pre>
<p>Using this, I added the following snippet to my Flask template</p>
<pre><code class="lang-xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">id</span>=<span class="hljs-string">'posts'</span>&gt;</span>
    <span class="hljs-comment">&lt;!-- Loop through posts --&gt;</span>
    {% for post in posts %}
    <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">'post'</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">h3</span>&gt;</span>{{ post.title }}<span class="hljs-tag">&lt;/<span class="hljs-name">h3</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>{{ post.brief }}<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
        {% if post.coverImage %}
        <span class="hljs-tag">&lt;<span class="hljs-name">img</span> <span class="hljs-attr">src</span>=<span class="hljs-string">'{{ post.coverImage }}'</span> <span class="hljs-attr">alt</span>=<span class="hljs-string">'Cover Image'</span>&gt;</span>
        {% endif %}
        <span class="hljs-tag">&lt;<span class="hljs-name">a</span> <span class="hljs-attr">href</span>=<span class="hljs-string">'{{ blogDomain }}/{{ post.slug }}'</span>&gt;</span>Read more<span class="hljs-tag">&lt;/<span class="hljs-name">a</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
    {% endfor %}
<span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
</code></pre>
<p>With some CSS (generated by ChatGPT to fit the rest of my website), it fit right in.</p>
<h2 id="heading-the-results">The results</h2>
<p><a target="_blank" href="https://marcusj.tech">Check out the live site here!</a></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1681788327113/e2be3e08-c495-44dc-a5d1-26ce4ea73f50.png" alt="Updated result" class="image--center mx-auto" /></p>
]]></content:encoded></item><item><title><![CDATA[Task Management system for langchain]]></title><description><![CDATA[View project on Replit

How does it work?
The TaskManager gets created, with a goal, a list of tools, and its own LLM. It includes a bunch of prompts which is used internally. When initialized, it creates an initial list of tasks to get started (this...]]></description><link>https://blog.marcusj.org/task-management-system-for-langchain</link><guid isPermaLink="true">https://blog.marcusj.org/task-management-system-for-langchain</guid><category><![CDATA[langchain]]></category><category><![CDATA[Python]]></category><category><![CDATA[automation]]></category><dc:creator><![CDATA[Marcus Weinberger]]></dc:creator><pubDate>Sun, 16 Apr 2023 23:57:42 GMT</pubDate><content:encoded><![CDATA[<p><a target="_blank" href="https://replit.com/@MarcusWeinberger/ai-task-manager">View project on Replit</a></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1681689146990/2d670ae8-6ebe-446c-9baa-e24097c290d4.png" alt class="image--center mx-auto" /></p>
<h1 id="heading-how-does-it-work">How does it work?</h1>
<p>The <code>TaskManager</code> gets created, with a <strong>goal</strong>, a list of <strong>tools</strong>, and its own <strong>LLM</strong>. It includes a bunch of prompts which is used internally. When initialized, it creates an initial list of tasks to get started (this behaviour is different when initializing with certain arguments, but I'll get into that later).</p>
<p>As a developer, it's up to you how you process those tasks, but you can see what I would do at the end of <a target="_blank" href="http://main.py"><code>main.py</code></a>. But, as a task is completed, you give the TaskManager the name of the task and the result. The <code>TaskManager</code> will then use another prompt to reflect on the results of this task. This is called the <strong>refinement</strong> process. If additional tasks are needed, they will be added here. Info that may be needed for future tasks is saved to <code>stored_info</code>, and stuff for the end result <em>should</em> go to <code>final_result</code>.</p>
<p>When the final goal has been met, the <code>TaskManager</code> will run the <code>complete_func</code> (which you can define in the initialization) which will - by default - save all variables to a file.</p>
<h2 id="heading-cool-features">Cool features</h2>
<ul>
<li><p>Internal LLM to fix JSON output</p>
</li>
<li><p>Can persist state</p>
</li>
<li><p>Uses <code>text-davinci-003</code> or something by default (cheap)</p>
</li>
</ul>
]]></content:encoded></item></channel></rss>