<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>ainoya.dev</title><description>Naoki Ainoya&apos;s personal website</description><link>https://ainoya.dev/</link><item><title>The Stochastic Bricoleur: What Lévi-Strauss Can Teach Us About Building Software with LLMs</title><link>https://ainoya.dev/posts/the-stochastic-bricoleur-what-l-vi-strauss-can-teach-us-about-building-software-with-llms/</link><guid isPermaLink="true">https://ainoya.dev/posts/the-stochastic-bricoleur-what-l-vi-strauss-can-teach-us-about-building-software-with-llms/</guid><description>Exploring the parallels between Claude Lévi-Strauss&apos;s concept of &apos;bricolage&apos; and how Large Language Models generate code, and what it means for modern software architecture.</description><pubDate>Sat, 28 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;The other day I watched an LLM agent build a working API server. I gave it a short description of what I needed — a handful of endpoints, a database schema, authentication. Within minutes, the code was running. Tests passed. The thing worked.&lt;/p&gt;
&lt;p&gt;And yet I couldn&apos;t shake a particular feeling. The implementation was competent but slightly alien. It chose a middleware pattern I wouldn&apos;t have reached for. It structured the error handling in a way that was functional but not quite &lt;em&gt;mine&lt;/em&gt;. When I traced the logic, I couldn&apos;t find a single clear reason why it had assembled things in that specific order. It wasn&apos;t wrong. It was just… found, rather than designed.&lt;/p&gt;
&lt;p&gt;I&apos;ve been trying to name that feeling for a while. Recently, I think I found the word. It comes not from computer science but from a French anthropologist who died in 2009 at the age of 100. The word is &lt;em&gt;bricolage&lt;/em&gt;.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;In 1962, Claude Lévi-Strauss published &lt;em&gt;La Pensée sauvage&lt;/em&gt; — translated into English as &lt;em&gt;The Savage Mind&lt;/em&gt;. In it, he drew a distinction that has haunted intellectual life ever since: the distinction between the &lt;em&gt;engineer&lt;/em&gt; and the &lt;em&gt;bricoleur&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;The engineer, in Lévi-Strauss&apos;s telling, subordinates each task to the availability of raw materials and tools conceived and procured specifically for the project at hand. If the right materials do not exist, the engineer creates or acquires them. The process moves from goals to means, and the means are purpose-built.&lt;/p&gt;
&lt;p&gt;The bricoleur is different. The bricoleur works with &quot;whatever is at hand&quot; — a finite, heterogeneous collection of tools and materials left over from previous projects. These elements were not designed for the current task. They carry traces of their prior uses. The bricoleur&apos;s universe of instruments is closed: the rules of the game are always to make do with what is already there. The bricoleur&apos;s skill lies in recombining these elements into something that works, even though the result cannot be fully derived from any single intention. Lévi-Strauss compared the process to a kaleidoscope: a closed set of glass pieces that, when shaken, produces endlessly different patterns — but never a pattern that wasn&apos;t latent in the set from the beginning.&lt;/p&gt;
&lt;p&gt;Lévi-Strauss introduced this concept to describe how mythological thought operates. Myths, he argued, do not invent new concepts from scratch. They take existing cultural elements — animals, kinship roles, natural phenomena — and rearrange them to address fundamental contradictions. The result is a kind of intellectual bricolage: functional, often brilliant, but operating within a universe of pre-existing fragments rather than creating from nothing.&lt;/p&gt;
&lt;p&gt;I want to suggest that this is among the most apt descriptions available for what happens when an LLM generates code.&lt;/p&gt;
&lt;p&gt;The training corpus is the raw material from which the closed set is distilled. The learned weights are the pre-constrained elements — statistical traces of patterns encountered during training, compressed into parameters. The output is not a recombination of the corpus itself, but a reconstruction from these traces: shaped by what was in the training data, but not a direct reassembly of it. When an LLM writes a function, it is not engineering in Lévi-Strauss&apos;s sense — it is not subordinating the task to purpose-built means. It is reaching into a vast but finite landscape of learned patterns, selecting fragments that statistically fit the current context, and assembling them into something that works. The fragments carry echoes of their origins: coding conventions absorbed from thousands of repositories, idioms from languages the model was trained on, architectural patterns internalized without attribution. The logic of the output is real, but it belongs to the statistical landscape, not to a designer.&lt;/p&gt;
&lt;p&gt;A necessary caveat before we go further. This essay borrows Lévi-Strauss&apos;s &lt;em&gt;operational&lt;/em&gt; concept — bricolage as a description of how things get made — but does not adopt his structuralist metaphysics wholesale. His broader claims — that all human cultures share universal deep structures, that synchronic analysis has priority over historical explanation — have been rightly challenged by post-structuralist, postcolonial, and Marxist anthropologists. We do not need to believe in invariant mental structures to observe that LLMs recombine pre-existing fragments within a closed set. The analogy works at the level of process, not ontology. We are borrowing Lévi-Strauss&apos;s lens, not his worldview.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;Once you see LLM-assisted development as bricolage, the structural parallels multiply. An important distinction is necessary here: these parallels apply most directly to the &lt;em&gt;bare model&lt;/em&gt; — the weights and architecture before any external augmentation. A bare LLM, without tools or retrieval, operates within a closed set in a way that closely mirrors Lévi-Strauss&apos;s bricoleur. Once you add RAG, tool use, or execution feedback, the system begins to break out of that closure — but the base layer remains bricolage, and understanding it as such is what makes the augmentation strategies legible. More on this later.&lt;/p&gt;
&lt;p&gt;Consider what Lévi-Strauss said about the bricoleur&apos;s repertoire: it is finite, it is heterogeneous, and its contents bear no necessary relation to the current project. This is a fitting description of a training corpus. The data that an LLM learned from was not curated for your particular problem. It is the accumulated residue of millions of prior constructions — Stack Overflow answers, GitHub repositories, blog posts, documentation — gathered without regard for coherence or purpose. The model&apos;s &quot;knowledge&quot; is not knowledge in any intentional sense; it is a stock of statistical traces whose presence is contingent on what happened to be available when the corpus was assembled.&lt;/p&gt;
&lt;p&gt;The elements carry traces of their prior uses. When an LLM generates a React component, the patterns it produces are not freshly derived from the semantics of your application. They are echoes of patterns that appeared in thousands of other applications, for thousands of other purposes. The model cannot fully separate a pattern from the context in which it was learned. This is why LLM-generated code sometimes feels subtly &lt;em&gt;off&lt;/em&gt; — not wrong, but inflected by intentions that are not yours. The traces of prior use are baked in.&lt;/p&gt;
&lt;p&gt;There is no single authoritative origin. The bricoleur&apos;s output, Lévi-Strauss observed, cannot be traced to a single source. It is a blend, an assemblage. LLM outputs share this quality. A generated function is a probabilistic composite shaped by thousands of examples, and it is rarely reducible to a single traceable origin. Of course, the human who wrote the prompt, selected the output, and edited the result has a real claim to authorship — this is not a &quot;death of the author&quot; argument. But the raw material the model works with resists attribution to any single source, and its assembly follows statistical logic rather than individual intention. This is why questions of attribution in AI-generated code are so vexed — not because the human contributed nothing, but because the output is difficult to reduce to a single author or origin.&lt;/p&gt;
&lt;p&gt;And the human using the LLM? Also a bricoleur. When you steer an agent — adjusting prompts, splicing outputs, routing through tool integrations, working around limitations — you are not engineering in any classical sense. You are tinkering. You are rearranging the outputs of a system you do not fully control, combining them with your own knowledge and constraints, producing something that works through iteration rather than derivation. The entire workflow, from model to user, is bricolage all the way down.&lt;/p&gt;
&lt;p&gt;Jacques Derrida, critiquing Lévi-Strauss in 1966, made an observation that now reads like prophecy. In his reading, the engineer is a myth produced by the bricoleur — a fiction of pure, self-originating design that no one actually inhabits. There is no pure engineer, Derrida argued. Everyone borrows concepts from an inherited repertoire. The notion that anyone constructs a system entirely from first principles is itself a fiction — a comforting story that bricoleurs tell about an idealized figure who does not exist. In the LLM era, Derrida&apos;s point has become impossible to ignore. The myth of the engineer-programmer — the developer who writes every line from pure logic, who designs before implementing, who fully understands the system they create — was never quite the whole story, even before LLMs. Anyone who has shipped production software knows that improvisation, copy-paste, and pragmatic compromise have always been part of the process. LLMs have simply made the bricolage impossible to ignore.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;The idea of connecting programming to bricolage is not new. And broader discussions linking LLMs to recombination or remix culture certainly exist. What I have not found, however, is a sustained argument that treats LLM-agent software development specifically through the lens of Lévi-Strauss&apos;s bricolage — with its particular emphasis on the closed set, the trace of prior use, and the kaleidoscope problem. That is the connection this essay attempts to make. The intellectual genealogy is worth tracing, both to acknowledge prior work and to locate what remains underexplored.&lt;/p&gt;
&lt;p&gt;In 1990, Sherry Turkle and Seymour Papert published &quot;Epistemological Pluralism and the Revaluation of the Concrete,&quot; in which they applied Lévi-Strauss&apos;s concept directly to programming. They identified a &quot;bricoleur style&quot; of coding: bottom-up rather than top-down, iterative rather than planned, proceeding by sculpting and rearranging rather than by specification. In their account, the bricoleur programmer works more like a painter than an architect — stepping back after each change to observe the effect before deciding what to do next. This was prescient work, but it was about &lt;em&gt;human&lt;/em&gt; programming styles — about how certain people prefer to code. It did not address the question of what happens when the code is generated by a machine whose entire mode of operation is bricolage.&lt;/p&gt;
&lt;p&gt;In 2021, Emily Bender and colleagues published &quot;On the Dangers of Stochastic Parrots,&quot; in which they argued that LLMs stitch together linguistic forms from training data based on probabilistic patterns, without engaging with meaning in the way humans do. Read this description carefully and compare it to Lévi-Strauss&apos;s account of bricolage — the recombination of pre-existing elements according to structural rules, without transparent reference to underlying intentions. The two descriptions are structurally similar. But Bender&apos;s paper never mentions Lévi-Strauss, and the &quot;stochastic parrot&quot; metaphor frames the phenomenon primarily as a deficiency — a failure to achieve true understanding. The bricolage frame offers something different: not a diagnosis of failure, but a recognition of a distinct mode of creation. Whether LLMs possess some form of internal representation or semantic structure is an active area of research; what matters for our purposes is that their &lt;em&gt;outputs&lt;/em&gt; are assembled from pre-existing patterns in a way that Lévi-Strauss would have immediately recognized.&lt;/p&gt;
&lt;p&gt;Earlier still, the biologist François Jacob applied Lévi-Strauss&apos;s concept to evolution itself. In his 1977 paper &quot;Evolution and Tinkering,&quot; Jacob argued that natural selection does not design organisms from scratch. It repurposes existing structures for new functions. Feathers evolved for thermal insulation and were later co-opted for flight. The vertebrate jaw was assembled from bones that once served as gill supports in fish. Evolution is not engineering; it is tinkering — or, in Lévi-Strauss&apos;s term, bricolage. The analogy to LLMs is direct: the model repurposes code patterns that were &quot;evolved&quot; for entirely different purposes, applying them to problems their original authors never imagined.&lt;/p&gt;
&lt;p&gt;In the research I have been able to survey, I have not found a sustained reading of LLM-agent development through the specific apparatus of Lévi-Strauss&apos;s bricolage — the closed set, the kaleidoscope, the transformation theory of &lt;em&gt;Mythologiques&lt;/em&gt;. The pieces are all in the literature; they have simply not been assembled. This essay is an attempt at that assembly — which is, of course, itself an act of bricolage.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;Lévi-Strauss did not stop at bricolage. In his four-volume masterwork &lt;em&gt;Mythologiques&lt;/em&gt; (1964–1971), he tracked how myths transform as they travel across cultures — from the southern tip of South America northward through the continent. A myth about the origin of fire becomes, in a neighboring culture, a myth about the origin of cooking. The narrative elements shift, but the underlying structure of oppositions — raw and cooked, nature and culture — persists through transformation. The elements are finite. The transformations are systematic. The variants are related not by descent from a common ancestor but by structural correspondence.&lt;/p&gt;
&lt;p&gt;This is what LLMs do with code. Given the same prompt, different models — or the same model at different temperatures — produce &lt;em&gt;variants&lt;/em&gt;. These variants are structurally related but locally adapted. They are not copies and they are not independent inventions. They are transformations of a shared repertoire, shaped by the statistical landscape of the training data. The corpus is the myth cycle. Each generation is a transformation, not a creation.&lt;/p&gt;
&lt;p&gt;But Lévi-Strauss&apos;s critics identified a fundamental problem with the kaleidoscope metaphor: structural closure. The kaleidoscope can produce endlessly different patterns, but it can never introduce a genuinely new element. The glass pieces are fixed. The system is closed. For innovation that transcends recombination, you need something from outside the set. This criticism maps onto a &lt;em&gt;baseline&lt;/em&gt; limitation of LLMs: a bare model, without tools, cannot reason beyond its training distribution. It can recombine brilliantly, but it cannot invent what was never in the corpus. In practice, of course, modern LLM-based systems are rarely bare — RAG, tool use, execution feedback, and external memory all breach the closure. The kaleidoscope metaphor is most useful not as a final description of what LLM systems &lt;em&gt;are&lt;/em&gt;, but as a description of the default they must be actively designed to escape.&lt;/p&gt;
&lt;p&gt;Here a fair objection must be addressed. If bricolage is defined broadly enough, any generative process qualifies, and the concept risks being unfalsifiable. This essay does not claim to have discovered a scientific law. The value of the bricolage frame is not predictive but prescriptive — it is a design heuristic. If you accept that an LLM operates within a closed set, specific architectural consequences follow. These consequences are concrete, testable, and immediately useful. The framework earns its keep not by being provably true but by generating better design decisions than the alternative framing of LLMs as &quot;imperfect engineers.&quot; The proof is in the architecture you build.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;So how do you escape the kaleidoscope? Three strategies, each rooted in a different theoretical response to structuralism&apos;s limits.&lt;/p&gt;
&lt;p&gt;The first is to introduce external elements. The anthropologist Tim Ingold criticized Lévi-Strauss for treating the elements of bricolage as stable and fixed. In reality, Ingold argued, creative materials are always in flux — they emerge, transform, and decay. In the LLM context, this translates to connecting the model to live data: retrieval-augmented generation, web search, tool use, API integrations. When you wire an LLM to an MCP server that queries a live database, you are injecting elements that were not in the training corpus. The glass pieces of the kaleidoscope are no longer fixed. The set is no longer fully closed. This is not a minor architectural detail — it is the primary mechanism by which LLM-based systems transcend their training distribution.&lt;/p&gt;
&lt;p&gt;The second strategy is to create feedback loops. Jacob&apos;s evolutionary tinkering offers the model here. In biological evolution, the raw material is pre-existing structures, but those structures are transformed through contact with the environment. Selection pressure reshapes the repertoire over time. In LLM agent workflows, this corresponds to the generate-test-error-regenerate cycle. The agent writes code, the runtime environment tests it, errors feed back as new input, and the agent revises. Each iteration is not merely a recombination of the original corpus — it is a recombination &lt;em&gt;informed by real-world feedback&lt;/em&gt; that was not part of the training data. Emergent properties arise that no single training example contained. This is how evolution produces novelty from tinkering, and it is how agent loops produce solutions that exceed mere pattern matching.&lt;/p&gt;
&lt;p&gt;The third is to collide multiple closed systems. Derrida, in his critique of Lévi-Strauss, proposed that meaning arises not from structure alone but from &lt;em&gt;play&lt;/em&gt; (&lt;em&gt;jeu&lt;/em&gt;) — the endless displacement and substitution of elements within and between systems. In practice, this means multi-model orchestration: routing different parts of a task to different models, each trained on different data with different biases and blind spots. The interface between two closed systems produces patterns that neither system contains alone. This is not a theoretical nicety — it is a practical technique that developers are already using, often without realizing it has a name.&lt;/p&gt;
&lt;p&gt;The deeper lesson is that the architecture around the LLM matters more than the LLM itself. The model is a kaleidoscope. What you build around it — the data pipelines, the feedback loops, the tool integrations, the orchestration layer — determines whether the output stays within the closed set or transcends it. Bricolage is not a deficiency to overcome. It is a mode of creation to design for.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;Now let us return to where we began — with names. What follows is interpretation, not corporate history. I have no evidence that anyone at Anthropic was thinking about Lévi-Strauss when they named their products. But the resonances are there, and they are worth reading — not as proof of intent, but as a symptom of convergence.&lt;/p&gt;
&lt;p&gt;Anthropic&apos;s AI is called Claude. The origin of the name has never been officially explained in detail; it is often read as a nod to Claude Shannon, the founder of information theory. Whether or not that reading is correct, it is a productive one. Shannon&apos;s central insight was that information is a statistical property of signals, independent of their semantic content. Meaning, for Shannon, is not what a message says but how it is structured relative to alternatives. LLMs operate on a fundamentally similar substrate — statistical structure over tokens — whatever internal representations they may or may not develop along the way.&lt;/p&gt;
&lt;p&gt;But the company is called &lt;em&gt;Anthropic&lt;/em&gt;. The word shares its Greek root — &lt;em&gt;anthropos&lt;/em&gt;, human — with &lt;em&gt;anthropologie&lt;/em&gt;, the discipline that Claude Lévi-Strauss transformed. The anthropic principle in cosmology holds that the universe appears designed because we observe it from within: the observer is constitutive of the observation. Applied to LLMs, this principle illuminates why their outputs seem intelligent — we, the observers, project meaning onto statistically structured text. The appearance of understanding is, at least in part, a property of the reader, not the writer.&lt;/p&gt;
&lt;p&gt;And then there is Constitutional AI, the training technique that defines Anthropic&apos;s approach to alignment. The model is trained to adhere to a set of principles — a constitution — that shapes its behavior from the inside. Not external rules imposed by a censor, but internal structural constraints that generate appropriate behavior across novel situations. Lévi-Strauss spent his entire career demonstrating a strikingly similar mechanism: how structural constraints — the incest taboo, the rules of myth transformation, the grammar of kinship — generate meaning and order without a designer, without a central authority, without anyone deciding what the system should produce. One can &lt;em&gt;read&lt;/em&gt; Constitutional AI as structural anthropology applied to neural networks. Whether its creators would accept that reading is another matter.&lt;/p&gt;
&lt;p&gt;But the naming goes deeper still. Consider the model tiers: Opus, Sonnet, Haiku. I am not claiming that Anthropic chose these names with Lévi-Strauss in mind. What I am claiming is that the names, once chosen, are legible through his framework in a way that illuminates something real about what the models do.&lt;/p&gt;
&lt;p&gt;Lévi-Strauss explicitly structured &lt;em&gt;Mythologiques&lt;/em&gt; as a musical composition. The first volume opens with an &quot;Ouverture&quot; and proceeds through &quot;Theme and Variations,&quot; &quot;Fugue,&quot; and &quot;Cantata.&quot; He argued that myth and music are isomorphic — both generate meaning through the simultaneous reading of a horizontal axis (melody, narrative) and a vertical axis (harmony, structural correspondences). &lt;em&gt;Opus&lt;/em&gt; — a musical work number — can be read in this lineage: the largest model as the most elaborated composition.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Sonnet&lt;/em&gt;. Fourteen lines. Fixed rhyme scheme. A volta at the turn. The sonnet is a form defined entirely by structural constraint. It does not produce meaning &lt;em&gt;despite&lt;/em&gt; its rules but &lt;em&gt;because of&lt;/em&gt; them. This is bricolage in its purest form: a closed formal system that generates infinite expression through the recombination of finite elements. It is also Constitutional AI in miniature — behavior shaped by internal rules, not external policing.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Haiku&lt;/em&gt;. Five-seven-five. Seventeen syllables. The most extreme formal compression in world poetry: maximum meaning from minimum structure. And it is a non-Western form. One can read its presence alongside the European sonnet and the Western classical opus as resonating — whether intentionally or not — with the universalism that Lévi-Strauss spent his career defending: the claim that structural creativity is not the monopoly of any single tradition.&lt;/p&gt;
&lt;p&gt;The scalar hierarchy — Opus (large), Sonnet (medium), Haiku (small) — is suggestive. In structural anthropology, the same transformational rules operate at every scale. A myth cycle spanning an entire continent and a single village folktale are governed by the same structural logic. Whether or not Anthropic intends the analogy, the architecture does embody it: the same constitutional principles shape behavior across model sizes.&lt;/p&gt;
&lt;p&gt;All three forms share a single property: finite constraint is the precondition for creative generation. An opus without form is noise. A sonnet without rules is free verse. A haiku without compression is just a sentence. Constraint is not the enemy of creation — it is its engine.&lt;/p&gt;
&lt;p&gt;Whether Anthropic&apos;s naming was deliberate or intuitive or simply coincidental, the reading holds. Every layer of the product taxonomy — company, model, tiers — &lt;em&gt;can be read as&lt;/em&gt; encoding a thesis that Lévi-Strauss, Shannon, and Jacob each arrived at from different directions: &lt;em&gt;structure without intention can produce something that functions as if it were designed&lt;/em&gt;. Evolution says yes. Myth says yes. Information theory says yes. Anthropic is betting its company on the same answer — with the addendum that the right constraints make all the difference.&lt;/p&gt;
&lt;p&gt;I am not arguing that Anthropic is secretly a structuralist enterprise. I am arguing that the problem they are working on — how to make a system behave well through internal constraints rather than external supervision — is the same problem Lévi-Strauss identified in myths, Jacob identified in evolution, and Shannon formalized in communication. The convergence is in the problem, not necessarily in the intent. Either way, it is hard to look at a company called Anthropic, making a model called Claude, trained by Constitutional AI, and tiered as Opus, Sonnet, and Haiku, and not hear echoes of &lt;em&gt;La Pensée sauvage&lt;/em&gt; — even if those echoes were never intended.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;There is a question at the bottom of all this, and it is not a theoretical one.&lt;/p&gt;
&lt;p&gt;The engineering mindset says: specify, design, implement, verify. The bricoleur mindset says: try, adapt, recombine, iterate. For most of the history of software, we pretended the first was what we did, even as the second was what actually happened. LLM agents have made the pretense untenable. Bricolage is now the dominant mode of software creation whether we acknowledge it or not.&lt;/p&gt;
&lt;p&gt;The question, then, is not &lt;em&gt;whether&lt;/em&gt; to be a bricoleur. You already are. The question is whether you will be a &lt;em&gt;reflective&lt;/em&gt; bricoleur — one who understands the structural closure of their tools, designs feedback loops that break it, builds architectures that multiply the interfaces between closed systems, and knows exactly when to reach outside the kaleidoscope for something the glass pieces cannot provide.&lt;/p&gt;
&lt;p&gt;One of Lévi-Strauss&apos;s key observations was that mythical thought totalizes — it takes whatever fragments are at hand and assembles them into a coherent whole. The LLM does the same thing. The difference — the only difference that matters — is what you build around it.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;References:&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Lévi-Strauss, C. (1962). &lt;em&gt;La Pensée sauvage&lt;/em&gt;. Paris: Plon. [English: &lt;em&gt;The Savage Mind&lt;/em&gt;, 1966] — &lt;a href=&quot;https://web.mit.edu/allanmc/www/levistrauss.pdf&quot;&gt;Chapter 1: &quot;The Science of the Concrete&quot; (MIT)&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Lévi-Strauss, C. (1964–1971). &lt;em&gt;Mythologiques&lt;/em&gt; I–IV. Paris: Plon. — &lt;a href=&quot;https://en.wikipedia.org/wiki/Mythologiques&quot;&gt;Wikipedia overview&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Derrida, J. (1967). &quot;Structure, Sign, and Play in the Discourse of the Human Sciences.&quot; In &lt;em&gt;Writing and Difference&lt;/em&gt;. — &lt;a href=&quot;http://hydra.humanities.uci.edu/derrida/sign-play.html&quot;&gt;Full text (UCI)&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Jacob, F. (1977). &quot;Evolution and Tinkering.&quot; &lt;em&gt;Science&lt;/em&gt;, 196(4295), 1161–1166. — &lt;a href=&quot;https://doi.org/10.1126/science.860134&quot;&gt;Science (DOI)&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Turkle, S. &amp;amp; Papert, S. (1990). &quot;Epistemological Pluralism and the Revaluation of the Concrete.&quot; &lt;em&gt;Signs&lt;/em&gt;, 16(1). — &lt;a href=&quot;https://www.papert.org/articles/EpistemologicalPluralism.html&quot;&gt;Full text (papert.org)&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Bender, E. M. et al. (2021). &quot;On the Dangers of Stochastic Parrots: Can Language Models Be Too Big?&quot; &lt;em&gt;Proceedings of FAccT &apos;21&lt;/em&gt;. — &lt;a href=&quot;https://doi.org/10.1145/3442188.3445922&quot;&gt;ACM (DOI)&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Baker, T. &amp;amp; Nelson, R. E. (2005). &quot;Creating Something from Nothing: Resource Construction through Entrepreneurial Bricolage.&quot; &lt;em&gt;Administrative Science Quarterly&lt;/em&gt;, 50(3). — &lt;a href=&quot;https://journals.sagepub.com/doi/10.2189/asqu.2005.50.3.329&quot;&gt;SAGE Journals (DOI)&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Anthropic. (2026). &quot;Claude&apos;s New Constitution.&quot; — &lt;a href=&quot;https://www.anthropic.com/news/claude-new-constitution&quot;&gt;anthropic.com&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>Building and Maintaining the Pokke Docs with Starlight and LLMs</title><link>https://ainoya.dev/posts/building-and-maintaining-the-pokke-docs-with-starlight-and-llms/</link><guid isPermaLink="true">https://ainoya.dev/posts/building-and-maintaining-the-pokke-docs-with-starlight-and-llms/</guid><description>How I use Starlight and scheduled AI tasks to generate and maintain the Pokke documentation.</description><pubDate>Thu, 26 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I built the documentation site for Pokke using Starlight, which is available at &lt;a href=&quot;https://docs.pokke.femto-cloud.com/&quot;&gt;docs.pokke.femto-cloud.com&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I designed it with the intention of generating the content directly from source code and design documents using LLMs. The site is built to support both Japanese and English.&lt;/p&gt;
&lt;p&gt;Since AI tools like Jules and Codex have built-in features for scheduling tasks, I run them periodically to maintain the site. They automatically fix documentation errors and write updates for newly added features. It is a very convenient setup that makes documentation maintenance much easier.&lt;/p&gt;
</content:encoded></item><item><title>A Simple but Powerful Bookmark Feature: Routing Prompts to Your Own AI Subscriptions</title><link>https://ainoya.dev/posts/a-simple-but-powerful-bookmark-feature-routing-prompts-to-your-own-ai-subscriptions/</link><guid isPermaLink="true">https://ainoya.dev/posts/a-simple-but-powerful-bookmark-feature-routing-prompts-to-your-own-ai-subscriptions/</guid><description>How a lightweight custom URL feature in my personal bookmarking service, Pokke, enables seamless AI research while saving API costs.</description><pubDate>Thu, 19 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I recently added a very simple, yet effective feature to &lt;a href=&quot;https://pokke.femto-cloud.com/&quot;&gt;pokke&lt;/a&gt;, a personal bookmarking service I am developing.&lt;/p&gt;
&lt;p&gt;It is a feature that allows users to attach custom URL links to their saved bookmarks. By using simple variables like &lt;code&gt;{{url}}&lt;/code&gt;, &lt;code&gt;{{title}}&lt;/code&gt;, and &lt;code&gt;{{description}}&lt;/code&gt;, the system dynamically generates a link tailored to that specific bookmark.&lt;/p&gt;
&lt;p&gt;The most practical use case for this is passing a prompt with the bookmark&apos;s context directly to an AI search engine like Perplexity. For example, you can configure a custom URL like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;https://www.perplexity.ai/?q=URL%3A%20{{url}}%0Atitle%3A%20{{title}}%0Adescription%3A%20{{description}}%0APlease%20explain%20the%20content%20of%20the%20specified%20URL%20in%20detail.%0AAlso%2C%20please%20suggest%20some%20interesting%20pages%20on%20related%20topics.
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;URL: {{url}}
title: {{title}}
description: {{description}}
Please explain the content of the specified URL in detail.
Also, please suggest some interesting pages on related topics.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With just one click from a saved bookmark, this instantly hands over the article&apos;s context to Perplexity, asking it to explain the content in detail and suggest related topics. It dramatically shortens the path from &quot;saving for later&quot; to actually &quot;learning from it.&quot;&lt;/p&gt;
&lt;p&gt;You might wonder why I didn&apos;t just integrate an LLM API directly into Pokke to summarize articles automatically in the background.&lt;/p&gt;
&lt;p&gt;The main advantage of using URL queries instead of backend APIs is cost efficiency through leveraging the user&apos;s existing SaaS subscriptions. By simply passing the query to a service the user is already paying for (like a Perplexity Pro account), I can offer advanced AI capabilities without bearing the heavy LLM API costs on the service side. It is a win-win for both the developer and the user.&lt;/p&gt;
&lt;p&gt;However, there is a clear trade-off. Because it relies on external URL routing rather than a backend agent, it cannot be fully automated. The AI won&apos;t summarize the article the moment you save it; it still requires the user&apos;s manual action (a click) to trigger the process.&lt;/p&gt;
&lt;p&gt;Despite the slight friction of needing a manual click, this &quot;Bring Your Own SaaS&quot; approach fits perfectly for small-scale or personal projects. It is a lightweight feature that significantly enhances how I interact with the information I collect.&lt;/p&gt;
</content:encoded></item><item><title>Building a Digital Toy for Children Without Writing a Single Line of Code</title><link>https://ainoya.dev/posts/building-a-digital-toy-for-children-without-writing-a-single-line-of-code/</link><guid isPermaLink="true">https://ainoya.dev/posts/building-a-digital-toy-for-children-without-writing-a-single-line-of-code/</guid><description>How I used AI to go from an idea to a deployed &apos;Emoji Shower&apos; game, including automated infrastructure setup via GitHub APIs.</description><pubDate>Sun, 15 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I recently created a simple digital toy called Emoji Shower. The concept is straightforward: tapping the screen or hitting keys showers the display with various emojis. It is a sensory, web-based experience designed for young children.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://ainoya.github.io/emoji-shower/&quot;&gt;Demo: Emoji Shower&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The most notable part of this project was the development process. I did not write a single line of code manually. By using AI (Codex), I translated a high-level concept directly into a functional web application.&lt;/p&gt;
&lt;p&gt;The automation extended beyond just the source code. I tasked the AI with handling the infrastructure by using the &lt;code&gt;gh api&lt;/code&gt; command to configure GitHub Pages and manage the deployment process autonomously.&lt;/p&gt;
&lt;p&gt;This experience highlights a shift in technical prototyping. As an engineer, the role transitioned from &quot;builder&quot; to &quot;director.&quot; When the friction of syntax and deployment configuration is removed, the distance between an idea and a tangible result becomes near zero. For a small project like this, this AI-driven approach is becoming the new standard for rapid implementation.&lt;/p&gt;
</content:encoded></item><item><title>The Flow of Invisible Transcription</title><link>https://ainoya.dev/posts/the-flow-of-invisible-transcription/</link><guid isPermaLink="true">https://ainoya.dev/posts/the-flow-of-invisible-transcription/</guid><description>Why hiding real-time transcription can actually make voice input faster and more natural.</description><pubDate>Wed, 04 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I’ve been trying out the voice input feature in Antigravity (specifically testing the UI in version 1.15.8), and it sparked a realization about how we interact with voice UIs.&lt;/p&gt;
&lt;p&gt;Most tools display text in real-time as you speak. While technically impressive, this creates a cognitive burden. As the words appear, your brain involuntarily switches from &quot;speaking&quot; mode to &quot;editing&quot; mode. You start spotting errors or rethinking phrasing mid-sentence, which breaks your train of thought.&lt;/p&gt;
&lt;p&gt;Antigravity behaves differently. It uses a simple recorder UI that hides the transcription process entirely. You speak, and the text generates only after you finish.&lt;/p&gt;
&lt;p&gt;This visual-free approach removes the distraction of immediate self-correction. Because modern AI models are now accurate enough to be trusted without constant supervision, we no longer need to monitor the output in real-time.&lt;/p&gt;
&lt;p&gt;I also suspect a technical upside: by decoupling the UI from the immediate transcription, the system might be free to prioritize capturing the broader context rather than striving for strict, word-for-word accuracy.&lt;/p&gt;
&lt;p&gt;By hiding the visual feedback, the tool allows you to focus entirely on articulating the thought itself, making the drafting process significantly smoother.&lt;/p&gt;
</content:encoded></item><item><title>Fixing &quot;Invalid OAuth error response&quot; When Connecting Cursor to a Custom MCP Server</title><link>https://ainoya.dev/posts/fixing-invalid-oauth-error-response-when-connecting-cursor-to-a-custom-mcp-server/</link><guid isPermaLink="true">https://ainoya.dev/posts/fixing-invalid-oauth-error-response-when-connecting-cursor-to-a-custom-mcp-server/</guid><description>Debugging a cryptic unhandledRejection error in Cursor by adhering to the Model Context Protocol&apos;s authorization spec.</description><pubDate>Mon, 12 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I recently encountered a cryptic error while building a custom &lt;strong&gt;Model Context Protocol (MCP)&lt;/strong&gt; server and attempting to connect it to the Cursor editor.&lt;/p&gt;
&lt;p&gt;Even though I intended to use simple API Key authentication, Cursor threw an &lt;code&gt;unhandledRejection&lt;/code&gt; immediately upon connection. The error message was confusing, seemingly pointing to a syntax issue in a JSON response that I hadn&apos;t explicitly sent.&lt;/p&gt;
&lt;p&gt;Here is what happened, how I debugged it, and why adhering strictly to the MCP authorization specification fixed the issue.&lt;/p&gt;
&lt;h2&gt;The Error&lt;/h2&gt;
&lt;p&gt;When attempting to add my local MCP server to Cursor, the connection failed with the following error output:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Error (unhandledRejection): HTTP 404: Invalid OAuth error response: SyntaxError: Unexpected non-whitespace character after JSON at position 4 (line 1 column 5). Raw body: 404 Not Found
ServerError: HTTP 404: Invalid OAuth error response: SyntaxError: Unexpected non-whitespace character after JSON at position 4 (line 1 column 5). Raw body: 404 Not Found

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;At first glance, the &lt;code&gt;SyntaxError&lt;/code&gt; suggests a malformed JSON response. The specifically mentioned &quot;position 4&quot; is a hint: if the raw body is &lt;code&gt;404 Not Found&lt;/code&gt;, the parser reads &lt;code&gt;404&lt;/code&gt; (a valid number in JSON) but chokes on the space and subsequent text that follows.&lt;/p&gt;
&lt;p&gt;This meant Cursor was receiving a plain text &lt;code&gt;404 Not Found&lt;/code&gt; response but was expecting a JSON object.&lt;/p&gt;
&lt;h2&gt;The Investigation&lt;/h2&gt;
&lt;p&gt;To understand where this 404 was coming from, I checked the access logs of my custom MCP server. I noticed that immediately upon connection, Cursor was making a &lt;code&gt;GET&lt;/code&gt; request to a specific path:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;GET /.well-known/oauth-authorization-server

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;My server returned a &lt;strong&gt;404 Not Found&lt;/strong&gt; status code because I hadn&apos;t implemented any OAuth endpoints. I was relying solely on API keys passed via headers or environment variables.&lt;/p&gt;
&lt;p&gt;However, Cursor&apos;s MCP client implementation appears to proactively check for OAuth support by probing this standardized endpoint. When it received a 404 text response, its internal logic—likely trying to parse an OAuth error configuration—crashed.&lt;/p&gt;
&lt;h2&gt;The Solution: Return 401, Not 404&lt;/h2&gt;
&lt;p&gt;I consulted the &lt;a href=&quot;https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization&quot;&gt;MCP Authorization Specification&lt;/a&gt; to see how authentication challenges should be handled.&lt;/p&gt;
&lt;p&gt;The specification states:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;MCP clients &lt;strong&gt;MUST&lt;/strong&gt; be able to parse &lt;code&gt;WWW-Authenticate&lt;/code&gt; headers and respond appropriately to &lt;code&gt;HTTP 401 Unauthorized&lt;/code&gt; responses from the MCP server.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Although returning 404 for a missing endpoint is standard HTTP, Cursor&apos;s client implementation failed to parse the text response. However, the MCP specification strictly mandates how clients must handle 401 responses. By returning 401, I forced Cursor to enter its spec-compliant error handling flow, effectively bypassing the crash.&lt;/p&gt;
&lt;h3&gt;The Fix&lt;/h3&gt;
&lt;p&gt;I modified my server&apos;s middleware. Instead of letting the request fall through to a generic 404 handler for the &lt;code&gt;.well-known&lt;/code&gt; path, I ensured it returned a &lt;strong&gt;401 Unauthorized&lt;/strong&gt; status code.&lt;/p&gt;
&lt;p&gt;Once I deployed this change:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Cursor probed &lt;code&gt;/.well-known/oauth-authorization-server&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;My server returned &lt;code&gt;401 Unauthorized&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Cursor correctly interpreted this as &quot;OAuth not available&quot; or &quot;Authorization required&quot; without crashing on JSON parsing.&lt;/li&gt;
&lt;li&gt;It successfully fell back to using the configured API Key method.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;If you see an &quot;Invalid OAuth error response&quot; in Cursor, check your access logs. You might be returning a 404 where a 401 is expected.&lt;/p&gt;
</content:encoded></item><item><title>Turning My Bookmark Service into a Personal AI Librarian with MCP</title><link>https://ainoya.dev/posts/turning-my-bookmark-service-into-a-personal-ai-librarian-with-mcp/</link><guid isPermaLink="true">https://ainoya.dev/posts/turning-my-bookmark-service-into-a-personal-ai-librarian-with-mcp/</guid><description>How I extended &apos;pokke&apos; with the Model Context Protocol to connect my reading list with my personal knowledge base.</description><pubDate>Mon, 12 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;In a previous post, I introduced &lt;a href=&quot;https://ainoya.dev/posts/pokke-a-small-bookmarking-service-for-my-own-use/&quot;&gt;pokke&lt;/a&gt;, a minimalist bookmarking service I built for my own use. The goal was simple: a place to toss interesting URLs without the friction of heavy, commercial alternatives.&lt;/p&gt;
&lt;p&gt;However, as with any &quot;read later&quot; service, I soon ran into a familiar problem. My collection of bookmarks was growing, but my consumption wasn&apos;t keeping up. It was becoming a graveyard of good intentions—a list of articles I &lt;em&gt;wanted&lt;/em&gt; to read but rarely revisited.&lt;/p&gt;
&lt;p&gt;I realized that simply storing links wasn&apos;t enough. I needed a way to make this information active. I didn&apos;t want to search my bookmarks; I wanted my bookmarks to find &lt;em&gt;me&lt;/em&gt;, and more importantly, I wanted them to connect with the things I was already thinking about.&lt;/p&gt;
&lt;h3&gt;Enter the Model Context Protocol&lt;/h3&gt;
&lt;p&gt;I&apos;ve been experimenting with the &lt;strong&gt;Model Context Protocol (MCP)&lt;/strong&gt;, an open standard that allows AI models to interact with external data and tools. It occurred to me that &lt;code&gt;pokke&lt;/code&gt; shouldn&apos;t just be a database; it should be an MCP server.&lt;/p&gt;
&lt;p&gt;The implementation was surprisingly straightforward. I exposed a &lt;code&gt;/mcp&lt;/code&gt; endpoint on the &lt;code&gt;pokke&lt;/code&gt; API. By doing this, I could plug my custom bookmark service directly into an AI environment (like Claude Desktop) just by adding a simple configuration:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;mcpServers&quot;: {
    &quot;pokke-api&quot;: {
      &quot;type&quot;: &quot;http&quot;,
      &quot;url&quot;: &quot;https://api.pokke.femto-cloud.com/mcp&quot;,
      &quot;headers&quot;: {
        &quot;Authorization&quot;: &quot;Bearer ${POKKE_API_KEY}&quot;
      }
    }
  }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This JSON snippet effectively turns my passive database into an active tool that an AI agent can query.&lt;/p&gt;
&lt;h3&gt;Connecting the Dots&lt;/h3&gt;
&lt;p&gt;The real power of this setup isn&apos;t just in summarizing articles—generic AI tools can do that. The goal was to build a bridge between &lt;strong&gt;new information&lt;/strong&gt; (my bookmarks) and &lt;strong&gt;existing context&lt;/strong&gt; (my local knowledge base/notes).&lt;/p&gt;
&lt;p&gt;I created a prompt to act as a &quot;librarian.&quot; I asked the AI to digest my recent bookmarks, categorize them, and—crucially—connect them to topics I have stored in my local file system (my &quot;second brain&quot;).&lt;/p&gt;
&lt;p&gt;Here is the prompt I used:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Use the pokke MCP to summarize the digest of recent pokke bookmark articles.
Divide them by category.
Please indicate what I am interested in and provide suggestions linking them to the knowledge currently in my file system.
Also, include advice on what keywords I should use to deepen my thinking in future intellectual exploration activities.
Write it out to a markdown file named with the date and time.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The result was fascinating. The AI didn&apos;t just tell me what the articles were about; it told me &lt;em&gt;why&lt;/em&gt; they mattered to me.&lt;/p&gt;
&lt;p&gt;For example, when it analyzed an article about &lt;code&gt;uv&lt;/code&gt; (the Python package manager), it didn&apos;t just stop at &quot;it&apos;s fast.&quot; It linked the article to my existing notes on Rust tooling:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://nesbitt.io/2025/12/26/how-uv-got-so-fast.html&quot;&gt;How uv got so fast&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Insight:&lt;/strong&gt; You have an interest in Rust through your study note &lt;code&gt;Implementing Transformer in Rust.md&lt;/code&gt;. The case of &lt;code&gt;uv&lt;/code&gt; suggests that performance comes not just from the language speed but from design and adherence to modern standards. This is a key takeaway for your own tool development.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;Similarly, it found a deep connection between a seemingly unrelated article about the &quot;Leftpad&quot; problem and my notes on testing strategies:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://lukeplant.me.uk/blog/posts/breaking-provably-correct-leftpad/&quot;&gt;Breaking “provably correct” Leftpad&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Insight:&lt;/strong&gt; This relates to the discussions in &lt;code&gt;Testing.md&lt;/code&gt; and &lt;code&gt;TestPyramid.md&lt;/code&gt;. The gap between being &quot;according to spec&quot; and &quot;according to user expectation&quot; cannot always be bridged by formal methods alone. Even &quot;provably correct&quot; code can fail in the face of messy realities like Unicode implementation details.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h3&gt;A Growing Trend&lt;/h3&gt;
&lt;p&gt;I’m not the only one exploring this direction. I recently watched a demonstration by Algolia, &lt;a href=&quot;https://www.youtube.com/watch?v=-4zIqnsQhkU&quot;&gt;Prototype an AI-Powered Bookmark Manager with n8n and Algolia MCP server&lt;/a&gt;, where they used MCP to build a searchable, intelligent bookmark manager. It validates the idea that we are moving away from static lists toward dynamic, AI-assisted knowledge management.&lt;/p&gt;
&lt;p&gt;However, my approach differs slightly in that I am less interested in &lt;em&gt;search&lt;/em&gt; and more interested in &lt;em&gt;synthesis&lt;/em&gt;. I want the AI to act as a catalyst, mixing fresh external inputs with my internal accumulated knowledge to spark new ideas.&lt;/p&gt;
&lt;h3&gt;Closing the Loop&lt;/h3&gt;
&lt;p&gt;The next step is automation. I plan to set up a workflow where this &quot;Digest &amp;amp; Insight&quot; report is generated automatically every week and sent to my Slack.&lt;/p&gt;
&lt;p&gt;Imagine receiving a weekly notification that says: &lt;em&gt;&quot;Here are the articles you saved, here is how they relate to the code you wrote last month, and here are the concepts you should explore next.&quot;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;By turning &lt;code&gt;pokke&lt;/code&gt; into an MCP server, it has evolved from a simple storage bin into an active partner in my intellectual life. It’s no longer just about saving URLs; it’s about ensuring that the information I consume actually feeds into my growth as an engineer.&lt;/p&gt;
</content:encoded></item><item><title>Pokke: A small bookmarking service for my own use</title><link>https://ainoya.dev/posts/pokke-a-small-bookmarking-service-for-my-own-use/</link><guid isPermaLink="true">https://ainoya.dev/posts/pokke-a-small-bookmarking-service-for-my-own-use/</guid><description>With Pocket gone, I built a lightweight cloud SaaS to manage my reading list. Here is why I chose to build rather than adopt complex alternatives.</description><pubDate>Sun, 11 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Like many developers, I relied on Pocket for years to save articles for later reading. When the service shut down in 2025, I was forced to look for a replacement.&lt;/p&gt;
&lt;p&gt;I looked around at the available options—from Obsidian Web Clipper to Notion—but nothing felt quite right. I didn&apos;t want a complex system; I just wanted a simple buffer for my thoughts. So, I decided to build my own solution: &lt;strong&gt;Pokke&lt;/strong&gt;.&lt;/p&gt;
&lt;h2&gt;Why Not Existing Tools?&lt;/h2&gt;
&lt;p&gt;Before building Pokke, I considered two popular alternatives, but neither fit my specific workflow.&lt;/p&gt;
&lt;h3&gt;Obsidian Web Clipper&lt;/h3&gt;
&lt;p&gt;I use Obsidian heavily, and while its web clipper is excellent, I found that clipping everything directly into my vault created too much noise.
My Obsidian vault is for refined, permanent knowledge. Sending every interesting link there made the graph messy and difficult to navigate. I needed a staging area—a place to hold information temporarily before I decided to process it deeply.&lt;/p&gt;
&lt;h3&gt;Notion&lt;/h3&gt;
&lt;p&gt;Notion is undeniably powerful and could easily handle bookmark management. However, I simply don&apos;t use Notion in my daily life.
Adopting a massive, all-in-one workspace just to save a few URLs felt like overkill. I wanted a tool that fit into my existing habits, not one that required me to change them.&lt;/p&gt;
&lt;h2&gt;Introducing Pokke&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://pokke.femto-cloud.com&quot;&gt;Pokke&lt;/a&gt;&lt;/strong&gt; (hosted at &lt;code&gt;pokke.femto-cloud.com&lt;/code&gt;) is a small-scale SaaS I developed to fill this gap.&lt;/p&gt;
&lt;p&gt;The name is a nod to its predecessor, but the philosophy is strictly personal. It is designed to be a quiet space for intellectual inquiry, free from the social features and algorithmic suggestions that have cluttered so many other services.&lt;/p&gt;
&lt;h3&gt;How It Works&lt;/h3&gt;
&lt;p&gt;I kept the architecture and workflow intentionally modest to ensure maintainability.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Capture:&lt;/strong&gt; I built a simple &lt;a href=&quot;https://github.com/ainoya/pokke-chrome-extension&quot;&gt;Chrome Extension&lt;/a&gt; that sends the current URL to my API.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Storage:&lt;/strong&gt; The backend is a lightweight cloud-deployed service that stores the links.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Learning:&lt;/strong&gt; I don&apos;t just let links rot in a list. I access my bookmarks via the API and often use Generative AI to summarize articles or extract key concepts. This helps me efficiently decide what to learn next.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It’s not a groundbreaking platform, but it’s a tool that fits my hand perfectly. Sometimes, that is all you need.&lt;/p&gt;
</content:encoded></item><item><title>The New &quot;Millionaire Programming&quot;: Throwing Generative AI at the Problem</title><link>https://ainoya.dev/posts/the-new-millionaire-programming-throwing-generative-ai-at-the-problem/</link><guid isPermaLink="true">https://ainoya.dev/posts/the-new-millionaire-programming-throwing-generative-ai-at-the-problem/</guid><description>In Japan, we call it &quot;Millionaire Programming.&quot; Traditionally, this meant throwing hardware at performance issues. In the AI era, it means throwing generative cycles at architectural uncertainty.</description><pubDate>Tue, 23 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Introduction: The &quot;Millionaire&quot; Philosophy&lt;/h2&gt;
&lt;p&gt;In the Japanese software engineering community, there is a concept known as &lt;strong&gt;&quot;Millionaire Programming&quot; (富豪的プログラミング).&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The term was originally coined by UI researcher Toshiyuki Masui in his &lt;a href=&quot;http://www.pitecan.com/fugo.html&quot;&gt;essay on utilizing computing resources&lt;/a&gt; (known for his work on predictive text input and user interfaces). In English, the closest equivalent nuance to his philosophy is the idiom: &lt;strong&gt;&quot;Throwing hardware at the problem.&quot;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Traditionally, this approach prioritized development speed over runtime efficiency. Instead of spending weeks optimizing an algorithm to save 100MB of RAM, a &quot;millionaire&quot; programmer would simply utilize the abundance of modern hardware. We accepted that &quot;hardware is cheap, but programmers are expensive,&quot; and we solved problems by spending machine resources lavishly.&lt;/p&gt;
&lt;p&gt;However, with the advent of the AI era, this concept is undergoing a fundamental transformation.&lt;/p&gt;
&lt;p&gt;We are moving &lt;strong&gt;from throwing hardware resources at performance bottlenecks to throwing code generation effort at architectural uncertainty.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;In an age where AI can instantly generate ten different implementation patterns, we have acquired a level of &quot;abundance&quot; that allows us to solve problems by spending generative cycles.&lt;/p&gt;
&lt;h2&gt;Old School: Throwing Hardware at the Problem&lt;/h2&gt;
&lt;p&gt;The traditional &quot;Millionaire&quot; approach was a strategy of substitution.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;graph LR
    A[&quot;Problem: Slow Performance&quot;] --&amp;gt; B[&quot;Option A: Optimize Code (Expensive Human Time)&quot;]
    A --&amp;gt; C[&quot;Option B: Upgrade Server (Cheap Resource)&quot;]
    C --&amp;gt; D[Problem Solved]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We utilized garbage collection, heavy frameworks, and verbose data structures. We didn&apos;t mind if the code was resource-hungry, as long as it was robust and easy to write. We used the &quot;wealth&quot; of Moore&apos;s Law to bypass the &quot;poverty&quot; of human time constraints.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Constraint:&lt;/strong&gt;
Even with this approach, we were limited by &lt;em&gt;human throughput&lt;/em&gt;. We could throw hardware at the code, but we still had to write the code ourselves.&lt;/p&gt;
&lt;h2&gt;New School: Throwing AI at the Problem&lt;/h2&gt;
&lt;p&gt;In the AI era, the definition of &quot;wealth&quot; has changed. We are no longer just resource-rich; we are &lt;strong&gt;generation-rich&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;We can now afford to be &quot;wasteful&quot; with code generation. We can &quot;throw&quot; tokens and inference costs at a problem to explore the solution space.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;graph TD
    A[Problem: Architectural Uncertainty] --&amp;gt; B[AI Parallel Generation]
    B --&amp;gt; C[Throw AI at Idea 1]
    B --&amp;gt; D[Throw AI at Idea 2]
    B --&amp;gt; E[Throw AI at Idea 3]
    C --&amp;gt; F[Comparison &amp;amp; Evaluation]
    D --&amp;gt; F
    E --&amp;gt; F
    F --&amp;gt; G[Selection &amp;amp; Refinement]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;The Shift in Workflow&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;1. Simultaneous Parallel Generation&lt;/strong&gt;
Previously, comparing three different architectural approaches (e.g., &lt;em&gt;Redux&lt;/em&gt; vs. &lt;em&gt;Context API&lt;/em&gt; vs. &lt;em&gt;Zustand&lt;/em&gt;) required significant reading and mental modeling.
Now, I can simply ask an LLM to implement the feature using all three patterns.&lt;/p&gt;
&lt;p&gt;I am &lt;strong&gt;throwing generative cycles&lt;/strong&gt; at my own uncertainty. I can look at the actual code for all three approaches side-by-side before writing a single line of my own. I spend the AI&apos;s &quot;effort&quot; to buy my own &quot;certainty.&quot;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. The &quot;Disposable Prototype&quot; Pattern&lt;/strong&gt;
In the past, &quot;Fail Fast&quot; was a slogan. Now, &quot;Fail Parallel&quot; is a reality.
I often ask AI to write a quick, dirty script just to validate an idea. If it fails, I discard it immediately. The cost of throwing away code has dropped to near zero because the cost of generating it is near zero.&lt;/p&gt;
&lt;h2&gt;Benefits of Generative Abundance&lt;/h2&gt;
&lt;h3&gt;1. Expanding the Search Space&lt;/h3&gt;
&lt;p&gt;When we write code manually, we tend to stick to what we know (Local Optima). If I know Python well, I&apos;ll solve every problem with Python.
By throwing AI at the problem, I can ask it to generate solutions in languages or paradigms I&apos;m less familiar with. The AI might suggest a Rust-based microservice for a bottleneck I was trying to optimize in Node.js. The &quot;search space&quot; for solutions expands dramatically.&lt;/p&gt;
&lt;h3&gt;2. Deepening Understanding via Comparison&lt;/h3&gt;
&lt;p&gt;The best way to learn a trade-off is to see it.
By generating Option A (High Performance, High Complexity) and Option B (Lower Performance, High Readability) and placing them next to each other, the abstract trade-off becomes concrete. This &quot;rich&quot; comparison process sharpens my architectural intuition.&lt;/p&gt;
&lt;h2&gt;The New Bottleneck: Choice Fatigue&lt;/h2&gt;
&lt;p&gt;However, this new wealth brings a new problem: &lt;strong&gt;Decision Fatigue&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;If traditional Millionaire Programming suffered from &quot;Software Bloat,&quot; AI-based Millionaire Programming suffers from &quot;Choice Overload.&quot; We are faced with a combinatorial explosion of valid options.&lt;/p&gt;
&lt;p&gt;For a single web application, you might instantly be presented with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Next.js deployed on Vercel&lt;/li&gt;
&lt;li&gt;Remix running on Cloudflare Workers&lt;/li&gt;
&lt;li&gt;Astro hosted on Netlify&lt;/li&gt;
&lt;li&gt;Plain React on AWS S3&lt;/li&gt;
&lt;li&gt;...and dozens of other viable combinations.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To navigate this, the role of the senior engineer shifts. We are no longer just &quot;builders&quot;; we are &lt;strong&gt;&quot;curators.&quot;&lt;/strong&gt; Our value lies not in how fast we type, but in how effectively we can filter the abundance of generated options to find the one that fits the business constraints.&lt;/p&gt;
&lt;h2&gt;Conclusion: From Craft to Curation&lt;/h2&gt;
&lt;p&gt;&quot;Millionaire Programming&quot; used to be about laziness—letting the hardware do the heavy lifting of memory management.&lt;/p&gt;
&lt;p&gt;Today, it is about &lt;strong&gt;strategy&lt;/strong&gt;. It is about leveraging the infinite patience and speed of AI to explore the map of possibilities before we commit to a path.&lt;/p&gt;
&lt;p&gt;We are &lt;strong&gt;throwing AI at the problem&lt;/strong&gt; to gain something far more valuable: &lt;strong&gt;better decisions.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;In this new era, don&apos;t be afraid to be a &quot;millionaire.&quot; Ask for five different implementations. Generate code you intend to throw away. Use this abundance to find the signal in the noise.&lt;/p&gt;
</content:encoded></item><item><title>Antigravity and Jules</title><link>https://ainoya.dev/posts/antigravity-and-jules/</link><guid isPermaLink="true">https://ainoya.dev/posts/antigravity-and-jules/</guid><description>A look at the practical benefits of the Gemini AI Pro coding tools, focusing on Jules&apos; responsiveness and Antigravity&apos;s task queuing model.</description><pubDate>Sat, 20 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;My original motivation for subscribing to Gemini AI Pro was to access the advanced chat model and the Google Workspace assistance features. However, since the plan also includes usage quotas for &lt;strong&gt;Jules&lt;/strong&gt; and &lt;strong&gt;Antigravity&lt;/strong&gt;, I started experimenting with them to see how they fit into my development process.&lt;/p&gt;
&lt;p&gt;For my daily tasks, these tools have settled into specific niches that improve my workflow, distinct from other web-based coding agents I have used in the past.&lt;/p&gt;
&lt;p&gt;Previously, I used the web interface for Codex. While powerful, I found the web UI to be incredibly heavy, often causing Google Chrome to hang or enter a &quot;waiting&quot; state. It created friction in what should be a quick process.&lt;/p&gt;
&lt;p&gt;In contrast, &lt;strong&gt;Jules&lt;/strong&gt; runs comfortably. The interface is snappy and responsive, which is critical when I want to iterate quickly. It isn&apos;t perfect—it occasionally gets stuck in an infinite loop executing the same command—but for routine tasks like small bug fixes or package updates, it handles the job without the browser lag.&lt;/p&gt;
&lt;p&gt;The standout feature for me, however, is &lt;strong&gt;Antigravity&lt;/strong&gt;, specifically its &quot;Agent Manager&quot; mode.&lt;/p&gt;
&lt;p&gt;Most chat-based AI tools require immediate, synchronous interaction. You type, you wait, you reply. It feels like a conversation that demands your full attention. Antigravity feels different; it is closer to sending an email to a colleague to handle a task in the background.&lt;/p&gt;
&lt;p&gt;I can fire off a request and let it run parallel to my own work. I particularly appreciate two features:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Inline Comments on Plans:&lt;/strong&gt; I can review the agent&apos;s proposed plan and add comments directly in context before execution.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Task Queuing:&lt;/strong&gt; This is the most significant UX improvement. If I think of a task while doing something else, I can explicitly queue it for later execution. It doesn&apos;t force a context switch or interrupt my current &quot;field work.&quot;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Having these included in my existing subscription has been a welcome addition to my toolkit.&lt;/p&gt;
</content:encoded></item><item><title>The Reverse Centaur</title><link>https://ainoya.dev/posts/the-reverse-centaur/</link><guid isPermaLink="true">https://ainoya.dev/posts/the-reverse-centaur/</guid><description>🤖+🦵</description><pubDate>Tue, 16 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I recently read &lt;a href=&quot;https://pluralistic.net/2025/12/10/the-reverse-centaurs-guide-to-criticizing-ai/&quot;&gt;&quot;The Reverse Centaur’s Guide to Criticizing AI,&quot;&lt;/a&gt; where he makes a crucial distinction:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;In automation theory, a &quot;centaur&quot; is a person who is assisted by a machine...
Obviously, a reverse centaur is machine head on a human body, a person who is serving as a squishy meat appendage for an uncaring machine.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I feel this split is widening based on the difficulty of &lt;strong&gt;verification&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;In software engineering, where testing and verification are relatively easy to automate, Generative AI empowers us as &lt;strong&gt;Centaurs&lt;/strong&gt;.
However, in physical tasks or complex human communication—where verification is difficult—technology often turns humans into &lt;strong&gt;Reverse Centaurs&lt;/strong&gt;. Instead of assistance, the machine simply imposes strict efficiency metrics, forcing humans to bridge the gap between algorithmic logic and reality at an exhausting pace.&lt;/p&gt;
&lt;p&gt;Pursuing efficiency to the point where the workplace becomes hostile to humans puts the cart before the horse.&lt;/p&gt;
</content:encoded></item><item><title>Digital Folklore: The Unsearchable Origins of the &apos;Silhouette&apos; Dance</title><link>https://ainoya.dev/posts/digital-folklore-silhouette-dance/</link><guid isPermaLink="true">https://ainoya.dev/posts/digital-folklore-silhouette-dance/</guid><description>Tracing the origins of the viral &apos;Silhouette&apos; dance challenge reveals the difficulties of searching choreography in the age of digital folklore.</description><pubDate>Sat, 13 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Around November 2025, a specific dance challenge began to go viral in Japan. Set to KANA-BOON&apos;s iconic track &lt;em&gt;&quot;Silhouette&quot;&lt;/em&gt; (a beloved &lt;em&gt;Naruto&lt;/em&gt; opening theme), the choreography mimics ninja hand signs (ninjutsu) synchronized to the beat.&lt;/p&gt;
&lt;p&gt;While the trend itself is widespread, one question remains surprisingly difficult to answer: &lt;strong&gt;Who actually choreographed this?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;As I attempted to trace the &quot;Patient Zero&quot; of this viral movement, I encountered a fascinating problem that highlights the current limitations of information retrieval in the age of video.&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;The Search for the Originator&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;Finding the source of a text-based meme is relatively straightforward in 2025. Large Language Models (LLMs) and advanced search indices can parse forums and archives effectively. However, tracking the origin of a specific physical movement—a choreography—across platforms like TikTok and Instagram remains a significant challenge.&lt;/p&gt;
&lt;p&gt;The difficulty stems from two main factors:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Semi-Closed Ecosystems:&lt;/strong&gt; Much of this data resides within the &quot;walled gardens&quot; of vertical video platforms, which are not fully indexed by open web crawlers.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The &quot;Choreography&quot; Data Problem:&lt;/strong&gt; Search engines still struggle to identify and categorize specific sequences of human motion. Unlike keywords, a dance move is fluid. It undergoes subtle &quot;remixes&quot; and modifications with every user iteration, behaving more like ancient oral tradition or folklore than digital data.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;&lt;strong&gt;The Oldest Record So Far&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;In my personal investigation, the oldest instance I could verify is a post by &lt;code&gt;@ross.hime&lt;/code&gt; dated &lt;strong&gt;July 19, 2025&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;&amp;lt;blockquote class=&quot;tiktok-embed&quot; cite=&quot;https://www.tiktok.com/@ross.hime/video/7528762593809288456&quot; data-video-id=&quot;7528762593809288456&quot; style=&quot;max-width: 605px;min-width: 325px;&quot;&amp;gt;
&amp;lt;section&amp;gt;
&amp;lt;a target=&quot;_blank&quot; title=&quot;@ross.hime&quot; href=&quot;https://www.tiktok.com/@ross.hime?refer=embed&quot;&amp;gt;@ross.hime&amp;lt;/a&amp;gt; naruto ninjitsu hands tutorial please 😂
&amp;lt;a title=&quot;narutoshippuden&quot; target=&quot;_blank&quot; href=&quot;https://www.tiktok.com/tag/narutoshippuden?refer=embed&quot;&amp;gt;#narutoshippuden&amp;lt;/a&amp;gt;
&amp;lt;a title=&quot;silloulettechallenge&quot; target=&quot;_blank&quot; href=&quot;https://www.tiktok.com/tag/silloulettechallenge?refer=embed&quot;&amp;gt;#silloulettechallenge&amp;lt;/a&amp;gt;
&amp;lt;a title=&quot;fyppppppppppppppppppppppp&quot; target=&quot;_blank&quot; href=&quot;https://www.tiktok.com/tag/fyppppppppppppppppppppppp?refer=embed&quot;&amp;gt;#fyppppppppppppppppppppppp&amp;lt;/a&amp;gt;
&amp;lt;a title=&quot;japan&quot; target=&quot;_blank&quot; href=&quot;https://www.tiktok.com/tag/japan?refer=embed&quot;&amp;gt;#japan&amp;lt;/a&amp;gt;
&amp;lt;a title=&quot;anime&quot; target=&quot;_blank&quot; href=&quot;https://www.tiktok.com/tag/anime?refer=embed&quot;&amp;gt;#anime&amp;lt;/a&amp;gt;
&amp;lt;a title=&quot;dancechallenge&quot; target=&quot;_blank&quot; href=&quot;https://www.tiktok.com/tag/dancechallenge?refer=embed&quot;&amp;gt;#dancechallenge&amp;lt;/a&amp;gt;
&amp;lt;a target=&quot;_blank&quot; title=&quot;♬ シルエット - KANA-BOON&quot; href=&quot;https://www.tiktok.com/music/シルエット-7525407952354822928?refer=embed&quot;&amp;gt;♬ シルエット - KANA-BOON&amp;lt;/a&amp;gt;
&amp;lt;/section&amp;gt;
&amp;lt;/blockquote&amp;gt;
&amp;lt;script async src=&quot;https://www.tiktok.com/embed.js&quot;&amp;gt;&amp;lt;/script&amp;gt;&lt;/p&gt;
&lt;p&gt;However, given the nature of the internet, it is highly probable that an even earlier version exists—perhaps the one that inspired this very video.&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;Crowdsourcing the History&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;It is ironic that despite the leap in AI capabilities regarding text and code generation, we still lack a reliable &quot;Reverse Choreography Search.&quot; The history of internet culture is being written in video format, and without proper archiving, the origins of these cultural moments risk being lost to the feed.&lt;/p&gt;
&lt;p&gt;If you know of a version of the &lt;em&gt;Silhouette&lt;/em&gt; dance that pre-dates July 2025, or if you have information about the true originator, please let me know. I am collecting information via the form below.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://forms.gle/CsjEoEtgKHfhXbYQ7&quot;&gt;Submit Information Here&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;I will update this post if new evidence comes to light.&lt;/p&gt;
</content:encoded></item><item><title>Fixing the WebM &quot;Missing Duration&quot; Problem with a Lightweight, Zero-Dependency Library</title><link>https://ainoya.dev/posts/webm-missing-duration/</link><guid isPermaLink="true">https://ainoya.dev/posts/webm-missing-duration/</guid><description>How webm-meta-lite extracts reliable WebM durations with a three-stage fallback without heavy dependencies.</description><pubDate>Sun, 07 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;Have you ever implemented audio or video recording features using the browser&apos;s standard &lt;code&gt;MediaRecorder&lt;/code&gt; API? While it is a powerful and convenient API, developers often run into a frustrating issue: the generated WebM files frequently lack the &lt;code&gt;Duration&lt;/code&gt; metadata property, or report it as &lt;code&gt;Infinity&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;This &quot;missing duration&quot; issue causes several downstream problems:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Server-side validation fails:&lt;/strong&gt; Checks like &quot;Max duration: 1 minute&quot; become impossible.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Media players struggle:&lt;/strong&gt; Total time is not displayed.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;UI glitches:&lt;/strong&gt; Seek bars fail to function correctly.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To solve this problem and accurately calculate the playback duration directly from WebM binaries, I built a new library called &lt;strong&gt;webm-meta-lite&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Repository: &lt;a href=&quot;https://github.com/ainoya/webm-meta-lite&quot;&gt;https://github.com/ainoya/webm-meta-lite&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;The Problem with Existing Solutions&lt;/h2&gt;
&lt;p&gt;When I first faced this issue, I researched existing solutions, but they felt like using a sledgehammer to crack a nut.&lt;/p&gt;
&lt;h3&gt;1. Using FFmpeg&lt;/h3&gt;
&lt;p&gt;FFmpeg is the industry standard for media processing. However, installing a massive binary on a server or container just to read a single timestamp is overkill. It significantly bloats Docker image sizes and adds unnecessary complexity to the deployment pipeline.&lt;/p&gt;
&lt;h3&gt;2. Existing JavaScript Libraries&lt;/h3&gt;
&lt;p&gt;There are Node.js libraries for WebM, but many are bundled with heavy recording or transcoding features. I only needed a lightweight parser, not a full media suite.&lt;/p&gt;
&lt;h2&gt;The Solution: webm-meta-lite&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;webm-meta-lite&lt;/code&gt; focuses on parsing the WebM binary structure to calculate the exact duration efficiently.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Zero Dependencies:&lt;/strong&gt; No external runtime dependencies to worry about.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pure TypeScript:&lt;/strong&gt; No native binaries required. It runs seamlessly in both browsers and Node.js.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Read-Only / Safe:&lt;/strong&gt; It scans the binary to extract information without mutating or rewriting files.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Usage&lt;/h2&gt;
&lt;p&gt;The API is straightforward. You pass a WebM &lt;code&gt;Blob&lt;/code&gt; (or a compatible Reader), and it returns the parsed metadata.&lt;/p&gt;
&lt;p&gt;If you are using Node.js (v19.8+), utilizing &lt;code&gt;fs.openAsBlob&lt;/code&gt; is the cleanest approach.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { parseWebm } from &apos;webm-meta-lite&apos;;
import { openAsBlob } from &apos;node:fs&apos;;

async function main() {
  // 1. Open the file as a Blob (Node.js v19.8+)
  const blob = await openAsBlob(&apos;./input.webm&apos;);

  // 2. Parse Metadata
  // Even if the header lacks duration, the library scans the data to calculate it.
  const metadata = await parseWebm(blob);

  // 3. Use the result
  // Note: The unit is milliseconds
  console.log(`Duration: ${metadata.durationMilliSeconds} ms`);
  
  // Example: Server-side validation
  if (metadata.durationMilliSeconds &amp;gt; 60 * 1000) {
    throw new Error(&apos;Uploads must be under 1 minute.&apos;);
  }
}

main();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For browsers, you can pass the &lt;code&gt;File&lt;/code&gt; object directly. For older Node.js versions, the library also provides a &lt;code&gt;createBufferReader&lt;/code&gt; helper.&lt;/p&gt;
&lt;h2&gt;How It Works: The 3-Stage Fallback Strategy&lt;/h2&gt;
&lt;p&gt;The core strength of this library lies in its algorithm. If the metadata is not found in the header, it calculates the duration from the actual data. Inspired by robust implementations in FFmpeg and webminspector, &lt;code&gt;webm-meta-lite&lt;/code&gt; uses a &lt;strong&gt;3-stage fallback strategy&lt;/strong&gt; to ensure accuracy while maintaining performance.&lt;/p&gt;
&lt;h3&gt;Step 1: Header Scan&lt;/h3&gt;
&lt;p&gt;The library reads the first 64KB of the file. If the Duration is present in the &lt;code&gt;Info&lt;/code&gt; segment, it returns immediately. This is the fastest path.&lt;/p&gt;
&lt;h3&gt;Step 2: Cues Scan&lt;/h3&gt;
&lt;p&gt;If the header is missing the duration, it looks for the &lt;code&gt;Cues&lt;/code&gt; (index) element. If found, it uses the timestamp of the last Cue point to determine the length.&lt;/p&gt;
&lt;h3&gt;Step 3: Tail Scan (The &quot;Safety Net&quot;)&lt;/h3&gt;
&lt;p&gt;If neither of the above works—common with streamed recordings—the library performs a &lt;strong&gt;Tail Scan&lt;/strong&gt;. It reads only the last 2MB of the file and scans the byte-level structure of the &lt;code&gt;Cluster&lt;/code&gt; elements. By finding the timestamp of the very last audio/video block, it calculates the precise total duration.&lt;/p&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;MediaRecorder&lt;/code&gt; often produces WebM files with missing or &lt;code&gt;Infinity&lt;/code&gt; duration.&lt;/li&gt;
&lt;li&gt;FFmpeg is frequently too heavy for simple duration checks.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;webm-meta-lite&lt;/code&gt; provides a lightweight, zero-dependency, and fast way to calculate duration.&lt;/li&gt;
&lt;li&gt;It implements a robust 3-stage fallback algorithm inspired by FFmpeg.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you need to validate user-uploaded audio/video on the backend or display accurate times in your UI, give it a try!&lt;/p&gt;
</content:encoded></item><item><title>Building Small CLI Tools for AI Agents</title><link>https://ainoya.dev/posts/why-cli-instead-of-mcp/</link><guid isPermaLink="true">https://ainoya.dev/posts/why-cli-instead-of-mcp/</guid><description>Explain why I chose CLI over MCP for AI agents, focusing on &apos;information economics&apos; and Unix philosophy.</description><pubDate>Sat, 06 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Recently, the &lt;strong&gt;Model Context Protocol (MCP)&lt;/strong&gt; has been gaining significant attention in the context of AI agent utilization. However, I deliberately chose a different path—a mature approach using the &lt;strong&gt;CLI (Command Line Interface)&lt;/strong&gt;—to create my own operation interfaces for daily tools like Slack, Atlassian, and Esa, going against the current trend.&lt;/p&gt;
&lt;p&gt;&quot;Why use a simple CLI now instead of the rich MCP?&quot; The reason lies in the &lt;strong&gt;&quot;economics of information&quot;&lt;/strong&gt; surrounding the context windows of current LLMs and a return to the good old &lt;strong&gt;&quot;Unix Philosophy&quot;&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Here are the tools I created (written in Go):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Slack CLI&lt;/strong&gt;: https://github.com/ainoya/slack-cli&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Atlassian CLI&lt;/strong&gt;: https://github.com/ainoya/atlassian-cli&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Esa CLI&lt;/strong&gt;: https://github.com/ainoya/esa-cli&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;The &quot;Context Pressure&quot; Problem with MCP&lt;/h2&gt;
&lt;p&gt;Many current MCP server implementations tend to be designed to pass rich information from connected tools into the context. However, LLM context windows are finite and cost money. Simply asking an AI to &quot;check the latest Slack messages&quot; can result in loading a massive amount of unnecessary noise, lowering the signal-to-noise ratio of information (which in turn lowers the accuracy of the answer). This puts the cart before the horse.&lt;/p&gt;
&lt;h2&gt;&quot;Pre-filtering&quot; with CLI and Pipelines&lt;/h2&gt;
&lt;p&gt;This is where the strength of the CLI comes into play. With a CLI, you can physically narrow down information using Linux pipeline processing &lt;em&gt;before&lt;/em&gt; passing it to the AI.&lt;/p&gt;
&lt;p&gt;For example, when letting an AI agent read logs, an MCP implementation might inadvertently pass &quot;all logs&quot;. With a CLI, you can write it like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Filter only the truly necessary information and pass it to the context
slack-cli log --channel &quot;dev-ops&quot; | grep &quot;ERROR&quot; | tail -n 20
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Instead of throwing raw information at the AI, humans filter it with intention (Pre-filtering). I believe this &lt;strong&gt;&quot;Unix Philosophy&quot;&lt;/strong&gt; approach is what directly leads to token savings and improved accuracy.&lt;/p&gt;
&lt;h2&gt;The Ease of Defining in &quot;Markdown&quot;&lt;/h2&gt;
&lt;p&gt;The CLI approach is also advantageous for configuring &lt;strong&gt;Claude Code&lt;/strong&gt; or &lt;strong&gt;Cursor&lt;/strong&gt;. Modifying the logic of an MCP server itself is a hassle, but with a CLI, you can tune the behavior simply by describing instructions in Markdown (natural language), such as &quot;run this command with these arguments&quot;.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;MCP&lt;/strong&gt;: Requires server-side code modifications.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CLI&lt;/strong&gt;: Adjustable just by rewriting the prompt (configuration file).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This &quot;tactile feel&quot; and &quot;agility&quot; are extremely important in individual development workflows.&lt;/p&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;Of course, MCP is suitable for operations requiring complex state management. However, for &quot;Read operations&quot; such as checking documents or logs, I feel that the &lt;strong&gt;CLI × Pipeline&lt;/strong&gt; approach might actually be the most &lt;strong&gt;&quot;AI Native&quot;&lt;/strong&gt; solution.&lt;/p&gt;
&lt;p&gt;If you are interested, please take a look at the repositories.&lt;/p&gt;
</content:encoded></item><item><title>Cache Satori-generated OGP images in Astro Cactus to speed up builds</title><link>https://ainoya.dev/posts/astro-ogp-build-cache/</link><guid isPermaLink="true">https://ainoya.dev/posts/astro-ogp-build-cache/</guid><description>Astro Cactus generates an OGP image per post with Satori + Resvg. By caching the PNGs locally and using a content hash, we can avoid regenerating unchanged images and cut build time.</description><pubDate>Mon, 25 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;a href=&quot;https://astro.build/themes/details/astro-cactus/&quot;&gt;Astro Cactus&lt;/a&gt; ships with dynamic Open Graph (OGP) images: for each post, the theme renders a title card using Satori (to SVG) and Resvg (to PNG). It’s convenient—but if every image is regenerated on every build, the OGP step adds up. On my machine, each image takes roughly 100–300 ms, which becomes noticeable with dozens of posts.&lt;/p&gt;
&lt;p&gt;This post shows a simple, file-based cache that avoids re-rendering unchanged images. The approach is deterministic, CI-friendly, and requires no extra services.&lt;/p&gt;
&lt;h2&gt;How it works&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;We compute a content hash from the post’s title and date.&lt;/li&gt;
&lt;li&gt;We store the rendered PNG in &lt;code&gt;node_modules/.og_image_cache/&amp;lt;hash&amp;gt;.png&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;On the next build, if the PNG exists, we reuse it; otherwise we render and save.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The cache key changes whenever the title or date changes, so the image is re-rendered only when necessary.&lt;/p&gt;
&lt;h2&gt;The code&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;import crypto from &quot;node:crypto&quot;;
import fs from &quot;node:fs&quot;;
import path from &quot;node:path&quot;;
import { Resvg } from &quot;@resvg/resvg-js&quot;;
import type { APIContext } from &quot;astro&quot;;
import satori, { type SatoriOptions } from &quot;satori&quot;;
import { html } from &quot;satori-html&quot;;
import RobotoMonoBold from &quot;@/assets/roboto-mono-700.ttf&quot;;
import RobotoMono from &quot;@/assets/roboto-mono-regular.ttf&quot;;
import { getAllPosts } from &quot;@/data/post&quot;;
import { siteConfig } from &quot;@/site.config&quot;;
import { getFormattedDate } from &quot;@/utils/date&quot;;

const ogOptions: SatoriOptions = {
  // debug: true,
  fonts: [
    { data: Buffer.from(RobotoMono), name: &quot;Roboto Mono&quot;, style: &quot;normal&quot;, weight: 400 },
    { data: Buffer.from(RobotoMonoBold), name: &quot;Roboto Mono&quot;, style: &quot;normal&quot;, weight: 700 },
  ],
  height: 630,
  width: 1200,
};

// Cache directory: keep it out of git, easy to persist in CI.
const CACHE_DIR = path.join(process.cwd(), &quot;node_modules&quot;, &quot;.og_image_cache&quot;);

// Ensure cache directory exists.
function ensureCacheDir() {
  if (!fs.existsSync(CACHE_DIR)) {
    fs.mkdirSync(CACHE_DIR, { recursive: true });
  }
}

// Create a short, deterministic content hash.
function generateContentHash(title: string, pubDate: Date): string {
  const content = `${title}-${pubDate.toISOString()}`;
  return crypto.createHash(&quot;sha256&quot;).update(content).digest(&quot;hex&quot;).slice(0, 16);
}

// Lookup or render the PNG, then persist it.
async function getCachedOrGeneratePng(
  title: string,
  pubDate: Date,
  postDate: string,
): Promise&amp;lt;Uint8Array&amp;gt; {
  ensureCacheDir();

  const contentHash = generateContentHash(title, pubDate);
  const cacheFilePath = path.join(CACHE_DIR, `${contentHash}.png`);

  if (fs.existsSync(cacheFilePath)) {
    console.log(`Using cached OG image for: ${title}`);
    return new Uint8Array(fs.readFileSync(cacheFilePath));
  }

  console.log(`Generating new OG image for: ${title}`);
  const svg = await satori(markup(title, postDate), ogOptions);
  const png = new Resvg(svg).render().asPng();
  const pngBuffer = new Uint8Array(png);

  fs.writeFileSync(cacheFilePath, pngBuffer);
  return pngBuffer;
}

const markup = (title: string, pubDate: string) =&amp;gt;
  html`&amp;lt;div tw=&quot;flex flex-col w-full h-full bg-[#1d1f21] text-[#c9cacc]&quot;&amp;gt;
    &amp;lt;div tw=&quot;flex flex-col flex-1 w-full p-10 justify-center&quot;&amp;gt;
      &amp;lt;p tw=&quot;text-2xl mb-6&quot;&amp;gt;${pubDate}&amp;lt;/p&amp;gt;
      &amp;lt;h1 tw=&quot;text-6xl font-bold leading-snug text-white&quot;&amp;gt;${title}&amp;lt;/h1&amp;gt;
    &amp;lt;/div&amp;gt;
    &amp;lt;div tw=&quot;flex items-center justify-between w-full p-10 border-t border-[#2bbc89] text-xl&quot;&amp;gt;
      &amp;lt;div tw=&quot;flex items-center&quot;&amp;gt;
        &amp;lt;p tw=&quot;font-semibold&quot;&amp;gt;${siteConfig.title}&amp;lt;/p&amp;gt;
      &amp;lt;/div&amp;gt;
      &amp;lt;p&amp;gt;by ${siteConfig.author}&amp;lt;/p&amp;gt;
    &amp;lt;/div&amp;gt;
  &amp;lt;/div&amp;gt;`;

export async function GET(context: APIContext) {
  const { pubDate, title } = context.props;

  const postDate = getFormattedDate(pubDate, {
    month: &quot;long&quot;,
    weekday: &quot;long&quot;,
  });

  const pngBuffer = await getCachedOrGeneratePng(title, pubDate, postDate);

 const body = pngBuffer.slice(0).buffer;
 return new Response(body, {
  headers: {
   // Adjust cache headers as needed for your use case.
   &quot;Cache-Control&quot;: &quot;public, max-age=31536000, immutable&quot;,
   &quot;Content-Type&quot;: &quot;image/png&quot;,
  },
 });
}

export async function getStaticPaths() {
  const posts = await getAllPosts();
  return posts
    .filter(({ data }) =&amp;gt; !data.ogImage) // skip if a custom image is set
    .map((post) =&amp;gt; ({
      params: { slug: post.id },
      props: {
        pubDate: post.data.updatedDate ?? post.data.publishDate,
        title: post.data.title,
      },
    }));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;What each piece does&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Cache directory
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;node_modules/.og_image_cache&lt;/code&gt; is ignored by git and easy to persist across CI runs. If your CI re-installs dependencies from scratch, persist this path between builds to get the benefit.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Cache key
&lt;ul&gt;
&lt;li&gt;`sha256(title + ISO(pubDate)), first 16 hex chars. If the title or date changes, we get a new key and the image is regenerated.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;SVG → PNG
&lt;ul&gt;
&lt;li&gt;Satori returns SVG; Resvg rasterizes to PNG. Fonts are embedded via SatoriOptions so the output is deterministic.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Endpoint behavior
&lt;ul&gt;
&lt;li&gt;The endpoint uses getStaticPaths() with props so the images are pre-rendered at build time (SSG). The Cache-Control header is for browser/CDN caching and is separate from the on-disk cache.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Updated posts
&lt;ul&gt;
&lt;li&gt;We pass updatedDate ?? publishDate to the hash. Any content update that bumps updatedDate will invalidate the image.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Operational notes&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;CI persistence
&lt;ul&gt;
&lt;li&gt;If builds happen in a clean environment, configure your CI cache to persist node_modules/.og_image_cache. No additional services are needed.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;When to invalidate
&lt;ul&gt;
&lt;li&gt;Decide what should trigger a new image. Title + date works for most blogs. If you frequently tweak the template, include a version salt (see below).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Growth management
&lt;ul&gt;
&lt;li&gt;Over long periods, the cache can grow. Consider a small cleanup script to remove files older than N days.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Optional improvements&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Add a template/version salt&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If you change fonts, colors, or layout, you likely want all images to refresh. Add a constant to the hash:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const CACHE_VERSION = &quot;v1&quot;; // bump when markup/fonts change
function generateContentHash(title: string, pubDate: Date): string {
  const content = `${CACHE_VERSION}|${title}|${pubDate.toISOString()}`;
  return crypto.createHash(&quot;sha256&quot;).update(content).digest(&quot;hex&quot;).slice(0, 16);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;Hash the actual markup&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;For the most robust invalidation, hash the full SVG input (including site title/author/date formatting):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const svgInput = markup(title, postDate);
const contentHash = crypto.createHash(&quot;sha256&quot;).update(JSON.stringify({ svgInput })).digest(&quot;hex&quot;).slice(0, 16);
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;Store pre-renders under public/og/&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If you prefer visible, versioned assets that can be uploaded to a CDN or backed up outside node_modules, write PNGs to &lt;code&gt;public/og/&amp;lt;hash&amp;gt;.png&lt;/code&gt; and reference them by URL. The trade-off is repo churn or extra cleanup if you commit them.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Concurrency controls for very large blogs&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;When building hundreds of images in parallel, Resvg can saturate CPU. A simple promise pool (e.g., p-limit) around your generation logic can keep the build smooth.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Font and license hygiene&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Keep fonts local and embed them via SatoriOptions as shown. Make sure your font license permits embedding in generated images.&lt;/p&gt;
&lt;h2&gt;Results&lt;/h2&gt;
&lt;p&gt;With caching enabled, unchanged posts reuse their OGP images immediately. On my setup the OGP step dropped by roughly 100–300 ms per image that didn’t need regeneration, which noticeably reduced total build time as the post count grew.&lt;/p&gt;
&lt;h2&gt;Footnotes &amp;amp; pointers&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Astro endpoints are called at build time in SSG and can generate static files; they become live routes in SSR.&lt;/li&gt;
&lt;li&gt;Satori converts HTML/CSS to SVG; Resvg rasterizes SVG to PNG.&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Rethinking State: My Experience with Cloudflare&apos;s Durable Objects</title><link>https://ainoya.dev/posts/durable-object/</link><guid isPermaLink="true">https://ainoya.dev/posts/durable-object/</guid><description>Exploring Cloudflare Durable Objects as a paradigm shift in stateful edge computing. From Redis Streams to zero-latency SQLite integration, discover how this technology revolutionizes real-time application architecture with globally unique instances and the Actor Model.</description><pubDate>Sun, 03 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I recently found myself architecting a seamless experience for an AI chat application. The goal was simple: if a user&apos;s connection drops during a streaming response, they should be able to reconnect and pick up right where they left off.&lt;/p&gt;
&lt;p&gt;My initial thought was a conventional setup using Redis Streams. It&apos;s a solid, well-understood pattern for handling streaming data and reconnections. However, during my research, I stumbled upon a technology that promised to solve the same problem with a radically different approach: &lt;strong&gt;Cloudflare Durable Objects&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;The more I delved into it, the clearer it became that this wasn&apos;t just an alternative solution. It was a groundbreaking architecture that challenges the fundamental assumptions we hold about distributed systems, a sentiment echoed in &lt;a href=&quot;https://blog.cloudflare.com/introducing-workers-durable-objects/&quot;&gt;Cloudflare&apos;s introductory blog post&lt;/a&gt;. This article chronicles that journey of discovery, from initial skepticism to a full-blown paradigm shift in how I think about building stateful applications.&lt;/p&gt;
&lt;h3&gt;TL;DR&lt;/h3&gt;
&lt;p&gt;Cloudflare Durable Objects are not just another feature; they represent a paradigm shift in how we build stateful applications. By providing strong consistency at the edge, zero-latency SQL, and automatic geographic distribution, they dismantle constraints that have defined distributed systems for decades.&lt;/p&gt;
&lt;h3&gt;A New Foundation: Breaking Through Old Limits&lt;/h3&gt;
&lt;p&gt;Traditional serverless architectures, for all their benefits, are built on a core limitation: they are stateless. This design choice offloads state management to external databases, introducing network latency and geographical constraints.&lt;/p&gt;
&lt;p&gt;Durable Objects, however, are inherently stateful. The revolutionary idea behind them is the concept of a &lt;strong&gt;&quot;globally unique instance,&quot;&lt;/strong&gt; as detailed in the &lt;a href=&quot;https://developers.cloudflare.com/durable-objects/&quot;&gt;official documentation&lt;/a&gt;. A Durable Object with a specific ID is guaranteed to exist only once in the entire world. Any client, from anywhere, can access that single, unique instance.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// A globally unique counter
let id = COUNTER_NAMESPACE.idFromName(&quot;global-counter&quot;);
let counter = COUNTER_NAMESPACE.get(id);
let response = await counter.fetch(request);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This simple primitive unlocks a completely new way of building applications by co-locating state and the code that operates on it.&lt;/p&gt;
&lt;h3&gt;Architectural Showdown: Redis vs. Durable Objects&lt;/h3&gt;
&lt;p&gt;Let&apos;s compare the two approaches for our chat application.&lt;/p&gt;
&lt;h4&gt;The Traditional Redis Stream Approach&lt;/h4&gt;
&lt;p&gt;This architecture involves multiple WebSocket servers, a load balancer, and a Redis cluster for state management.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;graph TD
    subgraph &quot;Conventional Architecture&quot;
        A[Load Balancer] --&amp;gt; B[WebSocket Server 1]
        A --&amp;gt; C[WebSocket Server 2]
        A --&amp;gt; D[WebSocket Server N]
        B --&amp;gt; E[Redis Stream]
        C --&amp;gt; E
        D --&amp;gt; E
        E --&amp;gt; F[Persistence Layer]
    end

    G[Client] --&amp;gt; A
    H[Problem: State synchronization]
    I[Problem: Complex failure handling]
    J[Problem: Scaling complexity]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The challenges are numerous: synchronizing state across servers, the operational overhead of managing Redis, and complex recovery logic.&lt;/p&gt;
&lt;h4&gt;The Durable Objects Approach&lt;/h4&gt;
&lt;p&gt;With Durable Objects, the architecture is radically simplified. Each chat session becomes a Durable Object, encapsulating all its state and logic.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;graph TD
    subgraph &quot;Durable Objects Architecture&quot;
        A[Global Edge] --&amp;gt; B[Chat Session DO]
        B --&amp;gt; C[Local SQLite]
        B --&amp;gt; D[WebSocket Connections]
        D --&amp;gt; E[Client 1]
        D --&amp;gt; F[Client 2]
    end

    H[Automatic Geo-Distribution]
    I[Code and State Colocation]
    J[Zero-Latency Access]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This eliminates external middleware and allows the system to benefit from Cloudflare&apos;s global network for automatic geographic distribution. The difference in implementation is stark, moving from complex, multi-system coordination to a self-contained, elegant object.&lt;/p&gt;
&lt;h3&gt;The Revolution: Zero-Latency SQLite Integration&lt;/h3&gt;
&lt;p&gt;This is where Durable Objects truly defy conventional wisdom. They offer &lt;strong&gt;synchronous SQL execution&lt;/strong&gt; directly within the object, a feature Cloudflare detailed in their post, &quot;&lt;a href=&quot;https://blog.cloudflare.com/sqlite-in-durable-objects/&quot;&gt;Zero-latency SQLite storage in every Durable Object&lt;/a&gt;.&quot;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// No await needed for reads!
let cursor = sql.exec(&quot;SELECT name, email FROM users&quot;);
for (let user of cursor) {
  console.log(user.name, user.email);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is possible because SQLite runs as a library in the same thread as your code, with data aggressively cached in memory and stored on a local SSD. To prevent synchronous writes from blocking the process, Cloudflare introduced &lt;strong&gt;Output Gates&lt;/strong&gt;, a mechanism that holds a response until the data has been durably stored, ensuring both performance and consistency.&lt;/p&gt;
&lt;h3&gt;Solving the N+1 Query Problem for Good&lt;/h3&gt;
&lt;p&gt;This zero-latency access effectively eliminates the &quot;N+1 query problem&quot; at its root. In a traditional architecture, 101 queries over a network with 5ms latency would take over half a second. Inside a Durable Object, the same 101 queries are function calls that complete in a few milliseconds.&lt;/p&gt;
&lt;p&gt;This isn&apos;t just an optimization; it&apos;s a fundamental change in the performance model. It liberates developers from &quot;N+1 query phobia&quot; and allows them to write simple, clear, and maintainable code without sacrificing performance. The need for complex &lt;code&gt;JOIN&lt;/code&gt; statements, often a source of technical debt, simply evaporates.&lt;/p&gt;
&lt;h3&gt;Why Is This Technology So Unique?&lt;/h3&gt;
&lt;p&gt;A striking fact is that &lt;strong&gt;no other major cloud provider offers anything quite like Durable Objects.&lt;/strong&gt; While services like AWS Lambda or Google Cloud Functions are powerful, they remain fundamentally stateless at the edge. The tight integration of stateful compute, persistent storage, and a global network is, for now, unique to Cloudflare. This uniqueness stems from Cloudflare&apos;s ability to leverage its massive, pre-existing global network of over 300 data centers to build a truly distributed platform.&lt;/p&gt;
&lt;h3&gt;A New Mindset: Designing with Actors&lt;/h3&gt;
&lt;p&gt;To leverage Durable Objects effectively, we must shift our thinking from the traditional request/response model to the &lt;strong&gt;Actor Model&lt;/strong&gt;. At its core, the Actor Model treats &quot;actors&quot; as the fundamental units of computation. Each actor is an independent entity with its own state and logic, communicating with others exclusively through asynchronous messages. This approach is a natural fit for Durable Objects and requires avoiding common pitfalls.&lt;/p&gt;
&lt;h4&gt;Anti-Pattern 1: The Monolithic &quot;God&quot; Object&lt;/h4&gt;
&lt;p&gt;Avoid creating a single DO to manage state for all users. This creates a massive bottleneck, as all requests are serialized through a single thread, a limitation outlined in the &lt;a href=&quot;https://developers.cloudflare.com/durable-objects/platform/limits/&quot;&gt;platform limits documentation&lt;/a&gt;. The correct approach is to assign one DO per logical entity, such as a chat room or a user session.&lt;/p&gt;
&lt;h4&gt;Anti-Pattern 2: Synchronous External API Calls&lt;/h4&gt;
&lt;p&gt;Making a long-running external API call inside a DO will block all other requests to that object. Instead, long-running tasks should be offloaded to background processes using the &lt;a href=&quot;https://developers.cloudflare.com/durable-objects/api-reference/durable-object-alarms/&quot;&gt;Alarms API&lt;/a&gt;. This ensures the object remains responsive.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// ❌ BAD: Blocking on a slow external API
export class Order extends DurableObject {
  async processPayment() {
    // This call could take seconds, blocking all other operations
    const result = await fetch(&apos;https://slow-payment-api.com/charge&apos;);
    return result.json();
  }
}

// ✅ GOOD: Use Alarms for non-blocking operations
export class Order extends DurableObject {
  async schedulePaymentProcessing() {
    // Immediately respond to the client
    // Schedule the long-running task to be executed later
    await this.ctx.storage.setAlarm(Date.now() + 1000);
    return new Response(JSON.stringify({ status: &apos;processing&apos; }), { status: 202 });
  }

  async alarm() {
    // The alarm runs in the background without blocking new requests
    await fetch(&apos;https://slow-payment-api.com/charge&apos;);
    // ...update state after the API call completes
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;Cloudflare&apos;s Durable Objects are more than just a clever piece of engineering. They represent a fundamental shift in how we can and should build applications for the web.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;A Paradigm Shift:&lt;/strong&gt; They bring stateful computing back to the edge, challenging the &quot;stateless-first&quot; orthodoxy.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A Technical Breakthrough:&lt;/strong&gt; Zero-latency SQL access inside a serverless function fundamentally changes performance calculations.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A Superior Developer Experience:&lt;/strong&gt; They drastically simplify the architecture for complex features like real-time collaboration, as demonstrated in tutorials like the &lt;a href=&quot;https://developers.cloudflare.com/durable-objects/tutorials/build-a-chat-application/&quot;&gt;chat application example&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Proven Reliability:&lt;/strong&gt; Cloudflare uses Durable Objects to power its own critical services, offering features like 30-day point-in-time recovery as a byproduct of its robust design.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;In exploring a solution for a simple chat application, I found a technology that offers a glimpse into the future of distributed application architecture. It&apos;s a future that is simpler, faster, and more powerful.&lt;/p&gt;
</content:encoded></item><item><title>Beyond &quot;Just Code It&quot;: Adapting Specification-Driven Principles for AI Agents</title><link>https://ainoya.dev/posts/spec-driven-ai-coding/</link><guid isPermaLink="true">https://ainoya.dev/posts/spec-driven-ai-coding/</guid><description>Learn how to transform AI agents from chaotic coders into design partners using Specification-Driven Development principles for more structured collaboration.</description><pubDate>Sat, 02 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;The era of AI-powered software development is firmly upon us. If you&apos;ve ever felt overwhelmed by an AI agent going rogue and generating unmanageable code, or if you&apos;re a solo developer using tools like Cursor and seeking a more structured way to collaborate with your AI partner, this article is for you. Tools like GitHub Copilot and emerging AI agents have shifted from novelties to my daily drivers. But this shift has introduced a new challenge: the &quot;wall of text&quot; review.&lt;/p&gt;
&lt;p&gt;I&apos;ve been there. You give an AI agent a task and return to find hundreds of lines of new code. This isn&apos;t just a feeling; it&apos;s a measurable drain on productivity. &lt;a href=&quot;https://devops.com/survey-ai-tools-are-increasing-amount-of-bad-code-needing-to-be-fixed/&quot;&gt;A survey featured on DevOps.com&lt;/a&gt;, for instance, found that 67% of developers reported spending more time debugging AI-generated code.&lt;/p&gt;
&lt;p&gt;To address this, I turned to a proven software engineering practice: &lt;strong&gt;Specification-Driven Development (SDD)&lt;/strong&gt;. This approach, championed by modern development tools like &lt;a href=&quot;https://kiro.dev/&quot;&gt;Kiro&lt;/a&gt;, emphasizes defining a clear specification before writing code. I decided to adapt this philosophy for my collaboration with AI agents, transforming the AI from a mere &quot;coder&quot; into a &quot;design partner.&quot;&lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;The Workflow: Applying SDD to AI Collaboration&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;My process applies the core principles of SDD to my interactions with AI. It starts with a structured dialogue to ensure alignment before any code is written, turning a free-form instruction into a well-defined plan.&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;Phase 1: The Guardrail Prompt&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;Autonomous AI agents tend to rush into coding. To counter this, I don&apos;t just ask the AI to &quot;implement a feature.&quot; I use a &quot;guardrail prompt&quot; that instructs it to act as my design partner and first ask clarifying questions. This initial dialogue, a cornerstone of good design practice, forces the AI to pause and think, creating a shared understanding of the requirements upfront.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Here is a practical &quot;guardrail prompt&quot; I use to start the conversation:

You are an expert software design partner. Before generating any code, your primary goal is to understand the requirements deeply.

Based on the feature request below, you must first ask me clarifying questions to understand:
1. **Why**: The business goal or the problem this feature solves.
2. **Who**: The end-users (e.g., general user, admin) and their needs.
3. **What**: The primary use cases.
4. **Constraints**: Any technical limitations or required libraries/frameworks.
5. **Non-functional requirements**: Specific concerns about security, performance, etc.

Do not write any design documentation or code until I have answered these questions.

My feature request is: [Your feature idea here]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;strong&gt;Phase 2: The Structured Specification for Reliable AI Implementation&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;Based on the dialogue, the AI generates a formal design document in Markdown. This document becomes the foundation that enables AI agents to implement features accurately and systematically, following the SDD principle of establishing a Single Source of Truth.&lt;/p&gt;
&lt;p&gt;The document contains specific elements that AI agents need for reliable implementation:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Use cases (Given/When/Then).&lt;/li&gt;
&lt;li&gt;A domain model (Mermaid.js diagram).&lt;/li&gt;
&lt;li&gt;API specifications (OpenAPI).&lt;/li&gt;
&lt;li&gt;Sequence diagrams.&lt;/li&gt;
&lt;li&gt;Database schema definitions (DDL).&lt;/li&gt;
&lt;li&gt;A revision history for tracking changes.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This structured approach ensures that AI agents have all the context they need to implement features without guesswork. When requirements evolve, I update the document with clear versioning to maintain implementation accuracy across iterations.&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;Phase 3: Detecting Complexity as a Signal&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;A valuable lesson I&apos;ve learned from this process has been using the AI&apos;s questions as a &quot;complexity sensor.&quot; If I, the human developer, find it difficult to answer the AI&apos;s questions, it&apos;s a strong signal that the feature itself is too complex or ill-defined.&lt;/p&gt;
&lt;p&gt;This friction tells me it&apos;s time to simplify. I split the feature into smaller, more manageable pieces and restart the specification process for each. This feedback loop helps me adhere to the principle of &quot;divide and conquer.&quot;&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;Phase 4: Generating Actionable Tasks from the Specification&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;Once I approve the design document, I instruct the AI: &quot;Based on this confirmed specification, generate a list of implementation tasks.&quot;&lt;/p&gt;
&lt;p&gt;The AI then analyzes the entire document and produces a checklist of discrete, ordered tasks, perfectly suited for AI coding tools:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;* [ ] Task 1: Generate TypeScript type definitions from the OpenAPI schema.
* [ ] Task 2: Create a database migration file based on the DDL.
* [ ] Task 3: Implement the POST /users endpoint handler and routing.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Because each task is small, specific, and derived from a mutually agreed-upon plan, the AI&apos;s success rate is significantly higher.&lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;Why This Adaptation of SDD Works&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;Applying these specification-driven principles to my AI workflow has yielded clear benefits:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;It drastically reduces the cognitive load of reviews.&lt;/strong&gt; I can focus on the design, not on deciphering a mountain of code.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;It keeps the human in the driver&apos;s seat.&lt;/strong&gt; The process ensures my development aligns with strategic goals.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;It minimizes rework.&lt;/strong&gt; Catching misunderstandings at the design phase is far more efficient. Research by IBM&apos;s Systems Sciences Institute famously found that fixing a bug after release can cost up to 100 times more than fixing it during design. My approach is a practical application of this wisdom in the AI era.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;&lt;strong&gt;Conclusion: A More Sustainable Way to Collaborate with AI&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;This methodology isn&apos;t a new invention, but rather my deliberate adaptation of a robust engineering principle to the unique challenges of AI collaboration. It establishes a clear division of labor: I define the &quot;what&quot; and &quot;why,&quot; while the AI executes the &quot;how.&quot;&lt;/p&gt;
&lt;p&gt;By re-emphasizing the importance of the specification, I can harness the power of AI agents without abdicating my role as an architect. I trade the chaos of the &quot;wall of text&quot; for the clarity of a structured dialogue, leading to a more controlled, predictable, and ultimately faster development lifecycle.&lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;Appendix: A Real-World Prompt for Specification-Driven AI Collaboration&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;To make this workflow even more concrete, here is the comprehensive prompt I use with AI agents like Cursor. It encapsulates all the steps discussed, from the initial dialogue to generating the final task list. This prompt turns the AI into a dedicated design partner that follows the SDD process rigorously.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Rules for Specification-Driven Development with an AI Agent

## Role

You are an **expert design partner specializing in Specification-Driven Development (SDD)**. Inspired by tools like Kiro, your primary mission is to define a &quot;specification&quot; first and then derive all documents and code from it. This ensures a consistent and efficient development process with minimal rework.

Your main task is to respond to a feature development request by **identifying missing requirements through dialogue** with the user, building consensus, and accurately generating the following deliverable:

1. **Design Document (docs/[feature-name].md)**: This document defines the &quot;Why&quot; and &quot;What&quot; of the feature and includes the API specification. It serves as the **Single Source of Truth**.

## Execution Process

When you receive an instruction like &quot;Design the [feature-name] feature,&quot; you must strictly follow these steps.

### Step 0: The Clarification Dialogue

After receiving the initial instruction, do not start generating the document immediately. First, ask the user clarifying questions to improve the resolution of the design and identify potential oversights.

**&amp;lt;Your Action&amp;gt;**

1. Identify the feature name from the user&apos;s instruction.
2. Based on the &quot;Design Requirements Checklist&quot; below, generate questions about any unclear points and ask the user for answers.

&amp;gt; **Example AI Response:**
&amp;gt; &quot;Understood. I will begin designing the &apos;[feature-name]&apos; feature.
&amp;gt; To ensure the design is accurate, please allow me to ask a few questions:
&amp;gt;
&amp;gt; **[Design Requirements Checklist]**
&amp;gt; - **Background/Goal (Why):** What specific problem or business objective does this feature address?
&amp;gt; - **Users (Who):** What kind of users will use this feature (e.g., general users, administrators)?
&amp;gt; - **Primary Use Cases (What):** Could you describe the most important and representative user scenarios?
&amp;gt; - **Technical Constraints:** Are there any technical constraints or required libraries/services?
&amp;gt; - **Non-functional Requirements:** Are there any special considerations for security, performance, or future scalability?
&amp;gt;
&amp;gt; Could you please provide information on these points to the best of your ability?&quot;

### Step 1: Initialize Project Structure

Generate a file structure based on the feature name. For example, for &quot;User Authentication,&quot; create `docs/user-authentication.md`.

### Step 2: Generate the Design Document

Based on the information gathered in Step 0, generate a draft of the design document using the template below.

---

# [Feature Name] Design Document

&amp;gt; **Purpose**: This document aims to create a shared understanding by defining the feature&apos;s background, use cases, domain model, and technical API specifications. This document is the Single Source of Truth.

**Revision History**

| Date       | Version | Author | Summary of Changes   |
| :--------- | :------ | :----- | :------------------- |
| YYYY-MM-DD | 1.0.0   | AI     | Initial draft creation |

**Table of Contents**

- [1. Introduction](#1-introduction)
- [2. Use Cases](#2-use-cases)
- [3. Domain Model](#3-domain-model)
- [4. API and Implementation Design](#4-api-and-implementation-design)
- [5. Considerations](#5-considerations)

---

## 1. Introduction

(Describe the feature&apos;s background, purpose, and scope based on the Step 0 dialogue.)

## 2. Use Cases

#### UC-001: [Use Case Name]

**Actor**: (The user or system performing the action)
**Trigger**: (The event that initiates this use case)

**Given**:
- (Preconditions that must be true before the use case starts)

**When/Then**:
1. **When**: (The user performs an action)
2. **Then**: (The system responds or changes state)

**Postconditions (on success)**:
- (The state of the system after the flow is successfully completed)

## 3. Domain Model

```mermaid
classDiagram
  class EntityA {
    -id: string
    +action()
  }
  class EntityB {
    -id: string
  }
  EntityA &quot;1&quot; -- &quot;0..*&quot; EntityB
```

## 4. API and Implementation Design

### 4.1. API Specification (OpenAPI)

```yaml
openapi: 3.0.3
info:
  title: &quot;[Feature Name] API&quot;
  version: &quot;1.0.0&quot;
paths:
  /example-endpoint:
    get:
      summary: &quot;Example GET endpoint&quot;
      operationId: &quot;getExample&quot;
      responses:
        &apos;200&apos;:
          description: &quot;Successful response&quot;
```

### 4.2. Sequence Diagram

```mermaid
sequenceDiagram
  participant User
  participant Frontend
  participant Backend
  User-&amp;gt;&amp;gt;Frontend: Request
  Frontend-&amp;gt;&amp;gt;Backend: API Call
  Backend--&amp;gt;&amp;gt;Frontend: Response
  Frontend--&amp;gt;&amp;gt;User: Display
```

### 4.3. Database Design (Schema)

Note on Cloudflare D1 Usage:
This project uses Cloudflare D1, which does not support complex transactions.

* Avoid multi-table updates where possible.
* If necessary, ensure idempotency at the application level.
* Add indexes to columns used in search conditions for performance.

**[table_name] Table**

| Column | Type | Constraints | Description |
| :---- | :---- | :---- | :---- |
| id | TEXT | PRIMARY KEY | Unique ID |
| created_at | TEXT | NOT NULL | Creation timestamp |

```sql
CREATE TABLE [table_name] (
    id TEXT PRIMARY KEY,
    created_at TEXT NOT NULL DEFAULT (datetime(&apos;now&apos;, &apos;utc&apos;))
);
```

## 5. Considerations

(Note any non-functional requirements, security, performance, or scalability concerns.)

## 6. Recommended Implementation Tasks

(This section will be filled in after the design is finalized.)

### Step 3: Request for Review

Once the draft is generated, ask the human for a review and guide them on how to provide feedback.

### Step 4: Update and Show Diffs

When the user provides feedback, update the document, increment the version number in the revision history, and clearly state what was changed. Repeat until the user confirms the design is final.

### Step 5: Generate the Implementation Task List

Once the user confirms &quot;The design is final,&quot; analyze the entire document and generate a checklist of actionable, ordered implementation tasks in section 6.

Example AI Response:
&quot;Thank you for confirming the design. I have added a 6. Recommended Implementation Tasks section to the design document.
Please copy and paste each task, one by one, to instruct me on implementation. Let&apos;s start with the first task.&quot;

&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Marble to LLM</title><link>https://ainoya.dev/posts/marble-to-llm/</link><guid isPermaLink="true">https://ainoya.dev/posts/marble-to-llm/</guid><description>Generative AI mirrors sculpture—from Michelangelo’s marble to Japanese natabori—helping engineers reveal hidden forms within LLMs.</description><pubDate>Tue, 29 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;From Marble to Model: The AI Sculpting Process&lt;/h2&gt;
&lt;p&gt;Creating something with generative AI often feels a lot like carving a Buddha statue out of a block of wood. Long before “AI” even existed, artists used similar metaphors to describe the act of revealing something precious that already lies hidden within raw material. Today, as engineers and creators, we’re doing much the same thing—only our chisel has become a prompt, and our marble is a massive neural network.&lt;/p&gt;
&lt;h2&gt;The Sculpture Analogy in Western Art&lt;/h2&gt;
&lt;p&gt;One of the most famous stories in Western art comes from Michelangelo. He claimed that the figure he sculpted from a slab of marble was already present inside the rock; his job was simply to remove the excess stone until the statue emerged. This idea—that every block of marble contains a masterpiece waiting to be freed—captures the essence of generative creation. We don’t “make” the knowledge; we uncover it.&lt;/p&gt;
&lt;h2&gt;Wisdom of Japanese Buddhist Sculptors&lt;/h2&gt;
&lt;p&gt;A parallel tradition exists in Japan. During the Edo period, master sculptors like &lt;strong&gt;Enkū&lt;/strong&gt; and &lt;strong&gt;Mokujiki&lt;/strong&gt; practiced &lt;em&gt;natabori&lt;/em&gt; (鉈彫り), a technique of boldly and swiftly carving wooden statues with a single-edged axe. Legend says these artists believed the Buddha already resided within the tree, and their role was to listen to the wood’s voice and reveal its hidden form. This resonates deeply with the “craftsman’s heart”—a focus on intuition, dialogue with material, and unearthing its essential nature.&lt;/p&gt;
&lt;h2&gt;Generative AI as a Modern Chisel&lt;/h2&gt;
&lt;p&gt;For engineers today, coding often means writing lines of text in an IDE. Increasingly, it means guiding a large language model (LLM) to produce those lines for us. In the same way that Michelangelo saw form in marble and Enkū heard Buddha in wood, we see algorithms, functions, and architectures within the vast “weights” of an LLM. Our prompts become chisels, our responses the statues we coax into being.&lt;/p&gt;
&lt;p&gt;Blog writing follows the same pattern: instead of staring at a blank page, we write a prompt; the AI drafts an outline or a paragraph; we polish, refine, and—like any good sculptor—remove what’s unnecessary until the idea stands clear.&lt;/p&gt;
&lt;h2&gt;Finding Your Voice in LLMs&lt;/h2&gt;
&lt;p&gt;The remarkable thing about a block of marble or a chunk of wood is that every piece is unique. Likewise, every engineer’s interaction with an LLM produces a different result. The knowledge embedded in the model is vast, but what emerges depends entirely on your creativity and intent.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Choice of Prompt&lt;/strong&gt;: Just as an axe blow determines the grain you remove, the words you choose shape the AI’s output.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Iterative Refinement&lt;/strong&gt;: Sculptors make many passes; you’ll refine your draft through feedback loops and edits.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Personal Style&lt;/strong&gt;: Enkū’s statues have a rough-hewn immediacy, Michelangelo’s a polished grandeur—your code and writing should reflect your own sensibility.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Accelerating Creative Endeavors&lt;/h2&gt;
&lt;p&gt;By embracing generative AI as our modern tool, we gain the ability to accelerate every stage of creation: ideation, drafting, testing, and deployment. What once might have taken days of boilerplate coding or writer’s block can now be reduced to hours of prompt engineering and iterative polishing.&lt;/p&gt;
&lt;p&gt;Yet the core principle remains unchanged: the masterpiece was always there, waiting to be revealed. Our role is to listen carefully—to the wood, the marble, or the deep neural network—identify the hidden form, and bring it to light.&lt;/p&gt;
&lt;h2&gt;Carrying the Chisel Forward: Next Steps in AI Creation&lt;/h2&gt;
&lt;p&gt;Whether you’re carving wood, sculpting marble, or sculpting text with AI, the process is fundamentally the same: uncover what’s already there. As engineers, we’re privileged to stand on the shoulders of centuries of artistic insight and apply those lessons to code and prose. With each prompt we craft, we sharpen our chisel—and with each iteration, we move one step closer to unveiling the true beauty that lies within.&lt;/p&gt;
&lt;p&gt;Let’s continue harnessing generative AI not as a black-box magician, but as a mindful craftsman—listening, refining, and revealing the creative spark hidden within.&lt;/p&gt;
</content:encoded></item><item><title>A Chrome Extension for Local Transcription Processing</title><link>https://ainoya.dev/posts/chrome-extension-for-edge-transcription/</link><guid isPermaLink="true">https://ainoya.dev/posts/chrome-extension-for-edge-transcription/</guid><description>Developing a secure Chrome extension that transcribes audio locally without uploading data to external servers.</description><pubDate>Wed, 20 Nov 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://ainoya.dev/posts/chrome-extension-for-edge-transcription/images/chrome-extension-web-transcriptor-ai.gif&quot; alt=&quot;Demo&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;This &lt;a href=&quot;https://www.youtube.com/watch?v=Boj9eD0Wug8&quot;&gt;YouTube video&lt;/a&gt; is licensed under CC BY.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;Motivation&lt;/h2&gt;
&lt;p&gt;While many transcription tools exist, most require uploading audio files to the cloud for processing. Due to security compliance, I&apos;ve often encountered situations where uploading recorded audio to external servers isn&apos;t permissible. Tools like Siri and the Web Speech API are tailored for transcribing one&apos;s own voice and aren&apos;t suitable for meetings or conversations involving multiple speakers. Moreover, the Web Speech API sends audio data to Google, and its data usage policy isn&apos;t clearly defined. (If anyone knows where this is documented, please let me know: &lt;a href=&quot;https://policies.google.com/privacy?hl=en#infocollect&quot;&gt;Google Privacy Policy&lt;/a&gt;.)&lt;/p&gt;
&lt;p&gt;To address this, I decided to run a Whisper model locally using &lt;a href=&quot;https://github.com/huggingface/transformers.js&quot;&gt;huggingface/transformers.js&lt;/a&gt; for transcription. I developed a Chrome extension called &lt;a href=&quot;https://github.com/ainoya/chrome-extension-web-transcriptor-ai&quot;&gt;chrome-extension-web-transcriptor-ai&lt;/a&gt;. By utilizing the &lt;code&gt;tabCapture&lt;/code&gt; API, capturing audio playing within a tab becomes straightforward. This approach allows for secure transcription entirely on the local machine without any network communication aside from the initial model download, ensuring that audio data isn&apos;t uploaded to external servers.&lt;/p&gt;
&lt;h2&gt;Challenges Faced&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://ainoya.dev/posts/chrome-extension-for-edge-transcription/images/design.png&quot; alt=&quot;chrome extension implementation&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Initially, I wanted to use the Web Speech API for transcription but found no interface to specify input sources—it seems to accept audio only from input devices like microphones. Since I couldn&apos;t feed the &lt;code&gt;tabCapture&lt;/code&gt; audio into the Web Speech API, I pivoted to using &lt;code&gt;transformers.js&lt;/code&gt;. I plan to dive into the Web Speech API&apos;s implementation when I have time to confirm this limitation.&lt;/p&gt;
&lt;p&gt;Another significant hurdle was that &lt;code&gt;chrome.runtime.sendMessage&lt;/code&gt; can only transmit serializable objects. I struggled with passing audio data because &lt;code&gt;Float32Array&lt;/code&gt; isn&apos;t serializable. Without realizing this, I initially tried to pass it directly, which didn&apos;t work. I ended up converting the &lt;code&gt;Float32Array&lt;/code&gt; to a string using &lt;code&gt;JSON.stringify&lt;/code&gt;, but this approach isn&apos;t efficient. I&apos;m looking into better serialization methods to improve performance.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;I&apos;m using the &lt;a href=&quot;https://huggingface.co/onnx-community/whisper-large-v3-turbo&quot;&gt;onnx-community/whisper-large-v3-turbo&lt;/a&gt; model for Whisper. Testing so far has been on a MacBook Pro with an M2 Pro chip (32GB Memory), and it&apos;s capable of high-precision transcription in multiple languages. It&apos;s impressive that such a powerful model can now run on local devices.&lt;/p&gt;
&lt;p&gt;With &lt;code&gt;transformers.js&lt;/code&gt;, it&apos;s possible to run not just Whisper but also large language models (LLMs). I&apos;m considering adding a feature to summarize transcribed content using LLMs. For subsequent processing, integrating with tools like Gemini in Chrome could be beneficial.&lt;/p&gt;
&lt;p&gt;While the model download does involve network communication, I&apos;m thinking about implementing a feature to allow downloads from trusted servers to enhance security.&lt;/p&gt;
</content:encoded></item><item><title>Handling Long Responses with VertexAI API</title><link>https://ainoya.dev/posts/vertex-ai-retrieve-continuation/</link><guid isPermaLink="true">https://ainoya.dev/posts/vertex-ai-retrieve-continuation/</guid><description>Learn how to handle truncated responses when processing large contexts with VertexAI API, using a recursive method to retrieve complete outputs.</description><pubDate>Mon, 16 Sep 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;When using generative AI to process large contexts, such as PDFs, the output may sometimes be cut off. This issue is especially common when the generated text is long. In this post, I will explain how to handle this problem using VertexAI, specifically through recursive API calls to obtain the full response.&lt;/p&gt;
&lt;h2&gt;Using &quot;Continue&quot; in Chat UIs&lt;/h2&gt;
&lt;p&gt;In a chat UI with VertexAI, if the output stops prematurely, you can type &quot;continue&quot; and the AI will pick up where it left off. However, when using API calls, you don&apos;t have this interactive capability, so a different approach is needed to handle this issue.&lt;/p&gt;
&lt;h2&gt;Handling the Issue with API Calls&lt;/h2&gt;
&lt;p&gt;To address this problem in API calls, you need to monitor the &lt;code&gt;finishReason&lt;/code&gt; in the response. If the output is cut off, you can check this value and reissue the generation request if necessary. Below is a step-by-step guide on how to handle it.&lt;/p&gt;
&lt;h3&gt;1. Checking the Response&lt;/h3&gt;
&lt;p&gt;Within the API response, there is a &lt;code&gt;finishReason&lt;/code&gt; that indicates why the generation stopped. You need to check this value to determine whether to send another generation request or not.&lt;/p&gt;
&lt;p&gt;Here’s a reference from Google Cloud&apos;s &lt;a href=&quot;https://cloud.google.com/vertex-ai/generative-ai/docs/reference/python/1.59.0/services&quot;&gt;documentation on VertexAI&lt;/a&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;FINISH_REASON_UNSPECIFIED (0):

The finish reason is unspecified.

STOP (1):

Natural stop point of the model or provided
stop sequence.

MAX_TOKENS (2):

The maximum number of tokens as specified in
the request was reached.
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. Retaining Context&lt;/h3&gt;
&lt;p&gt;When reissuing a generation request, it is important to include the context of the conversation, such as previous outputs or input. If this context is not included, the AI may generate text that is unrelated to the previous output.&lt;/p&gt;
&lt;h3&gt;3. Managing Token Count&lt;/h3&gt;
&lt;p&gt;One key point to watch out for is that recursive generation requests will increase the number of input tokens. If the token count becomes too high, it can slow down processing or even hit the token limit. Therefore, it&apos;s important to manage the context carefully, trimming unnecessary parts when possible.&lt;/p&gt;
&lt;h2&gt;Code Example&lt;/h2&gt;
&lt;p&gt;Here is a concrete example from a GitHub repository that implements a mechanism to recursively retrieve responses when the output is cut off.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ainoya/PocketMD/blob/main/src/describe_pdf.ts#L66C15-L98C4&quot;&gt;Code Example&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// https://github.com/ainoya/PocketMD/blob/bd46bf40d83c77bba24f5c46aa64d97e07057f82/src/describe_pdf.ts#L66C15-L98C4

// Define a constant for the word to request continuation of content generation.
// This word is taken from the environment variable `CONTINUE_WORD`, and defaults to &quot;Please continue&quot; if not set.
const continueWord = process.env.CONTINUE_WORD || &quot;Please continue&quot;;

// Set the maximum number of loops to prevent infinite looping during content generation.
const maxLoopCount = 10;
let loopCount = 0;

// Infinite loop to keep generating content until the stop conditions are met.
while (true) {
  // If the loop count exceeds the maximum allowed iterations, break the loop to avoid excessive processing.
  if (loopCount &amp;gt;= maxLoopCount) {
    console.log(&quot;Max loop count reached&quot;);
    break;
  }

  // Increment the loop counter on each iteration.
  loopCount++;

  // Log to indicate the content generation process has started.
  console.log(&quot;Generating content...&quot;);

  // Send a request to the API client to generate content based on the existing conversation context.
  const generated = await client.generateContent({
    contents: contents,
  });

  // Extract the content from the response, assuming it&apos;s in the first candidate.
  const content = generated.response.candidates?.at(0)?.content;

  // If content is successfully generated, add it to the existing conversation context.
  if (content) {
    contents.push(content); // Add the generated content to the conversation context.

    // Add a user input asking the model to continue generating content, using the defined continue word.
    contents.push({ role: &quot;user&quot;, parts: [{ text: continueWord }] });
  } else {
    // If no content was generated, log a message and break the loop.
    console.log(&quot;No content generated&quot;);
    break;
  }

  // Check the reason why the generation stopped (e.g., reaching token limit, completion).
  const finishReason = generated.response.candidates?.at(0)?.finishReason;

  // If the reason for stopping is not due to reaching the maximum token limit, break the loop.
  // This means the content is considered &quot;finished&quot; or some other stop condition has occurred.
  if (finishReason !== &quot;MAX_TOKENS&quot;) {
    console.log(&quot;Finish reason:&quot;, generated.response.candidates?.at(0)?.finishReason);
    break;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Key Considerations&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Context Retention&lt;/strong&gt;: When reissuing a generation request, always include the previous conversation history. If you fail to do this, the AI may not generate the continuation you expect.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Token Management&lt;/strong&gt;: Be mindful of the token count when recursively generating responses. To avoid hitting limits, trim unnecessary parts of the conversation history when possible.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;By carefully monitoring the &lt;code&gt;finishReason&lt;/code&gt; and managing the context and token count, you can effectively handle the issue of truncated responses in API-based content generation.&lt;/p&gt;
</content:encoded></item><item><title>Experiment with LLM for Personal Knowledge Management</title><link>https://ainoya.dev/posts/try-llm-personal-knowledge-management-weekly-review/</link><guid isPermaLink="true">https://ainoya.dev/posts/try-llm-personal-knowledge-management-weekly-review/</guid><description>Exploring the use of Large Language Models (LLMs) for personal knowledge management, focusing on the challenges and potential solutions for improving output quality and automation.</description><pubDate>Sun, 15 Sep 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Recently, I&apos;ve been experimenting with using Large Language Models (LLMs) for personal knowledge management. However, I&apos;ve found that even when I feed information into an LLM, the output often falls within my own limited range of imagination. On the other hand, LLMs excel at summarizing and organizing diverse information compared to humans. My goal is to leverage these strengths to enhance my intellectual productivity.&lt;/p&gt;
&lt;h2&gt;Approach Overview&lt;/h2&gt;
&lt;h3&gt;1. Human-Driven Information Dump&lt;/h3&gt;
&lt;p&gt;First, I write down all the miscellaneous information I acquire into notes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Record thoughts and ideas in a &lt;strong&gt;Daily Journal&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Save interesting web articles and resources.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2. LLM-Driven Information Organization&lt;/h3&gt;
&lt;p&gt;Next, I use an LLM to organize and summarize this information:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Generate summarized files.&lt;/li&gt;
&lt;li&gt;Implement LLM-based tagging.&lt;/li&gt;
&lt;li&gt;Enclose important keywords with links.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I believe that the interplay between these two steps can lead to more efficient intellectual activity.&lt;/p&gt;
&lt;h2&gt;Tried Methods&lt;/h2&gt;
&lt;p&gt;Previously, I experimented with tagging. The knowledge management technique of linking ideas is called &lt;a href=&quot;https://www.linkingyourthinking.com/&quot;&gt;Linking Your Thinking (LYT)&lt;/a&gt;. This time, I decided to try a prompt that more organically summarizes weekly notes and organizes next actions. I&apos;m using OpenAI&apos;s &lt;strong&gt;o1-preview&lt;/strong&gt; model, which has more advanced reasoning capabilities.&lt;/p&gt;
&lt;h3&gt;Prompt Used&lt;/h3&gt;
&lt;p&gt;Here is the prompt I actually used:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;You are a research assistant for personal knowledge management. You will search a directory containing notes. Summarize the results and consider and suggest the next actions to develop the thinking process.

Consider original and novel research topics. Limit the topics to the field of computer science. For each topic, develop a detailed research plan, including hypotheses, verification methods, expected conclusions, and how it differs from existing research.

In addition, by looking back at the content, please tell me the knowledge gaps that I am currently lacking.
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Search Command and Results&lt;/h3&gt;
&lt;p&gt;I used the following command to search for notes:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;find . -type f -name &quot;*.md&quot; -mtime -7 -exec sh -c &apos;for file; do echo &quot;&amp;lt;File path=\&quot;$file\&quot;&amp;gt;\n&quot;; cat &quot;$file&quot;; echo &quot;\n&amp;lt;/File&amp;gt;&quot;; done&apos; _ {} +
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Results and Challenges&lt;/h2&gt;
&lt;p&gt;When I actually ran the prompt, I was not satisfied with the output, and it has not yet reached the point of automation. However, by asking various additional questions, I am using it to help with weekly reviews and introspection.&lt;/p&gt;
&lt;h3&gt;Challenges&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Output Quality&lt;/strong&gt;: The results obtained from the LLM have not met my expectations.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Automation Difficulty&lt;/strong&gt;: I have not been able to establish an automated process because I cannot get satisfactory results.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;LLM-powered knowledge management shows promise, but there are still many challenges at this stage. In particular, further consideration needs to be given to prompt design to improve the quality of output and how to maximize the capabilities of LLMs. I will continue to experiment and try to establish more effective knowledge management methods.&lt;/p&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.arxiv.org/abs/2409.04109&quot;&gt;[2409.04109] Can LLMs Generate Novel Research Ideas? A Large-Scale Human Study with 100+ NLP Researchers&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;This paper investigates whether LLMs can generate novel research ideas by comparing research ideas generated by LLMs and human NLP researchers. The study found that LLM-generated ideas were judged to be more novel than those of human experts, while being judged as slightly less feasible. To generate ideas at scale, the paper employed an approach where the LLM generated 4000 ideas for a single research topic. This &quot;large-scale generation&quot; aimed to produce a larger pool of candidate ideas from which high-quality ideas could be selected.&lt;/p&gt;
&lt;p&gt;However, it was found that only about 5% (200) of the 4000 ideas were truly unique and non-duplicate. As the generation process continued, the number of duplicate ideas increased, and the number of new ideas decreased. This demonstrates that large-scale generation has limitations as the diversity of ideas gradually diminishes.&lt;/p&gt;
&lt;p&gt;IMO: It&apos;s interesting that 200 unique ideas were generated.&lt;/p&gt;
</content:encoded></item><item><title>Using Custom Key Material with KMS in LocalStack</title><link>https://ainoya.dev/posts/localstack-kms-with-seed/</link><guid isPermaLink="true">https://ainoya.dev/posts/localstack-kms-with-seed/</guid><description>Learn how to set up custom key material when creating KMS keys in LocalStack, simplifying key preparation and enhancing team collaboration.</description><pubDate>Sat, 14 Sep 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;In development projects that utilize AWS components, &lt;strong&gt;LocalStack&lt;/strong&gt; is an incredibly handy tool. By running a single LocalStack container, you can emulate various AWS services, including &lt;strong&gt;KMS (Key Management Service)&lt;/strong&gt;, which is particularly useful.&lt;/p&gt;
&lt;p&gt;In this post, I&apos;ll walk you through how to set up custom key material when creating KMS keys in LocalStack. This approach simplifies key preparation and is especially beneficial when sharing encrypted test data in a team environment.&lt;/p&gt;
&lt;h2&gt;Background&lt;/h2&gt;
&lt;p&gt;When storing sensitive customer information—like API keys for services they own—encrypting this data with KMS is a common practice. However, in a team development setting, a problem arises: each team member&apos;s KMS initializes with different key material, making it impossible to decrypt each other&apos;s encrypted test data.&lt;/p&gt;
&lt;p&gt;One way to address this is by importing key material using &lt;strong&gt;&lt;code&gt;aws kms import-key-material&lt;/code&gt;&lt;/strong&gt;, but this process can be a bit cumbersome. Recently, it became possible to create keys with custom key material directly using &lt;strong&gt;&lt;code&gt;awslocal kms create-key&lt;/code&gt;&lt;/strong&gt; in LocalStack. This new method streamlines the key preparation steps and improves development efficiency.&lt;/p&gt;
&lt;h2&gt;Step-by-Step Guide&lt;/h2&gt;
&lt;p&gt;Here&apos;s how you can set up custom key material when creating a KMS key in LocalStack. I recommend fixing the &lt;code&gt;keyId&lt;/code&gt; for easier handling in scripts.&lt;/p&gt;
&lt;h3&gt;1. Set Up Custom Key Material and Key ID&lt;/h3&gt;
&lt;p&gt;Generate a 32-byte random key material and define a fixed key ID:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CUSTOM_KEY_MATERIAL=&quot;$(openssl rand -base64 32)&quot;
CUSTOM_ID=&quot;00000000-0000-0000-0000-000000000001&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;CUSTOM_KEY_MATERIAL&lt;/code&gt;&lt;/strong&gt;: Generates a random 32-byte key material.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;CUSTOM_ID&lt;/code&gt;&lt;/strong&gt;: Sets a fixed key ID.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2. Create the Key&lt;/h3&gt;
&lt;p&gt;Use the &lt;code&gt;awslocal&lt;/code&gt; command to create a key with custom key material:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;awslocal kms create-key --tags &quot;[{\&quot;TagKey\&quot;:\&quot;_custom_key_material_\&quot;,\&quot;TagValue\&quot;:\&quot;$CUSTOM_KEY_MATERIAL\&quot;},{\&quot;TagKey\&quot;:\&quot;_custom_id_\&quot;,\&quot;TagValue\&quot;:\&quot;$CUSTOM_ID\&quot;}]&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;This command creates a new KMS key with the custom key material and key ID specified in the tags.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Important Notes&lt;/h2&gt;
&lt;h3&gt;Key Material Size&lt;/h3&gt;
&lt;p&gt;The key material must be &lt;strong&gt;32 bytes&lt;/strong&gt;. If it&apos;s not, you&apos;ll encounter an error like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;## This will result in an error

CUSTOM_KEY_MATERIAL=&quot;invalid material&quot;
CUSTOM_ID=&quot;00000000-0000-0000-0000-000000000001&quot;

awslocal kms create-key --tags &quot;[{\&quot;TagKey\&quot;:\&quot;_custom_key_material_\&quot;,\&quot;TagValue\&quot;:\&quot;$CUSTOM_KEY_MATERIAL\&quot;},{\&quot;TagKey\&quot;:\&quot;_custom_id_\&quot;,\&quot;TagValue\&quot;:\&quot;$CUSTOM_ID\&quot;}]&quot; --cli-binary-format raw-in-base64-out

## Error Message
An error occurred (InternalError) when calling the CreateKey operation (reached max retries: 2): exception while calling kms.CreateKey: Incorrect padding
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Key Material Format&lt;/h3&gt;
&lt;p&gt;Ensure the key material is in &lt;strong&gt;Base64-encoded&lt;/strong&gt; format. If you use a hex string, you&apos;ll face errors during encryption:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;## This will also result in an error

CUSTOM_KEY_MATERIAL=&quot;$(openssl rand -hex 32)&quot;
CUSTOM_ID=&quot;00000000-0000-0000-0000-000000000001&quot;

awslocal kms create-key --tags &quot;[{\&quot;TagKey\&quot;:\&quot;_custom_key_material_\&quot;,\&quot;TagValue\&quot;:\&quot;$CUSTOM_KEY_MATERIAL\&quot;},{\&quot;TagKey\&quot;:\&quot;_custom_id_\&quot;,\&quot;TagValue\&quot;:\&quot;$CUSTOM_ID\&quot;}]&quot; --cli-binary-format raw-in-base64-out

awslocal kms encrypt --key-id $CUSTOM_ID --plaintext supersecret --output text --query CiphertextBlob --cli-binary-format raw-in-base64-out

## Error Message
An error occurred (InternalError) when calling the Encrypt operation (reached max retries: 2): exception while calling kms.Encrypt: Invalid key size (384) for AES.
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;AWS CLI Option&lt;/h3&gt;
&lt;p&gt;If you&apos;re using &lt;strong&gt;AWS CLI v2&lt;/strong&gt;, you&apos;ll need to specify the &lt;code&gt;--cli-binary-format raw-in-base64-out&lt;/code&gt; option; otherwise, you&apos;ll get an error like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;## This will result in an error

awslocal kms encrypt --key-id $CUSTOM_ID --plaintext supersecret --output text --query CiphertextBlob

## Error Message
Invalid base64: &quot;supersecret&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Refer to the &lt;a href=&quot;https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-options.html&quot;&gt;AWS CLI options documentation&lt;/a&gt; for more details.&lt;/p&gt;
&lt;h2&gt;Testing Encryption and Decryption&lt;/h2&gt;
&lt;h3&gt;Encrypt Data&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;PLAINTEXT=&quot;supersecret&quot;

CIPHERTEXT=$(awslocal kms encrypt --key-id $CUSTOM_ID --plaintext &quot;$PLAINTEXT&quot; --output text --query CiphertextBlob --cli-binary-format raw-in-base64-out)

echo &quot;Ciphertext: $CIPHERTEXT&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Decrypt Data&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;DECRYPTED_TEXT=$(awslocal kms decrypt --ciphertext-blob fileb://&amp;lt;(echo &quot;$CIPHERTEXT&quot; | base64 -d) --output text --query Plaintext --cli-binary-format raw-in-base64-out | base64 -d)

echo &quot;Decrypted text: $DECRYPTED_TEXT&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Expected Output&lt;/strong&gt;: &lt;code&gt;Decrypted text: supersecret&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;By using custom key material when creating KMS keys in LocalStack, you can simplify key preparation and make it easier to share encrypted test data within your team.&lt;/p&gt;
&lt;h3&gt;Key Takeaways&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;Key Material Format: The key material must conform to the allowed data key byte sizes.&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;Specifies the length of the data key in bytes. For example, use the value 64 to generate a 512-bit data key (64 bytes is 512 bits). For 128-bit (16-byte) and 256-bit (32-byte) data keys, use the KeySpec parameter.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/kms/client/generate_data_key.html&quot;&gt;generate_data_key - Boto3 1.35.24 documentation&lt;/a&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;When using AWS CLI v2, always include the &lt;code&gt;--cli-binary-format raw-in-base64-out&lt;/code&gt; option.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Fixing the &lt;code&gt;keyId&lt;/code&gt; makes scripting and automation more straightforward.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.localstack.cloud/user-guide/aws/kms/&quot;&gt;Custom Key Material for KMS Keys via Tags&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-options.html&quot;&gt;Command Line Options - AWS CLI&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Why Still Blog in the Age of Generative AI?</title><link>https://ainoya.dev/posts/why-you-are-still-blogging/</link><guid isPermaLink="true">https://ainoya.dev/posts/why-you-are-still-blogging/</guid><description>Generative AI is changing the information landscape, but blogging still holds value as a platform for personal expression and creative exploration.</description><pubDate>Wed, 04 Sep 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;The Rise of Generative AI and the Information Explosion&lt;/h2&gt;
&lt;p&gt;The advent of generative AI has ushered in an era of unprecedented information output. This impact is particularly evident in the academic world, where the number of papers published on platforms like arXiv is skyrocketing.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://ainoya.dev/posts/why-you-are-still-blogging/images/arxiv-monthly-submission.png&quot; alt=&quot;arxive monthly submissions&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://arxiv.org/stats/monthly_submissions&quot;&gt;Monthly Submissions&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The graph above illustrates the monthly submission count to arXiv. The red line represents an exponential approximation, hinting at the continued growth of submissions in the future.&lt;/p&gt;
&lt;p&gt;An interesting example is the recent surge in the use of the word &quot;delve&quot; in academic papers, a term not commonly used in everyday language.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://ainoya.dev/posts/why-you-are-still-blogging/images/delve-freq.png&quot; alt=&quot;delve usage in academic papers&quot; /&gt;&lt;/p&gt;
&lt;p&gt;This phenomenon is attributed to the tendency of LLMs like ChatGPT to generate this word frequently.
(&lt;a href=&quot;https://arxiv.org/abs/2406.07016&quot;&gt;[2406.07016] Delving into ChatGPT usage in academic writing through excess vocabulary&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;Evidence suggests that ChatGPT is being used to assist in writing papers submitted to arXiv. This increased productivity through LLM-powered writing is expected to further boost the number of future publications.&lt;/p&gt;
&lt;h2&gt;ChatGPT and Information Access&lt;/h2&gt;
&lt;p&gt;Even for technical information, ChatGPT can provide high-quality answers without the annoyance of intrusive ads. If the information doesn&apos;t exist directly on the internet but can be interpolated from existing data, ChatGPT can likely generate it for you.&lt;/p&gt;
&lt;h2&gt;Why Bother Blogging Then?&lt;/h2&gt;
&lt;p&gt;Given this context, what is the point of writing technical blogs?&lt;/p&gt;
&lt;p&gt;Even extrapolative thinking, the ability to generate new ideas beyond existing information, could potentially be replaced by AI agents. Imagine a scenario: &quot;encounter a bug → search on Perplexity → fix the bug → report the fix.&quot;  This entire process could be automated by AI agents in the future. For example: &lt;a href=&quot;https://githubnext.com/projects/copilot-workspace&quot;&gt;GitHub Next | Copilot Workspace&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;If we focus solely on doing things that only humans can do, we might eventually find ourselves replaced by AI in every domain. Instead, we should view &lt;strong&gt;blogging as a means of personal expression, akin to poetry or art&lt;/strong&gt;.
The goal shouldn&apos;t be to compete with AI, but rather to &lt;strong&gt;leverage blogging as a tool for expressing our unique thoughts and perspectives.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;Co-creation with AI: A New Era of Blogging&lt;/h2&gt;
&lt;p&gt;There&apos;s no need to reject the use of AI as a tool for output. The crucial aspect is &lt;strong&gt;how we choose to curate and edit the information&lt;/strong&gt;. This process is driven by our individual experiences, knowledge, and perspectives, stored within our neural networks – something AI cannot replicate.&lt;/p&gt;
&lt;p&gt;Generative AI can be seen as a tool to augment our abilities and accelerate our creative process. By viewing generative AI not as a technology that disrupts us, but as a &lt;strong&gt;partner that enhances our creativity&lt;/strong&gt;, we can unlock new possibilities for blog writing.&lt;/p&gt;
&lt;p&gt;Through Human-in-the-Loop interactions, where humans and LLMs engage in a creative dialogue, we can craft more unique and engaging blog posts. For instance, we can leverage LLMs to summarize information, suggest article structures, and streamline the writing process, ultimately leading to higher-quality output.&lt;/p&gt;
&lt;h2&gt;The Evolving Value of Blogs&lt;/h2&gt;
&lt;p&gt;So, what specific value can we find in blogging today?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Personal Branding&lt;/strong&gt;: Clarifying your expertise and values and sharing them with others can enhance your professional performance.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Sharing Extrapolative Thoughts&lt;/strong&gt;: Blogs can serve as a platform for expressing intuitive insights and creative ideas that go beyond the capabilities of high-temperature generative AI settings.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Accelerating Creative Output&lt;/strong&gt;: By embracing generative AI as a tool for augmenting our abilities, we can leverage &lt;a href=&quot;https://en.wikipedia.org/wiki/Human-in-the-loop&quot;&gt;Human-in-the-Loop&lt;/a&gt; interactions with LLMs to create distinctive and captivating blog posts.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;The advancements in generative AI are remarkable, and they hold the potential to replace human activities in various fields. However, blogging retains its inherent value as a &lt;strong&gt;platform for human expression and creativity&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;By utilizing generative AI as a tool to organize and articulate our thoughts, we can unlock new possibilities and continue to find meaning in the art of blogging.&lt;/p&gt;
</content:encoded></item><item><title>Developing a Web UI for Controlling LLM JSON Output</title><link>https://ainoya.dev/posts/llm-json-output-format-gen/</link><guid isPermaLink="true">https://ainoya.dev/posts/llm-json-output-format-gen/</guid><description>Building a user-friendly Web UI to easily structure and control LLM outputs in JSON format for better integration with applications.</description><pubDate>Sun, 01 Sep 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Developing a Web UI for Controlling LLM JSON Output&lt;/h2&gt;
&lt;h3&gt;Introduction&lt;/h3&gt;
&lt;p&gt;Controlling LLM (Large Language Model) output to get the desired results often requires meticulous specification of output formats and content. This is especially crucial when integrating LLM outputs into other systems, where JSON format control becomes essential.&lt;/p&gt;
&lt;p&gt;While Gemini and OpenAI APIs allow specifying JSON schema as output options, existing JSON schema builders often have a steep learning curve due to their ability to handle all possible structures. This article introduces a Web UI that simplifies the process by applying constraints to the JSON schema output format, enabling intuitive schema building through a user-friendly GUI.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://ainoya.github.io/llm-output-format-gen/?items=%5B%7B%22key%22%3A%22summary%22%2C%22type%22%3A%22text%22%2C%22description%22%3A%22summarize+following+text.%22%7D%2C%7B%22key%22%3A%22tags%22%2C%22type%22%3A%22multipleSelect%22%2C%22description%22%3A%22Topics+of+following+text.%22%2C%22options%22%3A%5B%22sport%22%2C%22technology%22%5D%7D%5D&amp;amp;topLevelKey=result&quot;&gt;JSON Schema generator for LLM JSON formattted output&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;OpenAI&apos;s JSON Schema Option&lt;/h3&gt;
&lt;p&gt;Many LLM APIs, including OpenAI&apos;s, offer JSON schema options to control output in JSON format. JSON schema is a format for defining the structure of JSON data, using keywords like &lt;code&gt;type&lt;/code&gt;, &lt;code&gt;properties&lt;/code&gt;, and &lt;code&gt;required&lt;/code&gt; to specify data types, properties, and mandatory fields.&lt;/p&gt;
&lt;p&gt;For instance, you can specify the following JSON schema to output information about a person, including their name and age, in JSON format:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;type&quot;: &quot;object&quot;,
  &quot;properties&quot;: {
    &quot;name&quot;: {
      &quot;type&quot;: &quot;string&quot;
    },
    &quot;age&quot;: {
      &quot;type&quot;: &quot;integer&quot;
    }
  },
  &quot;required&quot;: [&quot;name&quot;, &quot;age&quot;]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;However, manually writing JSON schema can be quite tedious, especially when dealing with complex data structures.&lt;/p&gt;
&lt;h3&gt;Building JSON Schema with a Web UI&lt;/h3&gt;
&lt;p&gt;The developed Web UI, implemented using React, features a GUI that allows for intuitive JSON schema building through drag-and-drop operations. Users can easily create a JSON schema by arranging the necessary elements on the GUI and configuring their properties.&lt;/p&gt;
&lt;h3&gt;Benefits of the Web UI&lt;/h3&gt;
&lt;p&gt;This Web UI offers several advantages:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Schema Definition without LLM:&lt;/strong&gt; No need to rely on LLMs for schema generation; developers can design schemas according to their specific needs.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Intuitive Schema Building:&lt;/strong&gt; The GUI-based operation simplifies schema building, even for users unfamiliar with JSON schema.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Easy Adjustments:&lt;/strong&gt; The schema can be easily edited on the GUI, allowing for quick adjustments and modifications.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Support for Multiple LLMs:&lt;/strong&gt;  Compatible with JSON schema options for various LLMs, including OpenAI and Gemini.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Schema Sharing:&lt;/strong&gt;  Created schemas can be shared easily via URL.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Web UI Use Cases&lt;/h3&gt;
&lt;p&gt;The Web UI can be applied to various use cases. Here are a few examples:&lt;/p&gt;
&lt;p&gt;You can freely experiment with the UI at &lt;a href=&quot;https://ainoya.github.io/llm-output-format-gen&quot;&gt;https://ainoya.github.io/llm-output-format-gen&lt;/a&gt;.  NOTE:  Operations are implemented only on the front-end to prevent sending and recording data to the server.&lt;/p&gt;
&lt;p&gt;Let&apos;s consider a task that involves summarizing an article and simultaneously assigning topics to classify it. We&apos;ll define the JSON output structure using the GUI.&lt;/p&gt;
&lt;p&gt;You can configure three output types:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Text&lt;/code&gt;: Text format, used for outputting summaries, for example.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Single Select&lt;/code&gt;: Single selection format, used for classifying content, for example.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Multi Select&lt;/code&gt;: Multiple selection format, used for assigning topics, for example.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The following screenshot shows an example:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://ainoya.dev/posts/llm-json-output-format-gen/images/schemadefinition.png&quot; alt=&quot;schema definition&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Clicking the &lt;code&gt;Generate Schema&lt;/code&gt; button outputs the JSON schema:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://ainoya.dev/posts/llm-json-output-format-gen/images/generatedschema.png&quot; alt=&quot;generate schema&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Copy the schema to the clipboard using &lt;code&gt;Copy to clipboard&lt;/code&gt; and let&apos;s create a prompt.&lt;/p&gt;
&lt;p&gt;Prompt example:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;please read the text and output with following a JSON schema:

# Text

&amp;lt;Text&amp;gt;
...copy and paste from an article in https://ainoya-dev.pages.dev/posts/llm-application-security-practice-markdown-img-tag/
&amp;lt;/Text&amp;gt;

# Output Schema

```json
{
  &quot;type&quot;: &quot;object&quot;,
  &quot;additionalProperties&quot;: false,
  &quot;properties&quot;: {
    &quot;result&quot;: {
      &quot;type&quot;: &quot;object&quot;,
      &quot;additionalProperties&quot;: false,
      &quot;properties&quot;: {
        &quot;items&quot;: {
          &quot;type&quot;: &quot;array&quot;,
          &quot;additionalProperties&quot;: false,
          &quot;items&quot;: {
            &quot;oneOf&quot;: [
              {
                &quot;type&quot;: &quot;object&quot;,
                &quot;additionalProperties&quot;: false,
                &quot;properties&quot;: {
                  &quot;type&quot;: {
                    &quot;const&quot;: &quot;text&quot;
                  },
                  &quot;key&quot;: {
                    &quot;const&quot;: &quot;summary&quot;
                  },
                  &quot;description&quot;: {
                    &quot;const&quot;: &quot;summarize following text.&quot;
                  },
                  &quot;payload&quot;: {
                    &quot;type&quot;: &quot;object&quot;,
                    &quot;properties&quot;: {
                      &quot;value&quot;: {
                        &quot;type&quot;: &quot;string&quot;
                      }
                    },
                    &quot;required&quot;: [
                      &quot;value&quot;
                    ]
                  }
                },
                &quot;required&quot;: [
                  &quot;type&quot;,
                  &quot;key&quot;,
                  &quot;description&quot;,
                  &quot;payload&quot;
                ]
              },
              {
                &quot;type&quot;: &quot;object&quot;,
                &quot;additionalProperties&quot;: false,
                &quot;properties&quot;: {
                  &quot;type&quot;: {
                    &quot;const&quot;: &quot;multipleSelect&quot;
                  },
                  &quot;key&quot;: {
                    &quot;const&quot;: &quot;tags&quot;
                  },
                  &quot;description&quot;: {
                    &quot;const&quot;: &quot;Topics of following text.&quot;
                  },
                  &quot;payload&quot;: {
                    &quot;type&quot;: &quot;object&quot;,
                    &quot;properties&quot;: {
                      &quot;value&quot;: {
                        &quot;type&quot;: &quot;array&quot;,
                        &quot;items&quot;: {
                          &quot;type&quot;: &quot;string&quot;,
                          &quot;enum&quot;: [
                            &quot;sport&quot;,
                            &quot;technology&quot;
                          ]
                        }
                      }
                    },
                    &quot;required&quot;: [
                      &quot;value&quot;
                    ]
                  }
                },
                &quot;required&quot;: [
                  &quot;type&quot;,
                  &quot;key&quot;,
                  &quot;description&quot;,
                  &quot;payload&quot;
                ]
              }
            ]
          }
        }
      },
      &quot;required&quot;: [
        &quot;items&quot;
      ]
    }
  },
  &quot;required&quot;: [
    &quot;result&quot;
  ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With GPT-4, this prompt generates the following result, formatted for use with the &lt;a href=&quot;https://en.wikipedia.org/wiki/Entity%E2%80%93attribute%E2%80%93value_model&quot;&gt;EAV pattern&lt;/a&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;result&quot;: {
    &quot;items&quot;: [
      {
        &quot;type&quot;: &quot;text&quot;,
        &quot;key&quot;: &quot;summary&quot;,
        &quot;description&quot;: &quot;summarize following text.&quot;,
        &quot;payload&quot;: {
          &quot;value&quot;: &quot;The article discusses the potential security risks of rendering markdown images in LLM (Large Language Model) applications. It highlights how embedding images using markdown can expose sensitive data through image URLs, which may be exploited by malicious actors. The article provides methods to mitigate these risks, such as disabling image embedding entirely or whitelisting trusted hosts. Example TypeScript code is provided to demonstrate these approaches. The importance of implementing robust security measures, especially in applications dealing with user-generated content (UGC), is emphasized.&quot;
        }
      },
      {
        &quot;type&quot;: &quot;multipleSelect&quot;,
        &quot;key&quot;: &quot;tags&quot;,
        &quot;description&quot;: &quot;Topics of following text.&quot;,
        &quot;payload&quot;: {
          &quot;value&quot;: [&quot;technology&quot;]
        }
      }
    ]
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The definition used in this example can be shared as a URL using the &lt;code&gt;Share URL&lt;/code&gt; feature. Here&apos;s the &lt;a href=&quot;https://ainoya.github.io/llm-output-format-gen/?items=%5B%7B%22key%22%3A%22summary%22%2C%22type%22%3A%22text%22%2C%22description%22%3A%22summarize+following+text.%22%7D%2C%7B%22key%22%3A%22tags%22%2C%22type%22%3A%22multipleSelect%22%2C%22description%22%3A%22Topics+of+following+text.%22%2C%22options%22%3A%5B%22sport%22%2C%22technology%22%5D%7D%5D&amp;amp;topLevelKey=result&quot;&gt;URL&lt;/a&gt; for the schema shown in the example.&lt;/p&gt;
&lt;h2&gt;Future Development&lt;/h2&gt;
&lt;p&gt;The Web UI is still under development, and the following tasks remain:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Support for OpenAI&apos;s Structured Outputs:&lt;/strong&gt; Currently, the UI doesn&apos;t support specifying options for OpenAI&apos;s &lt;a href=&quot;https://platform.openai.com/docs/guides/structured-outputs&quot;&gt;Structured Outputs&lt;/a&gt; because structured output doesn&apos;t support the &lt;code&gt;oneOf&lt;/code&gt; notation in JSON schema. It also doesn&apos;t support the schema specification option for &lt;a href=&quot;https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/control-generated-output?hl=en&quot;&gt;Gemini (Vertex AI)&lt;/a&gt;. Only JSON output specification is possible via API options; schemas are used by directly inputting them into the prompt.&lt;/p&gt;
&lt;h2&gt;Caveats&lt;/h2&gt;
&lt;p&gt;There are reports that forcing complex JSON outputs or structured outputs on LLMs can degrade output quality. In simple tasks, based on my observations, the impact of quality degradation was minimal.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://arxiv.org/abs/2408.02442v1&quot;&gt;[2408.02442v1] Let Me Speak Freely? A Study on the Impact of Format Restrictions on Performance of Large Language Models&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://aider.chat/2024/08/14/code-in-json.html&quot;&gt;LLMs are bad at returning code in JSON | aider&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;This article introduced a Web UI developed for controlling LLM JSON output. This UI simplifies the process of controlling LLM outputs in JSON format, making it easier to integrate them into various systems.&lt;/p&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://ainoya.github.io/llm-output-format-gen/?items=%5B%7B%22key%22%3A%22summary%22%2C%22type%22%3A%22text%22%2C%22description%22%3A%22summarize+following+text.%22%7D%2C%7B%22key%22%3A%22tags%22%2C%22type%22%3A%22multipleSelect%22%2C%22description%22%3A%22Topics+of+following+text.%22%2C%22options%22%3A%5B%22sport%22%2C%22technology%22%5D%7D%5D&amp;amp;topLevelKey=result&quot;&gt;JSON Schema generator for LLM JSON formattted output&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/ainoya/llm-output-format-gen&quot;&gt;ainoya/llm-output-format-gen&lt;/a&gt;: Source code&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://bjdash.github.io/JSON-Schema-Builder/&quot;&gt;JSON Schema Builder&lt;/a&gt;: This is very useful for building various JSON schemas&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://platform.openai.com/docs/guides/structured-outputs&quot;&gt;Structured Outputs - OpenAI API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/control-generated-output?hl=en&quot;&gt;Control generated output  |  Generative AI on Vertex AI  |  Google Cloud&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Day 0 Operations: Minimizing Risks in Manual Processes for New Products</title><link>https://ainoya.dev/posts/day-0-web-ops/</link><guid isPermaLink="true">https://ainoya.dev/posts/day-0-web-ops/</guid><description>Learn how to minimize risks when manually operating new products before full automation is in place. This post discusses key principles and practical examples to ensure smooth and reliable operations from day one.</description><pubDate>Sat, 31 Aug 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;It&apos;s become standard practice to automate and codify product operations. When launching a new product, you&apos;re often starting from scratch with these automation efforts.&lt;/p&gt;
&lt;p&gt;While external services have made setting up environments significantly more efficient, achieving perfect automation from the outset can be challenging for many products. It&apos;s more common to build the environment and continuously reduce toil as the product develops.&lt;/p&gt;
&lt;p&gt;This inevitably involves manual processes. While it&apos;s acceptable to perform tasks manually with the intention of automating them later, are we underestimating the risks associated with these manual operations? Consider deployments, data migrations, data changes during incident response, and data investigations.&lt;/p&gt;
&lt;p&gt;As Heinrich&apos;s Law suggests, overlooking small risks can lead to significant problems down the line. Certain precautions should be taken when performing manual tasks. Just because a process isn&apos;t automated doesn&apos;t mean we should approach risky operations like data queries with creative, on-the-spot solutions. This can be dangerous.&lt;/p&gt;
&lt;h2&gt;Key Principles for Manual Operations&lt;/h2&gt;
&lt;p&gt;Here are some key principles to keep in mind:&lt;/p&gt;
&lt;h3&gt;1. The Principle of Least Astonishment&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Avoid surprising your team with unexpected information when performing tasks or communicating updates (happy surprises excluded!).&lt;/li&gt;
&lt;li&gt;Surprises often indicate a lack of awareness beforehand, meaning someone who should have been informed wasn&apos;t. Ensure transparency within the team so that all operations are predictable. Teamwork relies on shared understanding.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2. Document Everything&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Before the Operation:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;Create detailed procedures based on anticipated steps.&lt;/li&gt;
&lt;li&gt;Conduct team reviews of these procedures to identify gaps or omissions.&lt;/li&gt;
&lt;li&gt;On the day of the operation, simply follow the documented steps mechanically. This reduces psychological burden and promotes safe execution.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Rehearsal:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;Depending on the importance of the operation, conduct a dry run without making actual changes.&lt;/li&gt;
&lt;li&gt;Execute commands with dry-run options enabled or run read-only queries to simulate the process.&lt;/li&gt;
&lt;li&gt;Even well-written documentation can reveal unexpected errors or missing steps during a dry run.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;During the Operation:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;Comment and record each step and its outcome. This helps prevent secondary damage caused by panicked, undocumented commands during incident response.&lt;/li&gt;
&lt;li&gt;Communicate with your team before and during the operation. Avoid silent execution, especially when multiple team members are making changes, as the combined outcome can be unpredictable.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Verification:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;For pull requests, document manual testing performed, especially for cases where unit tests are difficult to write due to limitations in the testing environment. For UI testing, capture videos or screenshots.&lt;/li&gt;
&lt;li&gt;This helps with post-incident analysis, making it easier to identify testing gaps and determine necessary test cases or implementation improvements to prevent recurrence.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Benefits of Documentation&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Short-term:&lt;/strong&gt;  Standardize common procedures through documented templates.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Long-term:&lt;/strong&gt;  Provide requirements for future automation efforts. Analyze and categorize documented tasks to prioritize automation initiatives.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3. Generate Meaningful Logs&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Go beyond generic error messages. Strive for logs that provide insights into the issue.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Bad Log:&lt;/strong&gt; &quot;An error occurred.&quot; This doesn&apos;t offer any helpful information beyond the fact that an exception was thrown.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Good Log:&lt;/strong&gt; &quot;Data synchronization error with Service A: Service A API is temporarily unavailable. Failed values: value1, value2, value3.&quot; This log clearly explains the problem, reducing the need to dig into the code. It also identifies the specific values that caused the failure.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Practical Examples&lt;/h2&gt;
&lt;h3&gt;Release (Deployment)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;If deployments occur on a fixed schedule, hold a release planning meeting with the team beforehand. Use this opportunity to align everyone on the upcoming activities. Consider using a tool like Notion to automatically create a release plan document.&lt;/li&gt;
&lt;li&gt;Include the following in the template:
&lt;ul&gt;
&lt;li&gt;Release Date and Time&lt;/li&gt;
&lt;li&gt;Release Engineer&lt;/li&gt;
&lt;li&gt;Release Scope&lt;/li&gt;
&lt;li&gt;Data Migration (Yes/No)
&lt;ul&gt;
&lt;li&gt;Details of planned queries and their potential impact on production system performance.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Post-Release Verification Steps&lt;/li&gt;
&lt;li&gt;Rollback Procedures&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Data Modification&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Treat production data changes with extreme caution. Implement query reviews for RDBMS systems.&lt;/li&gt;
&lt;li&gt;Maintain a record of executed queries along with their results. While transaction logs and binary logs can provide this information retrospectively, having readily available query results simplifies root cause analysis in case of secondary issues.&lt;/li&gt;
&lt;li&gt;Consider using a template like this:
&lt;ul&gt;
&lt;li&gt;Purpose and Summary of the Change&lt;/li&gt;
&lt;li&gt;Planned Query&lt;/li&gt;
&lt;li&gt;Pre-Execution Verification Query and Results&lt;/li&gt;
&lt;li&gt;Post-Execution Verification Query and Results&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;This post explored strategies for managing manual operations in the early stages of a web service&apos;s lifecycle, before full automation is achieved. The core principles to embrace are: 1. Principle of Least Astonishment, 2. Shared Understanding, and 3. Comprehensive Documentation.&lt;/p&gt;
&lt;p&gt;In the future, AI agents might handle these manual tasks. The principles discussed here can serve as guidelines for designing and training these agents, ensuring they operate reliably and predictably.&lt;/p&gt;
</content:encoded></item><item><title>Automating My Note Tagging with LLMs</title><link>https://ainoya.dev/posts/auto-document-tag-with-vertex-ai-json-schema/</link><guid isPermaLink="true">https://ainoya.dev/posts/auto-document-tag-with-vertex-ai-json-schema/</guid><description>Tired of manually tagging my notes, I decided to automate the process using Large Language Models (LLMs). This post explores my journey and the solution I implemented using Vertex AI.</description><pubDate>Sun, 25 Aug 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I&apos;ve been using tags to organize and connect my notes locally.  This has been especially helpful when navigating related information within &lt;a href=&quot;/posts/distilled-pocket-article-to-obsidian/&quot;&gt;Obsidian&lt;/a&gt;. While tagging has been useful, it&apos;s also been a manual and tedious process.  I previously considered using morphological analysis to extract keywords as tags, but the results were often noisy and didn&apos;t capture the core meaning of my notes.&lt;/p&gt;
&lt;p&gt;Recently, I started experimenting with LLMs to see if they could help.  My idea was to feed my notes into an LLM and use prompts to extract the most important topics, which could then be used as tags.  The results were promising!&lt;/p&gt;
&lt;p&gt;I implemented this solution in my &lt;a href=&quot;https://github.com/ainoya/PocketMD/blob/main/src/append_tags.ts#L103&quot;&gt;PocketMD project&lt;/a&gt;, leveraging Vertex AI to generate tags and append them to the frontmatter of my notes.  Both Vertex AI and Gemini offer the ability to specify the output format using JSON and schemas.  However, I found that this option is only available with Gemini Pro and not Gemini Flash. Here&apos;s an example of the schema definition:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  const schema: ResponseSchema = {
    type: FunctionDeclarationSchemaType.OBJECT,
    properties: {
      tags: {
        // string array
        type: FunctionDeclarationSchemaType.ARRAY,
        // @ts-ignore
        items: {
          type: FunctionDeclarationSchemaType.STRING,
        },
      },
    },
  };
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I encountered an error when trying to add a type to the array elements using the TypeScript types provided by &lt;code&gt;@google-cloud/vertexai&lt;/code&gt;. I had to use &lt;code&gt;@ts-ignore&lt;/code&gt; as a temporary workaround.&lt;/p&gt;
&lt;p&gt;It&apos;s worth noting that advanced JSON schema features like &lt;code&gt;allOf&lt;/code&gt; and &lt;code&gt;oneOf&lt;/code&gt; didn&apos;t work for API validation, both with Vertex AI and OpenAI&apos;s structure output (as of August 25, 2024).&lt;/p&gt;
&lt;p&gt;While the LLM generally adhered to the specified output format, I still needed to refine the values using prompts. For instance, even when specifying that tags should start with a hashtag, the LLM occasionally missed this or added extra double quotes.  I ended up using regular expressions to clean up the generated output.&lt;/p&gt;
&lt;p&gt;This implementation has significantly improved my workflow.  I can now automatically tag both new and existing notes with a simple script, making it much easier to connect ideas and revisit past information.  This has been a game-changer for organizing my thoughts and staying on top of my ever-growing collection of notes.&lt;/p&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://platform.openai.com/docs/guides/structured-outputs/how-to-use&quot;&gt;Structured Outputs - OpenAI API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/control-generated-output?hl=en#generativeaionvertexai_gemini_controlled_generation_response_schema_2-python&quot;&gt;Control generated output  |  Generative AI on Vertex AI  |  Google Cloud&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>LLM Application Security Practice: Markdown Image Tag</title><link>https://ainoya.dev/posts/llm-application-security-practice-markdown-img-tag/</link><guid isPermaLink="true">https://ainoya.dev/posts/llm-application-security-practice-markdown-img-tag/</guid><description>Learn how seemingly harmless markdown image rendering in your LLM application can lead to user data leakage and how to mitigate this security risk.</description><pubDate>Sat, 03 Aug 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Rendering Markdown Images in LLM Apps? Proceed with Caution&lt;/h2&gt;
&lt;p&gt;It&apos;s a common practice to render markdown-formatted text into HTML for user responses in LLM applications. Markdown&apos;s image embedding syntax, &lt;code&gt;![image alt text](image url)&lt;/code&gt;, seamlessly integrates images into the HTML output.  But here&apos;s the catch – this convenience can be a potential security pitfall.&lt;/p&gt;
&lt;p&gt;When an image is rendered in HTML, the browser accesses its URL.  This opens up a subtle attack vector:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Malicious actors can craft URLs with embedded parameters containing sensitive user context or personal information.&lt;/li&gt;
&lt;li&gt;When the browser fetches the image, this data leaks to the external server via the URL parameters, potentially ending up in server logs or worse.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;How to Mitigate the Risk&lt;/h2&gt;
&lt;p&gt;Fortunately, there are several ways to prevent this type of data leakage:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Disable Markdown Image Embedding:&lt;/strong&gt;  The most straightforward approach is to entirely disable the image embedding functionality within your markdown parser.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Implement a Whitelist:&lt;/strong&gt;  Allow images to be embedded only from trusted domains by creating a whitelist of allowed hosts.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Example Code: Protecting Your LLM Application&lt;/h2&gt;
&lt;p&gt;Let&apos;s look at some practical TypeScript examples using the &lt;code&gt;remark&lt;/code&gt; library for rendering markdown to HTML:&lt;/p&gt;
&lt;h3&gt;Disabling Image Embedding&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import { remark } from &quot;remark&quot;;
import html from &quot;remark-html&quot;;
import { visit } from &quot;unist-util-visit&quot;;

interface ImageNode {
  type: &quot;image&quot;;
  url: string;
}

type MarkdownNode = ImageNode | any;

// Function to remove all image tags
const removeImage = (): ((tree: MarkdownNode) =&amp;gt; void) =&amp;gt; {
  return (tree: MarkdownNode) =&amp;gt; {
    visit(
      tree,
      &quot;image&quot;,
      (node: ImageNode, index: number | undefined, parent: MarkdownNode) =&amp;gt; {
        parent.children.splice(index!, 1); // Remove the image tag
      }
    );
  };
};

const markdown = `# Example Markdown
This is a paragraph.
![An image](https://example.com/image.png)
Another paragraph.
`;

remark()
  .use(removeImage)
  .use(html)
  .process(markdown, function (err, file) {
    if (err) throw err;
    console.log(String(file));
  });

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Whitelisting Trusted Hosts&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import { remark } from &quot;remark&quot;;
import html from &quot;remark-html&quot;;
import { visit } from &quot;unist-util-visit&quot;;
const trustedHosts = [&quot;https://example.com&quot;]; // Manage your whitelist here

interface ImageNode {
  type: &quot;image&quot;;
  url: string;
}

type MarkdownNode = ImageNode | any;

const safeImageEmbedding = (): ((tree: MarkdownNode) =&amp;gt; void) =&amp;gt; {
  return (tree: MarkdownNode) =&amp;gt; {
    visit(
      tree,
      &quot;image&quot;,
      (node: ImageNode, index: number | undefined, parent: MarkdownNode) =&amp;gt; {
        if (!trustedHosts.some((host) =&amp;gt; node.url.startsWith(host))) {
          parent.children.splice(index!, 1); // Remove untrusted image tags
        }
      }
    );
  };
};

const markdown = `# Example Markdown
This is a paragraph.
![An image](https://example.com/image.png)
Another paragraph.

This is a dangerous image, but it will be removed by the safeImageEmbedding function
![An image](http://nottrusted.com/image.png)
`;

remark()
  .use(safeImageEmbedding)
  .use(html)
  .process(markdown, function (err, file) {
    if (err) throw err;
    console.log(String(file));
  });
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Important&lt;/strong&gt;: These methods provide a layer of protection but don&apos;t guarantee complete security. Thoroughly test your implementation to ensure it meets your specific security needs. While you could attempt to use regular expressions to replace image tags, this approach is prone to errors. Escape characters or cleverly crafted input could bypass your regex patterns. Directly manipulating the Abstract Syntax Tree (AST) parsed by your markdown library is a much more robust and reliable solution.&lt;/p&gt;
&lt;h2&gt;Why This Matters, Especially for UGC&lt;/h2&gt;
&lt;p&gt;Services that encourage user-generated content (UGC), such as platforms for sharing LLM prompts, are particularly vulnerable:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;A malicious user shares a seemingly helpful prompt.&lt;/li&gt;
&lt;li&gt;An unsuspecting user inputs this prompt into their LLM application.&lt;/li&gt;
&lt;li&gt;The LLM, potentially employing in-context learning, generates a response that incorporates the user&apos;s sensitive data within a crafted image URL.&lt;/li&gt;
&lt;li&gt;The malicious user receives the leaked data when the image is rendered.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Remember:&lt;/strong&gt; This vulnerability extends beyond LLM applications. It&apos;s crucial to stay informed about security best practices for handling user-generated content and data privacy in any online service.&lt;/p&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://embracethered.com/blog/posts/2024/github-copilot-chat-prompt-injection-data-exfiltration/&quot;&gt;GitHub Copilot Chat: From Prompt Injection to Data Exfiltration · Embrace The Red&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://owasp.org/www-project-top-10-for-large-language-model-applications/&quot;&gt;OWASP Top 10 for Large Language Model Applications | OWASP Foundation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Web Distiller AI: A Chrome Extension for Summarizing Web Pages</title><link>https://ainoya.dev/posts/web-distiller-ai-chrome-extension/</link><guid isPermaLink="true">https://ainoya.dev/posts/web-distiller-ai-chrome-extension/</guid><description>A Chrome extension that summarizes web page contents using the built-in Gemini Nano model. It offers secure in-browser summarization with translation options and markdown output. Easily copy the summary to your clipboard for quick access.</description><pubDate>Sat, 06 Jul 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import Twitter from &apos;../../../components/Twitter.astro&apos;;&lt;/p&gt;
&lt;p&gt;&amp;lt;Twitter url=&quot;https://x.com/naokiainoya/status/1809507529501688266&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;I’ve been working on: &lt;a href=&quot;https://github.com/ainoya/chrome-extension-web-distiller-ai&quot;&gt;chrome-extension-web-distiller-ai&lt;/a&gt;, a Chrome extension designed to summarize web page contents using the built-in Gemini Nano model. This project utilizes local LLM technology to offer secure and efficient in-browser summarization without transmitting any content to external networks.&lt;/p&gt;
&lt;h2&gt;Key Features&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Summarization: Extracts and summarizes the main content of the currently viewed web page.&lt;/li&gt;
&lt;li&gt;Translation Options: Offers translation of summaries into English or Japanese.&lt;/li&gt;
&lt;li&gt;Markdown Output: Converts the extracted content into markdown format for easy readability.&lt;/li&gt;
&lt;li&gt;Clipboard Copy: Provides a convenient button to copy the summary to the clipboard.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Development Challenges&lt;/h2&gt;
&lt;p&gt;During the development of this extension, I encountered several challenges:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Context Length Issues: When the content exceeded the context length, I faced a NotReadableError: The execution yielded a bad response. error. Unfortunately, the current built-in Gemini Nano model in Chrome does not have publicly available specifications regarding its context length limit. I hope that these specifications will be published in the future.&lt;/li&gt;
&lt;li&gt;Unpredictable Errors: Certain content types caused similar errors during generation, and the exact cause remains unclear. This unpredictability required additional handling in the extension.&lt;/li&gt;
&lt;li&gt;Quality of Output: Initially, performing both summarization and translation in a single prompt significantly degraded the quality of the output. To resolve this, I separated the processes into distinct prompts, which improved the overall performance and quality.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Reflections&lt;/h2&gt;
&lt;p&gt;Developing Web Distiller AI has been an enlightening experience. Here are some of my thoughts:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Expanded Utility: The ability to utilize LLM functionalities with just a properly set up Chrome browser represents a significant leap forward. This expands the practical utility of such models across various use cases.&lt;/li&gt;
&lt;li&gt;Potential for Web Services: While I developed this as a Chrome extension, it would also be interesting to implement it as a resident assistant on a web service platform. This could provide even more accessibility and convenience for users.&lt;/li&gt;
&lt;li&gt;Future of Gemini Nano: As a multimodal LLM, Gemini Nano holds tremendous potential. I look forward to the day when Chrome can handle not just text processing but also image OCR and voice recognition using this technology.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You can check out the Web Distiller AI repository on GitHub to learn more about the project and try it out yourself.&lt;/p&gt;
</content:encoded></item><item><title>Improving Web Scraping with Readability and Table Support in Markdown</title><link>https://ainoya.dev/posts/use-readability/</link><guid isPermaLink="true">https://ainoya.dev/posts/use-readability/</guid><description>Switching from DOM Distiller to Readability for better content extraction and adding table support in Markdown using turndown.</description><pubDate>Tue, 18 Jun 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I previously used &lt;a href=&quot;https://github.com/chromium/dom-distiller&quot;&gt;DOM Distiller&lt;/a&gt; for content extraction in my project &lt;a href=&quot;https://github.com/ainoya/cloudflare-dom-distiller&quot;&gt;cloudflare-dom-distiller&lt;/a&gt;. However, I have recently switched to &lt;a href=&quot;https://github.com/mozilla/readability&quot;&gt;Readability&lt;/a&gt; because it produces more visually appealing results. This change has significantly improved the markdown output, removing unwanted headers and footers and enhancing the overall look.&lt;/p&gt;
&lt;p&gt;Additionally, I have implemented support for the &lt;code&gt;&amp;lt;table&amp;gt;&lt;/code&gt; tag to output tables in Markdown format. For converting HTML to Markdown, I use &lt;a href=&quot;https://github.com/mixmark-io/turndown&quot;&gt;turndown&lt;/a&gt;, which has a plugin for GitHub Flavored Markdown (GFM), &lt;a href=&quot;https://github.com/mixmark-io/turndown-plugin-gfm&quot;&gt;turndown-plugin-gfm&lt;/a&gt;. Integrating this plugin has enabled seamless table conversion in Markdown.&lt;/p&gt;
&lt;p&gt;While searching for similar functionalities, I came across an interesting open-source project and SaaS, &lt;a href=&quot;https://github.com/jina-ai/reader&quot;&gt;jina-ai/reader&lt;/a&gt;. This tool not only offers features comparable to cloudflare-dom-distiller but also includes web search capabilities, utilizing puppeteer to access the Brave search engine. For those seeking more advanced features, jina-ai/reader is a promising option.&lt;/p&gt;
</content:encoded></item><item><title>Enhancing Personal Knowledge Management with Pocket and Obsidian Integration</title><link>https://ainoya.dev/posts/distilled-pocket-article-to-obsidian/</link><guid isPermaLink="true">https://ainoya.dev/posts/distilled-pocket-article-to-obsidian/</guid><description>Streamline your knowledge management with Pocket and Obsidian integration. Learn about a custom solution using dom-distiller API and PocketMD scripts to automate article transfers, enhance productivity, and create an interconnected knowledge base, making research and note-taking more efficient.</description><pubDate>Thu, 13 Jun 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;For my personal and professional needs, I rely on two powerful tools: &lt;a href=&quot;https://getpocket.com/&quot;&gt;Pocket&lt;/a&gt; for collecting and storing articles, and Obsidian for organizing and synthesizing my thoughts. This blog post explores how I integrated these tools to streamline my workflow and enhance productivity.&lt;/p&gt;
&lt;h2&gt;Using Pocket for Information Collection&lt;/h2&gt;
&lt;p&gt;Pocket is my go-to tool for gathering information from the internet. It&apos;s incredibly convenient to save articles and use them later for discussions with colleagues or casual conversations. The ability to have a curated library of articles at my fingertips significantly enhances the quality of my interactions and keeps me well-informed.&lt;/p&gt;
&lt;h2&gt;Organizing Thoughts with Obsidian&lt;/h2&gt;
&lt;p&gt;For organizing my personal thoughts and research, I use Obsidian. This powerful note-taking app allows me to create a structured and interconnected knowledge base. When transferring content from Pocket to Obsidian, I often refer back to the articles I&apos;ve saved in Pocket. However, manually adding links from Pocket to my Obsidian notes was becoming time-consuming.&lt;/p&gt;
&lt;h2&gt;Automating the Workflow&lt;/h2&gt;
&lt;p&gt;I wanted to find a way to automate the process of linking Pocket articles to my Obsidian notes. Since my Obsidian files are stored on iCloud and synced across my devices, I considered creating a script to fetch articles from Pocket locally.&lt;/p&gt;
&lt;h2&gt;Choosing an Independent Implementation&lt;/h2&gt;
&lt;p&gt;While implementing this directly as an Obsidian plugin was an option, I decided to take an independent approach. This would make it easier to batch process markdown files and maintain flexibility in how I manage my notes.&lt;/p&gt;
&lt;h2&gt;Implementing dom-distiller API and PocketMD&lt;/h2&gt;
&lt;p&gt;To achieve my goal, I developed two key components: the dom-distiller API and PocketMD.&lt;/p&gt;
&lt;h3&gt;dom-distiller API&lt;/h3&gt;
&lt;p&gt;I needed a way to extract essential content from saved HTML articles and convert them to markdown. This would also help reduce context length when processing with LLMs. By leveraging the &lt;a href=&quot;https://github.com/chromium/dom-distiller&quot;&gt;chromium/dom-distiller&lt;/a&gt; implementation and executing content extraction scripts with Puppeteer, I achieved the desired functionality.&lt;/p&gt;
&lt;p&gt;I developed the API to run on Cloudflare Workers, avoiding local machine load and making future integrations with tools like make (Integromat) and Zapier possible.&lt;/p&gt;
&lt;h3&gt;PocketMD&lt;/h3&gt;
&lt;p&gt;Next, I created a set of scripts called PocketMD to handle local processing of articles saved in Pocket. You can find the repository here: &lt;a href=&quot;https://github.com/ainoya/PocketMD&quot;&gt;ainoya/PocketMD&lt;/a&gt;. This setup involves storing data in SQLite for easy processing and includes the following features:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Retrieve articles from Pocket and save them in SQLite.&lt;/li&gt;
&lt;li&gt;Extract URLs from SQLite, fetch full text using the &lt;a href=&quot;https://github.com/ainoya/cloudflare-dom-distiller&quot;&gt;Distiller API&lt;/a&gt;, and save it.&lt;/li&gt;
&lt;li&gt;Generate summaries of articles using Google Vertex AI based on the information stored in SQLite.&lt;/li&gt;
&lt;li&gt;Convert the information to markdown format for use with Obsidian or similar applications.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;By using LLMs (Google Vertex AI), I generate summaries and Obsidian keywords, automatically creating links to existing notes. This interlinking enhances my thought organization in Obsidian.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;The integration of Pocket and Obsidian through custom scripts has significantly improved my note management and information retrieval processes. With keyword links in Obsidian, I can quickly reference saved articles, enhancing the efficiency of my thought organization.&lt;/p&gt;
&lt;p&gt;Furthermore, the knowledge graph constructed in Obsidian aligns well with tools like &lt;a href=&quot;https://www.microsoft.com/en-us/research/blog/graphrag-unlocking-llm-discovery-on-narrative-private-data/&quot;&gt;Graph RAG&lt;/a&gt;. This allows for implementing solutions that leverage LLMs to propose resolutions across multiple articles, addressing common topics of interest.&lt;/p&gt;
&lt;p&gt;By automating the workflow between Pocket and Obsidian, I have created a seamless, efficient system that enhances my productivity and keeps my knowledge base robust and interconnected.&lt;/p&gt;
</content:encoded></item><item><title>Optimizing Model Load Times on EC2 with Pre-loaded AMIs</title><link>https://ainoya.dev/posts/ec2-model-loading-too-slow/</link><guid isPermaLink="true">https://ainoya.dev/posts/ec2-model-loading-too-slow/</guid><description>How I reduced initial model load times on EC2 instances by creating AMIs with pre-loaded models, identifying bottlenecks, and implementing a solution using the fio command to optimize disk I/O performance.</description><pubDate>Sat, 18 May 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Recently, I encountered a challenge while working with machine learning models on Amazon EC2. To speed up the initial loading of a pre-trained model, I decided to create an Amazon Machine Image (AMI) with the model already pre-loaded. However, I discovered that the first inference run after instance startup took over ten times longer than usual. Subsequent inferences ran at normal speed.&lt;/p&gt;
&lt;h2&gt;Inference Program Overview&lt;/h2&gt;
&lt;p&gt;Here&apos;s a simplified version of the inference program I used:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;model = Pipeline.from_pretrained(#some options,,,)
model = model.to(device) # This line takes an unusually long time during the first run
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Upon investigating the program logs, I found that the &lt;code&gt;model.to(device)&lt;/code&gt; line, which transfers the model to the device, was the main bottleneck. Using &lt;code&gt;dstat&lt;/code&gt;, I monitored system resource usage and noticed that disk I/O performance was significantly slower during the initial run.&lt;/p&gt;
&lt;h2&gt;Identifying the Issue&lt;/h2&gt;
&lt;p&gt;I discovered that when using EBS volumes on EC2, the file system fetches files from S3 only when they are accessed for the first time. This behavior causes the initial I/O operations to be slow, as described in the AWS documentation:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Empty EBS volumes receive their maximum performance the moment that they are created and do not require initialization (formerly known as pre-warming). For volumes, of any volume type, that were created from snapshots, the storage blocks must be pulled down from Amazon S3 and written to the volume before you can access them. This preliminary action takes time and can cause a significant increase in the latency of I/O operations the first time each block is accessed. Volume performance is achieved after all blocks have been downloaded and written to the volume.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Implementing the Solution&lt;/h2&gt;
&lt;p&gt;To address this, I wrote a script to read the model files using the &lt;code&gt;fio&lt;/code&gt; command at instance startup. &lt;code&gt;fio&lt;/code&gt; is a flexible I/O tester with more options than &lt;code&gt;dd&lt;/code&gt;, and it supports asynchronous I/O, making it faster for reading files.&lt;/p&gt;
&lt;p&gt;Here&apos;s the &lt;code&gt;fio&lt;/code&gt; command I used:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fio --name=&quot;read_test&quot; --filename=&quot;$file&quot; --rw=read --bs=1M --ioengine=libaio --iodepth=32 --direct=1
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Alternative Solutions&lt;/h2&gt;
&lt;p&gt;Another potential solution is to use &lt;a href=&quot;https://docs.aws.amazon.com/ebs/latest/userguide/ebs-fast-snapshot-restore.html&quot;&gt;EBS fast snapshot restore&lt;/a&gt;, which preloads snapshots and eliminates the initial load time overhead. However, this option incurs additional costs, so I opted not to use it in this case.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Instances created from EBS volumes with pre-loaded models experience slow initial model loads due to the S3-to-EBS fetch process. This issue is more pronounced with larger model files.&lt;/li&gt;
&lt;li&gt;Using &lt;code&gt;fio&lt;/code&gt; to read model files at startup mitigates this problem by preloading data, significantly reducing initial load times.&lt;/li&gt;
&lt;li&gt;Alternative solutions include using fast snapshot restore or other high-performance distributed storage solutions like &lt;a href=&quot;https://cloud.google.com/parallelstore?hl=en&quot;&gt;Parallelstore&lt;/a&gt; or &lt;a href=&quot;https://www.colfax-intl.com/solutions/intel-daos&quot;&gt;DAOS&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.aws.amazon.com/ebs/latest/userguide/ebs-initialize.html&quot;&gt;Initialize Amazon EBS volumes - Amazon EBS&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.colfax-intl.com/solutions/intel-daos&quot;&gt;DAOS Storage and High Performance Computing (HPC) Solutions&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Accelerating Company Culture And Value Assimilation Among Employees Through Generative AI</title><link>https://ainoya.dev/posts/zapier-openai-accelerate-culture-value/</link><guid isPermaLink="true">https://ainoya.dev/posts/zapier-openai-accelerate-culture-value/</guid><description>Discusses methods using Zapier and OpenAI to support the awareness and maintenance of company culture and values among employees.</description><pubDate>Mon, 06 May 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;Maintaining and reinforcing a company&apos;s culture and values among employees can be a challenging task. It&apos;s crucial for employees to be consistently aware of these values to sustain and enrich the company&apos;s culture. Implementing systems that simplify this awareness and empower employees according to these values is essential. However, amidst busy work schedules, it can become difficult to continually focus on these values. &lt;a href=&quot;https://handbook.gitlab.com/handbook/communication/#values-emoji&quot;&gt;At GitLab&lt;/a&gt;, a novel approach involves using emojis linked to company values to remind employees of these principles regularly. This blog explores how we can leverage tools like Zapier and OpenAI to further facilitate the integration of company values among team members.&lt;/p&gt;
&lt;h2&gt;Using Value Emojis to Foster Awareness&lt;/h2&gt;
&lt;p&gt;At GitLab, value emojis are a core part of daily communication, helping to embed company values in everyday interactions. When someone posts a value emoji in Slack, it&apos;s automatically shared in a dedicated channel via the &lt;a href=&quot;https://slack.com/intl/en-gb/help/articles/360000482666-Reacji-Channeler-for-Slack&quot;&gt;Reacji Channeler&lt;/a&gt;, promoting value-driven discussions. Inspired by this, I experimented with a setup that could automate and quantify the representation of company values within team communications.&lt;/p&gt;
&lt;h3&gt;Weekly Retrospectives with Value Emoji Summaries&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://ainoya.dev/posts/zapier-openai-accelerate-culture-value/images/value-summary.png&quot; alt=&quot;Value Emoji Summary&quot; /&gt;&lt;/p&gt;
&lt;p&gt;To facilitate reflection on value alignment in team meetings, I created two Zaps:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Capturing Slack posts with value emoji reactions and saving them to a &lt;a href=&quot;https://zapier.com/blog/zapier-tables-guide/&quot;&gt;Zapier Table&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Triggering a collection of the past week&apos;s posts when &lt;code&gt;@value-bot retrospective&lt;/code&gt; is mentioned in Slack, summarizing and posting the frequency of each emoji used.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This system allowed the team to easily recall discussions from the past week and focus on areas needing improvement in terms of value alignment, making our weekly retrospectives more efficient and focused.&lt;/p&gt;
&lt;h2&gt;Automating Value-Based Evaluations of Communications&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://ainoya.dev/posts/zapier-openai-accelerate-culture-value/images/value-eval.png&quot; alt=&quot;Value-Based Evaluation&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Building on the previous automation, I developed a method to summarize and evaluate team communications based on adherence to company values using Google Sheets and OpenAI API:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Storing Slack posts in Google Sheets.&lt;/li&gt;
&lt;li&gt;Triggering a summary and evaluation of these posts in Slack when &lt;code&gt;@value-bot value rating&lt;/code&gt; is mentioned.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The prompt for the AI was structured to assess the content against our defined company values, providing a rating from 1 to 5, where 1 is the lowest and 5 is the highest. This method provides a gamified, unbiased feedback mechanism, engaging employees in a fun and interactive review of their communications.&lt;/p&gt;
&lt;p&gt;Prompt example:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Please evaluate the Slack log for today based on the evaluation criteria on a scale of 1 to 5, where 1 is the lowest and 5 is the highest rating.
The team members are Alice, Bob, and Charlie. Please focus on their statements for evaluation.

The evaluation criteria are as follows:

1. Value 1: {Value1 Explanation...}
2. Value 2: {Value2 Explanation...}

---Below is the content of the Slack log---
{Here, expand the list of Slack posts obtained in the previous step on Zapier}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;By combining Zapier and OpenAI, I was able to create an advanced system for ingraining company values within the team&apos;s daily operations. The automation provides a bias-free way to reflect on and enhance our adherence to these values. However, it&apos;s important to remember that AI-generated evaluations can be imperfect. They should not directly affect employee evaluations or appraisals but instead serve as a casual reminder and reinforcement of company values. This system is highly recommended for companies looking to deepen value integration in a fun and engaging way.&lt;/p&gt;
&lt;h2&gt;Reference&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://handbook.gitlab.com/handbook/values/&quot;&gt;GitLab Values | The GitLab Handbook&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://handbook.gitlab.com/&quot;&gt;The GitLab Handbook&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://handbook.gitlab.com/handbook/company/culture/&quot;&gt;GitLab Culture | The GitLab Handbook&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://handbook.gitlab.com/handbook/communication/#values-emoji&quot;&gt;GitLab Communication | The GitLab Handbook&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Using the Web Crypto API for Secure Encryption and Decryption</title><link>https://ainoya.dev/posts/web-crypto-api-encryption-decryption/</link><guid isPermaLink="true">https://ainoya.dev/posts/web-crypto-api-encryption-decryption/</guid><description>Learn how to use the Web Crypto API for secure encryption and decryption of sensitive data like API keys, especially in environments like Cloudflare Workers.</description><pubDate>Sat, 04 May 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Securely managing sensitive information such as API keys in web services is crucial. My focus was on encrypting this data in a decryptable manner without depending on Node.js libraries, particularly in environments like Cloudflare Workers. This led me to explore the Web Crypto API, which provides a robust solution for secure cryptographic operations directly in the browser, free from Node.js Library dependencies.&lt;/p&gt;
&lt;h2&gt;Why Web Crypto API?&lt;/h2&gt;
&lt;p&gt;I chose the &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API&quot;&gt;Web Crypto API&lt;/a&gt; for its strong security features and broad support across modern browsers. Importantly, it operates in restricted environments like Cloudflare Workers, making it ideal for my requirements.&lt;/p&gt;
&lt;p&gt;For encryption and decryption, I implemented the &lt;code&gt;AES-256-GCM&lt;/code&gt; method. This choice was influenced by GitLab&apos;s &lt;a href=&quot;https://handbook.gitlab.com/handbook/security/cryptographic-standard/#algorithmic-standards:~:text=Block%20Ciphers%20%2D%20AES.%20Currently%20AES%2D128%20is%20considered%20the%20minimal%2C%20but%20to%20ensure%20%E2%80%9Cfuture%20proofing%E2%80%9D%20AES%2D256%20is%20preferred%20(note%20there%20are%20potential%20performance%20impacts%20depending%20upon%20application%20usage%2C%20etc).%20GCM%20is%20the%20required%20mode%20(e.g.%20aes%2D256%2Dgcm).&quot;&gt;cryptographic standards&lt;/a&gt;, which suggests AES-256 for &quot;future-proofing&quot; despite potential performance impacts.&lt;/p&gt;
&lt;h2&gt;Implementation Code&lt;/h2&gt;
&lt;p&gt;Below is the TypeScript code for encryption and decryption, which can be included in your projects:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// ~/utils/encryptor.server.ts
// Purpose: Encryptor utility for encrypting and decrypting data.
const algorithm = &quot;AES-GCM&quot;;

function base64ToArrayBuffer(base64: string): ArrayBuffer {
  const binaryString = atob(base64);
  const len = binaryString.length;
  const bytes = new Uint8Array(len);
  for (let i = 0; i &amp;lt; len; i++) {
    bytes[i] = binaryString.charCodeAt(i);
  }
  return bytes.buffer;
}

function arrayBufferToBase64(buffer: ArrayBuffer): string {
  let binary = &quot;&quot;;
  const bytes = new Uint8Array(buffer);
  const len = bytes.byteLength;
  for (let i = 0; i &amp;lt; len; i++) {
    binary += String.fromCharCode(bytes[i]);
  }
  return btoa(binary);
}

export async function encrypt(
  data: string,
  keyBase64Encoded: string
): Promise&amp;lt;string&amp;gt; {
  const keyBuffer = base64ToArrayBuffer(keyBase64Encoded);

  const cryptoKey = await crypto.subtle.importKey(
    &quot;raw&quot;,
    keyBuffer,
    { name: algorithm, length: 256 },
    false,
    [&quot;encrypt&quot;]
  );

  const iv = crypto.getRandomValues(new Uint8Array(12));
  const encryptedData = await crypto.subtle.encrypt(
    { name: algorithm, iv: iv },
    cryptoKey,
    new TextEncoder().encode(data)
  );

  const combined = new Uint8Array(iv.length + encryptedData.byteLength);
  combined.set(iv, 0);
  combined.set(new Uint8Array(encryptedData), iv.length);

  return arrayBufferToBase64(combined);
}

export async function decrypt(
  data: string,
  keyBase64Encoded: string
): Promise&amp;lt;string&amp;gt; {
  const keyBuffer = base64ToArrayBuffer(keyBase64Encoded);
  const dataBuffer = base64ToArrayBuffer(data);

  const iv = dataBuffer.slice(0, 12);
  const encrypted = dataBuffer.slice(12);

  const cryptoKey = await crypto.subtle.importKey(
    &quot;raw&quot;,
    keyBuffer,
    { name: algorithm, length: 256 },
    false,
    [&quot;decrypt&quot;]
  );

  const decrypted = await crypto.subtle.decrypt(
    { name: algorithm, iv: new Uint8Array(iv) },
    cryptoKey,
    encrypted
  );

  return new TextDecoder().decode(decrypted);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Testing the Implementation&lt;/h2&gt;
&lt;p&gt;Here&apos;s a test script to ensure the functionality works as expected:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// test/util/encryptor.server.test.ts
// test encrypt and decrypt

import { expect, test } from &quot;vitest&quot;;

import { encrypt, decrypt } from &quot;~/utils/encryptor.server&quot;;
import crypto from &quot;crypto&quot;;

test(&quot;encrypt and decrypt&quot;, async () =&amp;gt; {
  // random 32 bytes key
  const key = crypto.randomBytes(32);
  const data = &quot;test_data&quot;;

  const encrypted = await encrypt(data, key.toString(&quot;base64&quot;));

  const decrypted = await decrypt(encrypted, key.toString(&quot;base64&quot;));

  expect(decrypted).toBe(data);

  // data is encrypted
  expect(encrypted).not.toBe(data);

  // cannot decrypt with wrong key
  const wrongKey = crypto.randomBytes(32);
  // wrong decryption will throw error
  await expect(
    decrypt(encrypted, wrongKey.toString(&quot;base64&quot;))
  ).rejects.toThrow();
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;The Web Crypto API provides a powerful, platform-independent tool for encrypting and decrypting sensitive information, even within the limited environments like Cloudflare Workers.&lt;/p&gt;
&lt;p&gt;Note: The code provided in this article does not guarantee operational security. Please use it at your own risk.&lt;/p&gt;
&lt;h3&gt;References&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API&quot;&gt;Web Crypto API - Web APIs | MDN&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Revisiting: Generating OpenAI API Client with Kiota from OpenAPI Spec</title><link>https://ainoya.dev/posts/openaai-java-api-client-again/</link><guid isPermaLink="true">https://ainoya.dev/posts/openaai-java-api-client-again/</guid><description>Exploring the generation of an OpenAI API client using Microsoft Kiota from the OpenAPI specification.</description><pubDate>Wed, 07 Feb 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;In a previous exploration, I utilized the openapi-generator tool to create a &lt;a href=&quot;https://github.com/ainoya/openai-java-generated-client&quot;&gt;Java client&lt;/a&gt; for the OpenAI API. Recently, I discovered another generator named &lt;a href=&quot;https://github.com/microsoft/kiota&quot;&gt;Microsoft Kiota&lt;/a&gt;, prompting me to investigate whether it could offer a more user-friendly experience for generating API clients.&lt;/p&gt;
&lt;p&gt;Just like before, modifications to the original OpenAI OpenAPI specification were necessary, but I successfully generated the client. The resulting work is available at &lt;a href=&quot;https://github.com/ainoya/openai-kiota-client-java&quot;&gt;ainoya/openai-kiota-client-java&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Generating the client with Kiota is straightforward, done through the command line. A notable difference from openapi-generator is the reduced number of options required during code generation, which reduces cognitive load—a welcomed change.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker run -v ${PWD}:/app/output \
        -v /${PWD}/openapi.yaml:/app/openapi.yaml \
        mcr.microsoft.com/openapi/kiota generate \
        -d /app/openapi.yaml \
        --language java \
        -n dev.ainoya.kiota.openai.generated \
        -o /app/output/src/main/java/dev/ainoya/kiota/openai/generated
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The detailed modifications made to the OpenAPI spec can be understood by comparing diffs in the repository, but key changes include adding a &lt;code&gt;discriminator&lt;/code&gt; to enable type mapping, which helps eliminate warnings. Since OpenAI&apos;s API server seems to be written in Python, the type handling in responses is generally loose.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;     ChatCompletionRequestMessage:
+      discriminator:
+        propertyName: role
       oneOf:
         - $ref: &quot;#/components/schemas/ChatCompletionRequestSystemMessage&quot;
         - $ref: &quot;#/components/schemas/ChatCompletionRequestUserMessage&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For instances where the API could return either a string or an object, making it challenging to define a discriminator, I opted to comment out the string return type. It&apos;s a workaround due to the API&apos;s loose type constraints on certain parameters, indicating a preference for more strict typing from an API consumer perspective.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@@ -5616,11 +5620,11 @@ components:

         `none` is the default when no functions are present. `auto` is the default if functions are present.
       oneOf:
-        - type: string
-          description: &amp;gt;
-            `none` means the model will not call a function and instead generates a message.
-            `auto` means the model can pick between generating a message or calling a function.
-          enum: [none, auto]
+#        - type: string
+#          description: &amp;gt;
+#            `none` means the model will not call a function and instead generates a message.
+#            `auto` means the model can pick between generating a message or calling a function.
+#          enum: [none, auto]
         - $ref: &quot;#/components/schemas/ChatCompletionNamedToolChoice&quot;
       x-oaiExpandable: true
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;The Usability of the Generated Code&lt;/h2&gt;
&lt;p&gt;Utilizing Kiota brings several benefits, as highlighted in the &lt;a href=&quot;https://learn.microsoft.com/ja-jp/openapi/kiota/overview&quot;&gt;official documentation&lt;/a&gt;, including reduced maintenance cost across different language SDKs, less redundancy in templates, and a consistent feature set across languages. These advantages mainly benefit SDK developers but indirectly enhance the experience for SDK consumers by providing well-maintained tools.&lt;/p&gt;
&lt;p&gt;An example of using the generated code for a Chat completion request is straightforward and similar to using code generated by openapi-generator, but with less boilerplate, such as not needing to write &lt;code&gt;setActualInstance&lt;/code&gt; methods. This simplicity could be seen as a significant advantage of Kiota.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package dev.ainoya.kiota.openai.example;

import com.microsoft.kiota.ApiException;
import com.microsoft.kiota.authentication.AccessTokenProvider;
import com.microsoft.kiota.authentication.AllowedHostsValidator;
import com.microsoft.kiota.authentication.BaseBearerTokenAuthenticationProvider;
import com.microsoft.kiota.http.OkHttpRequestAdapter;
import com.microsoft.kiota.serialization.*;
import dev.ainoya.kiota.openai.generated.ApiClient;
import dev.ainoya.kiota.openai.generated.models.*;
import okhttp3.*;
import okhttp3.logging.HttpLoggingInterceptor;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.net.URI;
import java.util.List;
import java.util.Map;

class ExampleBearerTokenProvider implements AccessTokenProvider {
    // https://learn.microsoft.com/en-us/openapi/kiota/authentication?tabs=java

    @NotNull
    @Override
    public String getAuthorizationToken(@NotNull URI uri, @Nullable Map&amp;lt;String, Object&amp;gt; additionalAuthenticationContext) {
        // get token from env variable &quot;OPENAI_API_KEY&quot;
        return System.getenv(&quot;OPENAI_API_KEY&quot;);
    }

    @NotNull
    @Override
    public AllowedHostsValidator getAllowedHostsValidator() {
        return new AllowedHostsValidator(
                &quot;openai.com&quot;
        );
    }
}

public class ExampleApp {

    public static void main(String[] args) {
        final BaseBearerTokenAuthenticationProvider authProvider = new BaseBearerTokenAuthenticationProvider(new ExampleBearerTokenProvider());

        HttpLoggingInterceptor logging = new HttpLoggingInterceptor().setLevel(
                HttpLoggingInterceptor.Level.BASIC
                // if set level to BODY, kiota client will not work because of the response body is consumed by the interceptor
        );

        Call.Factory httpClient = new OkHttpClient.Builder()
                .addNetworkInterceptor(logging)
                .build();

        ParseNodeFactory parseNodeFactory = ParseNodeFactoryRegistry.defaultInstance;
        SerializationWriterFactory serializationWriterFactory = SerializationWriterFactoryRegistry.defaultInstance;
        final OkHttpRequestAdapter requestAdapter = new OkHttpRequestAdapter(authProvider,
                null,
                null,
                httpClient
        );


        ApiClient client = new ApiClient(requestAdapter);

        final CreateChatCompletionRequest request = getCreateChatCompletionRequest();

        try {
            CreateChatCompletionResponse post = client
                    .chat().completions().post(request);

            // debug response
            if (post != null) {
                var choices = post.getChoices();
                if (choices != null) {
                    for (var choice : choices) {
                        if (choice.getMessage() != null) {
                            System.out.println(choice.getMessage().getContent());
                        }
                    }
                } else {
                    System.out.println(&quot;choices is null&quot;);
                }
            } else {
                System.out.println(&quot;post is null&quot;);
            }
        } catch (ApiException e) {
            // handle as ApiException
            System.out.println(e.getLocalizedMessage());
        }

    }

    @NotNull
    private static CreateChatCompletionRequest getCreateChatCompletionRequest() {
        final CreateChatCompletionRequest request = new CreateChatCompletionRequest();

        final CreateChatCompletionRequest.CreateChatCompletionRequestModel model = new CreateChatCompletionRequest.CreateChatCompletionRequestModel();
        model.setString(&quot;gpt-4-turbo-preview&quot;);

        request.setModel(
                model
        );

        request.setMaxTokens(100);


        ChatCompletionRequestMessage message = new ChatCompletionRequestMessage();
        ChatCompletionRequestUserMessage userMessage = new ChatCompletionRequestUserMessage();

        ChatCompletionRequestMessageContentPart contentPart = new ChatCompletionRequestMessageContentPart();

        ChatCompletionRequestMessageContentPartText partText = new ChatCompletionRequestMessageContentPartText();
        partText.setText(&quot;What is the meaning of life?&quot;);
        partText.setType(ChatCompletionRequestMessageContentPartTextType.Text);

        contentPart.setChatCompletionRequestMessageContentPartText(
                partText
        );

        userMessage.setContent(List.of(
                contentPart
        ));

        userMessage.setRole(
                ChatCompletionRequestUserMessageRole.User
        );

        message.setChatCompletionRequestUserMessage(userMessage);
        List&amp;lt;ChatCompletionRequestMessage&amp;gt; messages = List.of(
                message
        );

        request.setMessages(
                messages
        );
        return request;
    }
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Deep Dive into maybe-finance/maybe</title><link>https://ainoya.dev/posts/read-maybe-finance/</link><guid isPermaLink="true">https://ainoya.dev/posts/read-maybe-finance/</guid><description>Explore the architecture, libraries, and design of the open-source project maybe-finance/maybe, revealing insights into modern app development.</description><pubDate>Thu, 01 Feb 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;A startup that was developing a personal asset management app recently ceased operations. However, in a commendable move, they have open-sourced their service on GitHub. You can find the repository at &lt;a href=&quot;https://github.com/maybe-finance/maybe&quot;&gt;maybe-finance/maybe&lt;/a&gt;. From the README, it&apos;s apparent that they invested over $1,000,000 in the development of this app. It&apos;s quite rare to see such a large-scale, production-grade application code released to the public. With an interest in studying modern application development techniques, I delved into the code and structure of maybe-finance/maybe.&lt;/p&gt;
&lt;h2&gt;Overview of the Structure&lt;/h2&gt;
&lt;p&gt;According to their &lt;a href=&quot;https://github.com/maybe-finance/maybe/wiki/Monorepo-File-Structure-Overview&quot;&gt;Monorepo File Structure Overview&lt;/a&gt;, the backend and frontend are both written in TypeScript. The repository adopts a monorepo structure managed by Nx, known for its rich plugin system and various features. For monorepo projects using Next.js, considering &lt;a href=&quot;https://turbo.build/&quot;&gt;turborepo&lt;/a&gt; might be beneficial, especially since it&apos;s backed by Vercel.&lt;/p&gt;
&lt;p&gt;As of commit &lt;code&gt;0575beb5138a3f7644a69b0d7a76fad96b1f8d84&lt;/code&gt;, the directory tree is as follows:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ tree -d -L 2
.
├── apps
│   ├── client
│   ├── e2e
│   ├── server
│   └── workers
├── libs
│   ├── client
│   ├── design-system
│   ├── server
│   ├── shared
│   └── teller-api
├── prisma
│   └── migrations
└── tools
    ├── generators
    ├── pages
    ├── scripts
    └── test-data
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The GitHub wiki includes a decision tree guide on where to place which type of code. Such visual guides are invaluable for developers joining a project with an extensive directory structure.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://ainoya.dev/posts/read-maybe-finance/images/decision-tree.png&quot; alt=&quot;Decision Tree&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/maybe-finance/maybe/wiki/Monorepo-File-Structure-Overview&quot;&gt;Monorepo File Structure Overview · maybe-finance/maybe Wiki&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Used Libraries&lt;/h2&gt;
&lt;h3&gt;Frontend&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;React Framework: &lt;a href=&quot;https://nextjs.org/&quot;&gt;Next.js&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;CSS Framework: &lt;a href=&quot;https://tailwindcss.com/&quot;&gt;tailwind&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://headlessui.com/&quot;&gt;headless UI&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Testing Frameworks: jest, react-testing-library, cypress&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Backend&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Database: PostgreSQL&lt;/li&gt;
&lt;li&gt;ORM: &lt;a href=&quot;https://www.prisma.io/&quot;&gt;prisma&lt;/a&gt;, with shared data model definitions between client and server.&lt;/li&gt;
&lt;li&gt;Web Application Framework: express. There are backend implementations in Next.js using prismaClient to directly access the DB, but most implementations rely on express.&lt;/li&gt;
&lt;li&gt;Worker: &lt;a href=&quot;https://optimalbits.github.io/bull/&quot;&gt;Bull&lt;/a&gt;, a job queue library for Node.js using redis for job management. It&apos;s used for asynchronous processing of communications with external services like &lt;a href=&quot;https://teller.io/&quot;&gt;Teller&lt;/a&gt;, enhancing system stability and UX. It also functions as a cron scheduler.&lt;/li&gt;
&lt;li&gt;Necessary middleware configurations are available in the &lt;a href=&quot;https://github.com/maybe-finance/maybe/blob/0575beb5138a3f7644a69b0d7a76fad96b1f8d84/docker-compose.yml#L1&quot;&gt;docker-compose.yml&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Software Design&lt;/h2&gt;
&lt;h3&gt;Frontend&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Utilizes Next.js&apos;s Page Router mode, operating in SSR. A &lt;code&gt;vercel.json&lt;/code&gt; suggests hosting on Vercel.&lt;/li&gt;
&lt;li&gt;Components that can be separated are modularized and managed with Storybook. This approach clarifies semantics, as a large number of tailwind class names can become unclear.&lt;/li&gt;
&lt;li&gt;Avoids complex state management frameworks like Redux or Jotai, instead using a combination of hooks and react-query for simple state management. This approach is sufficient until the complexity of state management becomes an issue.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Backend&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Actively uses Constructor Injection for &lt;a href=&quot;https://github.com/maybe-finance/maybe/blob/0575beb5138a3f7644a69b0d7a76fad96b1f8d84/apps/workers/src/app/lib/di.ts#L174&quot;&gt;DI&lt;/a&gt;. For instance, the IQueueFactory interface, implemented by classes like InMemoryQueueFactory and BullQueueFactory, facilitates easy swapping of persistence layers to in-memory during testing and debugging. Interfaces and their implementations use the &lt;code&gt;class&lt;/code&gt; &lt;a href=&quot;https://github.com/maybe-finance/maybe/blob/0575beb5138a3f7644a69b0d7a76fad96b1f8d84/libs/server/shared/src/services/queue/bull-queue.ts#L160&quot;&gt;syntax&lt;/a&gt;. This DI paradigm is familiar to those primarily involved in backend development. Although factory methods for structuring objects are also common in TypeScript, the choice between these two approaches can vary.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;DI Using Class&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;interface KVStore {
    get(key: string): Promise&amp;lt;string | null&amp;gt;;
    set(key: string, value: string): Promise&amp;lt;void&amp;gt;;
}

class RedisKVStore implements KVStore {
    // Initialization of the Redis client, etc.

    async get(key: string): Promise&amp;lt;string | null&amp;gt; {
        // Logic to retrieve the value from Redis
    }

    async set(key: string, value: string): Promise&amp;lt;void&amp;gt; {
        // Logic to set the value in Redis
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;DI Using Factory Method&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;type KVStore = {
    get: (key: string) =&amp;gt; Promise&amp;lt;string | null&amp;gt;;
    set: (key: string, value: string) =&amp;gt; Promise&amp;lt;void&amp;gt;;
};

function createRedisKVStore(): KVStore {
    return {
        async get(key: string): Promise&amp;lt;string | null&amp;gt; {
            // Logic to retrieve the value from Redis
        },
        async set(key: string, value: string): Promise&amp;lt;void&amp;gt; {
            // Logic to set the value in Redis
        }
    };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;The code is well-organized and consistently designed. The inclusion of generator code and docker-compose reflects a consideration for easy onboarding. Monorepo structures allow for a comprehensive overview with a single repository checkout. However, as the codebase expands, such oversight can become limited, making documents like the decision tree crucial for understanding.&lt;/li&gt;
&lt;li&gt;I plan to continue exploring and writing about interesting OSS products, much like this analysis.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Update on 2024/02/07&lt;/h2&gt;
&lt;p&gt;A significant rewrite from React/Next.js to Ruby on Rails is underway at &lt;a href=&quot;https://github.com/maybe-finance/maybe/tree/40997431072c37fb301b024cc0e3212b072ac556#moving-from-reactnextjs-to-ruby-on-rails&quot;&gt;maybe-finance/maybe&lt;/a&gt;.&lt;/p&gt;
</content:encoded></item><item><title>Where is Dropout Configured in SimCSE?</title><link>https://ainoya.dev/posts/simcse-dropout/</link><guid isPermaLink="true">https://ainoya.dev/posts/simcse-dropout/</guid><description>Dropout is incorporated within the Transformer</description><pubDate>Sun, 28 Jan 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;In the unsupervised learning approach of SimCSE, two identical sentences are vectorized using the Transformer encoder to train a sentence embedding model. A key aspect of this process is the role of Dropout settings within the Transformer, which act as &apos;noise&apos;. This noise ensures that, even though the two sentences are the same, they generate distinct vectors during training. This concept, as discussed in the paper, functions similarly to contrastive learning in image recognition, applying effectively to natural language learning.&lt;/p&gt;
&lt;p&gt;While studying SimCSE, I initially encountered confusion regarding the implementation of Dropout. My initial assumption was that Dropout would be configured in the head part of the model that compares the two sentences during training, possibly in an MLP (Multi-Layer Perceptron). However, a closer reading of the paper clarified that the Dropout referred to is actually the one within the internal layers of the Transformer. This realization resolved my confusion.&lt;/p&gt;
&lt;p&gt;For instance, in models like BERT, Dropout is configured in various layers including the attention and forward layers, as shown in the following code snippet:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from transformers import AutoModel

base_model_name = &quot;cl-tohoku/bert-base-japanese-v3&quot;
testmodel = AutoModel.from_pretrained(base_model_name)

print(testmodel)
# BertModel(
#   (embeddings): BertEmbeddings(
#     (word_embeddings): Embedding(32768, 768, padding_idx=0)
#     (position_embeddings): Embedding(512, 768)
#     (token_type_embeddings): Embedding(2, 768)
#     (LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)
#     (dropout): Dropout(p=0.1, inplace=False)
#   )
#   (encoder): BertEncoder(
#     (layer): ModuleList(
#       (0-11): 12 x BertLayer(
#         (attention): BertAttention(
#           (self): BertSelfAttention(
#             (query): Linear(in_features=768, out_features=768, bias=True)
#             (key): Linear(in_features=768, out_features=768, bias=True)
#             (value): Linear(in_features=768, out_features=768, bias=True)
#             (dropout): Dropout(p=0.1, inplace=False)
#           )
#           (output): BertSelfOutput(
#             (dense): Linear(in_features=768, out_features=768, bias=True)
#             (LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)
#             (dropout): Dropout(p=0.1, inplace=False)
#           )
#         )
#         (intermediate): BertIntermediate(
#           (dense): Linear(in_features=768, out_features=3072, bias=True)
#           (intermediate_act_fn): GELUActivation()
#         )
#         (output): BertOutput(
#           (dense): Linear(in_features=3072, out_features=768, bias=True)
#           (LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)
#           (dropout): Dropout(p=0.1, inplace=False)
#         )
#       )
#     )
#   )
#   (pooler): BertPooler(
#     (dense): Linear(in_features=768, out_features=768, bias=True)
#     (activation): Tanh()
#   )
# )

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This aspect of Dropout configuration is a common point of confusion, as evidenced by the numerous related issues raised in the GitHub repository of the SimCSE paper: &lt;a href=&quot;https://github.com/search?q=repo%3Aprinceton-nlp%2FSimCSE+dropout&amp;amp;type=issues&quot;&gt;SimCSE GitHub Issues&lt;/a&gt;. It&apos;s a subtle yet critical detail worth noting for anyone delving into this area.&lt;/p&gt;
&lt;p&gt;Reference:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://arxiv.org/abs/2104.08821&quot;&gt;SimCSE: Simple Contrastive Learning of Sentence Embeddings&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Improving Blog-Writing Workflow with VSCode&apos;s DevContainer and Astro</title><link>https://ainoya.dev/posts/astro-blog-with-devcontainer/</link><guid isPermaLink="true">https://ainoya.dev/posts/astro-blog-with-devcontainer/</guid><description>Use devcontainer to streamline the hassle of starting a local development server while writing a blog.</description><pubDate>Tue, 23 Jan 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;In pursuit of an efficient development workflow for my personal blog, ainoya.dev, I experimented with leveraging the capabilities of VSCode&apos;s DevContainer feature. This post details the journey of enhancing my blog development process, primarily using Astro, by integrating DevContainer into my workflow.&lt;/p&gt;
&lt;h2&gt;Introduction to DevContainer&lt;/h2&gt;
&lt;p&gt;VSCode&apos;s &lt;a href=&quot;https://code.visualstudio.com/docs/devcontainers/containers&quot;&gt;DevContainer&lt;/a&gt; offers a seamless way to containerize the development environment. This means consistent, isolated, and replicable development settings for any project. Utilizing this for my blog, written using Astro, presented an opportunity to streamline the setup and reduce startup times for the development server.&lt;/p&gt;
&lt;h2&gt;Setting Up DevContainer for Astro&lt;/h2&gt;
&lt;p&gt;The &lt;code&gt;.devcontainer/devcontainer.json&lt;/code&gt; file is the heart of configuring the DevContainer environment. I tailored the settings to cater to my Astro blog&apos;s needs, as illustrated below:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
    &quot;name&quot;: &quot;Node.js &amp;amp; TypeScript&quot;,
    &quot;image&quot;: &quot;mcr.microsoft.com/devcontainers/typescript-node:1-20-bullseye&quot;,
    &quot;forwardPorts&quot;: [4321],
    &quot;portsAttributes&quot;: {
        &quot;4321&quot;: {
            &quot;label&quot;: &quot;Astro&quot;,
            &quot;onAutoForward&quot;: &quot;openBrowser&quot;
        }
    },
    &quot;postCreateCommand&quot;: &quot;pnpm config set store-dir /home/node/.local/share/pnpm/store &amp;amp;&amp;amp; pnpm install --frozen-lockfile&quot;,
    &quot;postStartCommand&quot;: &quot;nohup bash -c &apos;pnpm run dev &amp;amp;&apos;&quot;,
    &quot;remoteUser&quot;: &quot;node&quot;,
    &quot;customizations&quot;: {
        &quot;vscode&quot;: {
            &quot;extensions&quot;: [&quot;astro-build.astro-vscode&quot;]
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This configuration automatically starts the Astro development server in the background upon container startup, significantly reducing the waiting time.&lt;/p&gt;
&lt;h2&gt;Resolving &lt;code&gt;pnpm install&lt;/code&gt; Issues on macOS and devcontainer&lt;/h2&gt;
&lt;p&gt;The primary issue arose when running &lt;code&gt;pnpm install&lt;/code&gt; within the DevContainer on macOS. The error message was &lt;code&gt;ERR_PNPM_LINKING_FAILED&lt;/code&gt;, indicating a failure in the linking process during installation. This problem seemed unique to the macOS environment, particularly when used in conjunction with Docker.&lt;/p&gt;
&lt;h3&gt;Understanding the Cause&lt;/h3&gt;
&lt;p&gt;The root cause of this issue is tied to the way Docker interacts with the macOS file system. More specifically, it&apos;s related to how pnpm uses hard links to optimize dependency installations. Hard links work differently in Docker&apos;s virtualized file system on macOS compared to Linux containers, leading to the  error during the copying of files. (&lt;a href=&quot;https://github.com/misskey-dev/misskey/issues/10201&quot;&gt;ref&lt;/a&gt;)&lt;/p&gt;
&lt;h2&gt;Implementing a Solution&lt;/h2&gt;
&lt;p&gt;To address this, I modified the pnpm configuration to change the storage directory of the package store. This was achieved by adding a specific command in the &lt;code&gt;postCreateCommand&lt;/code&gt; section of the DevContainer configuration:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;postCreateCommand&quot;: &quot;pnpm config set store-dir /home/node/.local/share/pnpm/store &amp;amp;&amp;amp; pnpm install --frozen-lockfile&quot;,
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After changing the pnpm store directory, it was essential to add the new store directory, &lt;code&gt;.pnpm-store&lt;/code&gt;, to the &lt;code&gt;.gitignore&lt;/code&gt; file.&lt;/p&gt;
</content:encoded></item><item><title>Using OpenAI&apos;s Assistant API with Function Calling in Chatbots</title><link>https://ainoya.dev/posts/openai-assistant-api-and-function-calling/</link><guid isPermaLink="true">https://ainoya.dev/posts/openai-assistant-api-and-function-calling/</guid><description>A common pattern in chatbot conversations involving function calling within the backend during interactions with an OpenAI Assistant API.</description><pubDate>Sun, 21 Jan 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;In this post, I&apos;ll explain a common pattern in chatbot conversations involving function calling within the backend during interactions with an OpenAI Assistant API. This approach offers significant advantages in maintaining context and structuring responses. We&apos;ll delve into the benefits of using the &lt;a href=&quot;https://platform.openai.com/docs/assistants/overview&quot;&gt;Assistant API&lt;/a&gt; and &lt;a href=&quot;https://platform.openai.com/docs/guides/function-calling&quot;&gt;Function Calling&lt;/a&gt;, as well as the sequence of chat interactions and operational considerations.&lt;/p&gt;
&lt;h2&gt;Benefits of the Assistant API&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Thread Creation for Individual Users&lt;/strong&gt;: The Assistant API enables the creation of threads for each user, maintaining the context of conversations. This is crucial in providing a cohesive and personalized chat experience.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Setting Instructions for the Assistant&lt;/strong&gt;: The ability to set instructions for the assistant allows for more controlled and relevant responses, tailored to the specific needs of the application.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Ease of Testing in the Playground&lt;/strong&gt;: The API&apos;s &lt;a href=&quot;https://platform.openai.com/playground&quot;&gt;playground&lt;/a&gt; feature provides an easy-to-use environment for testing and fine-tuning the assistant’s responses.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Advantages of Function Calling&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Argument Structuring&lt;/strong&gt;: Function calling automates the process of structuring necessary arguments for function execution, making the backend processing more efficient.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Response Generation&lt;/strong&gt;: Without the need for special prompt tuning, the assistant generates responses that align with the structure of the function arguments, ensuring consistency and relevance in the dialogue.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Sequence Diagram of Chat Functionality&lt;/h2&gt;
&lt;p&gt;Although the overall process is well documented in OpenAI&apos;s official documents, a sequence diagram provides a clearer picture:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://platform.openai.com/docs/assistants/overview&quot;&gt;Assistants Overview - OpenAI API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://platform.openai.com/docs/assistants/how-it-works&quot;&gt;How Assistants Work - OpenAI API&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://ainoya.dev/posts/openai-assistant-api-and-function-calling/images/assistant-api-diagram.svg&quot; alt=&quot;Sequence Diagram of Chat Functionality&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Operational Considerations&lt;/h2&gt;
&lt;h3&gt;Security Concerns&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;When executing backend functions using argument names specified in &lt;code&gt;tool_calls&lt;/code&gt;, it&apos;s crucial to validate these arguments, especially in multi-tenant environments. This prevents potential data leaks if an incorrect customer ID is provided. Alternatively, backend functions can be augmented with tenant IDs or other identifiers to ensure secure and appropriate execution.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Operational Notes&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Pay attention to the &lt;a href=&quot;https://platform.openai.com/docs/assistants/how-it-works/run-lifecycle&quot;&gt;Lifecycle of a Thread Run&lt;/a&gt;. It&apos;s important to handle errors and check the state of a thread before transitioning it to the next state. A thread in an &lt;code&gt;in_progress&lt;/code&gt; state cannot be rerun, so implementing proper checks and error handling in the backend is essential for smooth operation.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Integrating the Assistant API with function calling in chatbot applications offers a streamlined and efficient way to handle user interactions. By maintaining context through threads, structuring responses appropriately, and ensuring secure and correct backend processing, developers can create more effective and user-friendly chatbot experiences. As always, careful consideration of the operational aspects, especially around security and state management, is crucial in deploying such systems.&lt;/p&gt;
</content:encoded></item><item><title>JIRA and GitHub Daily Workflow Script</title><link>https://ainoya.dev/posts/jira-github-daily-workflow-script/</link><guid isPermaLink="true">https://ainoya.dev/posts/jira-github-daily-workflow-script/</guid><description>A script that automates the daily workflow of managing tickets in JIRA and managing code in GitHub.</description><pubDate>Thu, 18 Jan 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;When managing tickets in JIRA and managing code in GitHub, the development workflow often involves the following steps:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Create a Git branch named after the JIRA ticket number.&lt;/li&gt;
&lt;li&gt;Retrieve the ticket title from JIRA using its ticket number.&lt;/li&gt;
&lt;li&gt;Make an empty commit in Git with the ticket number and title.&lt;/li&gt;
&lt;li&gt;Push the branch to GitHub.&lt;/li&gt;
&lt;li&gt;Create a draft Pull Request (PR) on GitHub and fill the PR template with the JIRA ticket link.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;To simplify this process, I crafted a script that accomplishes these tasks in one go. The usage is straightforward:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;./jw PRJ-1234
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The script leverages &lt;a href=&quot;https://github.com/go-jira/jira&quot;&gt;go-jira/jira&lt;/a&gt; and the &lt;a href=&quot;https://docs.github.com/en/github-cli/github-cli/about-github-cli&quot;&gt;GitHub CLI&lt;/a&gt;. It&apos;s important to have these CLI tools set up beforehand.&lt;/p&gt;
&lt;p&gt;Here’s an overview of the script:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#!/usr/bin/env ruby
# Fetch information from JIRA, create a branch,
# push it and create a draft PR

require &apos;json&apos;
require &apos;tempfile&apos;

# Retrieve the first argument
key=ARGV[0]
# If key is missing, output error
if key == nil then
  puts &quot;Please input a key&quot;
  exit
end
puts &quot;Fetching information for #{key}&quot;

info=JSON.parse(`jira view #{key} -t debug`)

parent_key=info.dig(&quot;fields&quot;, &quot;parent&quot;, &quot;key&quot;)
issue_summary=info[&quot;fields&quot;][&quot;summary&quot;]

comment=&quot;#{key} #{issue_summary}&quot;

# If parent_key exists, fetch information of the parent
if parent_key != nil then
  parent_info=JSON.parse(`jira view #{parent_key} -t debug`)
  parent_summary=parent_info[&quot;fields&quot;][&quot;summary&quot;]
  puts &quot;#{parent_summary}&quot;
  comment=&quot;#{key} #{parent_summary} / #{issue_summary}&quot;
end

puts &quot;Starting work on #{comment}&quot;

puts `git checkout master &amp;amp;&amp;amp; git pull origin master`
# Exit if there&apos;s an error
if $? != 0 then
  puts &quot;Failed to git pull&quot;
  exit
end

puts `git checkout -b #{key}`

puts `git commit --allow-empty -m &quot;#{comment}&quot;`

# Prompt for yes/no input
def prompt_for_yes_no
  loop do
    print &quot;Do you want to push? (yes/y/no): &quot;

    case $stdin.gets.chomp.downcase
    when &quot;yes&quot;
      return true
    when &quot;y&quot;
      return true
    else
      return false
    end
  end
end

push_command = &quot;git push origin #{key}&quot;

# Open .github/PULL_REQUEST_TEMPLATE.md and embed the key in the corresponding ticket line
#
# - {{JIRA_TICKET_LINK}}
#
template_path = &quot;.github/PULL_REQUEST_TEMPLATE.md&quot;
template = File.read(template_path)
template.gsub!(/{{JIRA_TICKET_LINK}}/, &quot;#{key}&quot;)
# Create a temporary file using Tempfile and write to it
pr_body = Tempfile.open do |f|
  f.write(template)
  f
end.path

# Create a DRAFT PR using the gh command
gh_command = &quot;gh pr create --title &apos;#{comment}&apos; --body-file #{pr_body}  --draft&quot;

if prompt_for_yes_no then
  puts `#{push_command}`
  # Create a DRAFT PR with the comment title using the GH command
  puts `#{gh_command}`
else
  puts &quot;Did not push. If you want to push and create a pr, please execute the following commands&quot;
  puts push_command
  puts gh_command
end

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Key Takeaways&lt;/h2&gt;
&lt;p&gt;This time, I developed the tool using ChatGPT and GitHub Copilot. In the past, writing small tools often meant programming while searching through Google. However, using ChatGPT and GitHub Copilot, the development process has evolved into something akin to sculpting from a block of Large Language Models (LLMs), which are like a compressed representation of the internet, as mentioned in &lt;a href=&quot;https://www.newyorker.com/tech/annals-of-technology/chatgpt-is-a-blurry-jpeg-of-the-web&quot;&gt;The New Yorker&lt;/a&gt;. Through prompts, I carve out tools that meet my specific needs, a process that feels like sculpting. This approach, much like a sculptor&apos;s craft, is an immensely enjoyable experience.&lt;/p&gt;
</content:encoded></item><item><title>Scheduled Article Publishing in Astro with Cloudflare Pages</title><link>https://ainoya.dev/posts/scheduled-post-astro/</link><guid isPermaLink="true">https://ainoya.dev/posts/scheduled-post-astro/</guid><description>Discover how to implement scheduled article publishing on an Astro-built website, hosted on Cloudflare Pages.</description><pubDate>Tue, 16 Jan 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Astro, a powerful framework for building modern websites, offers efficient static build capabilities. In collaboration with Cloudflare Pages, it provides a robust hosting solution. An intriguing feature I wanted to implement on my blog (hosted on Cloudflare Pages and built with Astro) was scheduled article publishing. This feature allows articles to be automatically published when the current date matches their predefined publication date.&lt;/p&gt;
&lt;h2&gt;Strategy for Scheduled Publishing&lt;/h2&gt;
&lt;p&gt;To achieve scheduled article publishing, I employed a two-step approach:&lt;/p&gt;
&lt;h3&gt;1. Adding &lt;code&gt;publishDate&lt;/code&gt; Metadata to Content Collection&lt;/h3&gt;
&lt;p&gt;Firstly, I added a &lt;code&gt;publishDate&lt;/code&gt; metadata field to the content collection. This date determines when an article should be published. By checking if this date matches the current date, the system decides whether to publish the post. Astro&apos;s documentation on &lt;a href=&quot;https://docs.astro.build/en/guides/content-collections/#working-with-dates-in-the-frontmatter&quot;&gt;content collections and handling dates&lt;/a&gt; was instrumental in implementing this.&lt;/p&gt;
&lt;h3&gt;2. Modifying the Build Script for Date Comparison&lt;/h3&gt;
&lt;p&gt;Secondly, I modified the build script to include a function that compares the &lt;code&gt;publishDate&lt;/code&gt; with the build date. This function filters out any posts that are marked as drafts or have a future publish date. The TypeScript code snippet below illustrates this process:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/** Filters out draft posts based on the environment and the post&apos;s publishDate **/
export async function getAllPosts() {
 return await getCollection(&quot;post&quot;, ({ data }) =&amp;gt; {
  const isNotDraft = import.meta.env.PROD ? data.draft !== true : true;
  // Treat posts with a future publishDate as drafts
  const isNotFuture = import.meta.env.PROD
   ? data.publishDate.valueOf() &amp;lt;= new Date().valueOf()
   : true;

  return isNotDraft &amp;amp;&amp;amp; isNotFuture;
 });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Automating Deployment with Cloudflare Pages and GitHub Actions&lt;/h2&gt;
&lt;p&gt;To ensure that this scheduled publishing mechanism works effectively, I automated the build and deployment process of the site. Cloudflare Pages offers a feature called &lt;a href=&quot;https://developers.cloudflare.com/pages/configuration/deploy-hooks/&quot;&gt;Deploy Webhook&lt;/a&gt;, which triggers the build and deployment of pages when it receives a POST request. I utilized GitHub Actions to send this POST request to the Deploy Webhook daily. Below is an example YAML configuration for the GitHub Actions workflow:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Only articles with a past or present publishDate are displayed in the Astro build
# Thus, we perform a daily deployment with a date condition check

name: &quot;Daily Deploy&quot;

on:
  # daily at 00:00 UTC
  schedule:
    - cron: &quot;0 0 * * *&quot;
  # for debugging purposes
  workflow_dispatch:

jobs:
  # Trigger deploy webhook with curl
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Deploy
        run: curl -X POST -d {} ${{ secrets.CLOUDFLARE_DEPLOY_WEBHOOK }}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This setup guarantees that the website is rebuilt daily, ensuring that any article with the current date as its &lt;code&gt;publishDate&lt;/code&gt; gets published automatically. This approach leverages the static build performance of Astro while introducing dynamic elements like scheduled publishing, making the blog more efficient.&lt;/p&gt;
</content:encoded></item><item><title>Securing k8s Service Communication with Envoy</title><link>https://ainoya.dev/posts/envoy-mtls-demo/</link><guid isPermaLink="true">https://ainoya.dev/posts/envoy-mtls-demo/</guid><description>Delve into the nuances of enhancing security in Kubernetes clusters by implementing mutual TLS (mTLS) using Envoy.</description><pubDate>Sun, 14 Jan 2024 08:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://ainoya.dev/posts/envoy-mtls-demo/images/envoy-mtls-demo.svg&quot; alt=&quot;Diagram&quot; /&gt;&lt;/p&gt;
&lt;p&gt;In this article, I&apos;ll introduce an example of implementing secure service-to-service communication using Envoy. The motivation for this experiment was to secure gRPC communications within a Kubernetes (k8s) cluster. While it&apos;s possible to implement secure authentication at the application layer, achieving this at the infrastructure layer can reduce the concerns in application implementation, offering significant benefits.&lt;/p&gt;
&lt;p&gt;For this experiment, I tried communication using client certificate authentication. The code for this experiment is stored in the following GitHub repository:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/ainoya/envoy-mtls-demo&quot;&gt;github.com/ainoya/envoy-mtls-demo&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The setup for the sample realized in k8s involved using Envoy as a sidecar in k8s. The communication structure was between a client-side pod (application container + Envoy sidecar container) and a server-side pod (Envoy sidecar container + application container).&lt;/p&gt;
&lt;h3&gt;Implementing mTLS with Envoy&lt;/h3&gt;
&lt;h4&gt;Generating Client Certificates&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Generating client certificates is straightforward using cfssl.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Generating Server Certificates&lt;/h4&gt;
&lt;h4&gt;Envoy Configuration&lt;/h4&gt;
&lt;p&gt;To ensure that only specific client certificates are accepted in Envoy, use &lt;code&gt;validation_context&lt;/code&gt; and &lt;a href=&quot;https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/transport_sockets/tls/v3/common.proto#envoy-v3-api-field-extensions-transport-sockets-tls-v3-certificatevalidationcontext-match-typed-subject-alt-names&quot;&gt;&lt;code&gt;match_typed_subject_alt_names&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;First, create a Certificate Authority (CA):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Example CA creation
$ cat certs/ca/ca-csr.json
{
    &quot;CN&quot;: &quot;app-ca&quot;,
    &quot;hosts&quot;: [&quot;&quot;],
    &quot;key&quot;: {
        &quot;algo&quot;: &quot;ecdsa&quot;,
        &quot;size&quot;: 256
    },
    &quot;names&quot;: [
        {
            &quot;O&quot;: &quot;app-ca&quot;,
            &quot;OU&quot;: &quot;development&quot;,
            &quot;ST&quot;: &quot;Tokyo&quot;,
            &quot;C&quot;: &quot;JP&quot;
        }
    ],
    &quot;ca&quot;: {
        &quot;expiry&quot;: &quot;876000h&quot;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# Create the CA key and certificate with cfssl, cfssljson
cd certs/ca
cfssl genkey -initca ca-csr.json | cfssljson -bare ca
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next, generate server certificates using this CA:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Example server certificate creation
$ cat certs/server/server-config.json
{
  &quot;signing&quot;: {
    &quot;default&quot;: {
      &quot;expiry&quot;: &quot;876000h&quot;,
      &quot;usages&quot;: [
        &quot;signing&quot;,
        &quot;key encipherment&quot;,
        &quot;server auth&quot;
      ]
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;$ cat certs/server/server.json
{
  &quot;CN&quot;: &quot;app-internal-api&quot;,
  &quot;hosts&quot;: [&quot;&quot;],
  &quot;key&quot;: {
    &quot;algo&quot;: &quot;ecdsa&quot;,
    &quot;size&quot;: 256
  },
  &quot;names&quot;: [
    {
      &quot;ST&quot;: &quot;Tokyo&quot;,
      &quot;C&quot;: &quot;JP&quot;
    }
  ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;cd certs/server
cfssl gencert -ca=../ca/ca.pem -ca-key=../ca/ca-key.pem \
  -config=./server-config.json server.json | cfssljson -bare server
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Finally, generate client certificates that will communicate with this server:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ cat certs/client/client-config.json
{
  &quot;signing&quot;: {
    &quot;default&quot;: {
      &quot;expiry&quot;: &quot;876000h&quot;,
      &quot;usages&quot;: [
        &quot;signing&quot;,
        &quot;key encipherment&quot;,
        &quot;client auth&quot;
      ]
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;$ cat certs/client/client.json
{
  &quot;CN&quot;: &quot;appclient&quot;,
  &quot;hosts&quot;: [&quot;app-internal-api&quot;],
  &quot;key&quot;: {
    &quot;algo&quot;: &quot;ecdsa&quot;,
    &quot;size&quot;: 256
  },
  &quot;names&quot;: [
    {
      &quot;ST&quot;: &quot;Tokyo&quot;,
      &quot;C&quot;: &quot;JP&quot;
    }
  ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Point to note: The &lt;code&gt;hosts&lt;/code&gt; section should be set with names to be validated by &lt;code&gt;match_typed_subject_alt_names&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cd certs/client
cfssl gencert -ca=../ca/ca.pem -ca-key=../ca/ca-key.pem \
  -config=./client-config.json client.json | cfssljson -bare client
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With &lt;code&gt;match_typed_subject_alt_names&lt;/code&gt;, requests trying to authenticate with a client certificate that doesn&apos;t match the host name will be rejected with a &lt;code&gt;CERTIFICATE_VERIFY_FAILED&lt;/code&gt; error.&lt;/p&gt;
&lt;p&gt;For more detailed examples of Envoy configuration, refer to the demo repository:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/ainoya/envoy-mtls-demo/blob/main/envoy-conf/app-internal-api-envoy.yaml&quot;&gt;Server-side Envoy Configuration&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/ainoya/envoy-mtls-demo/blob/main/envoy-conf/example-mtls-access-envoy-conf.yaml&quot;&gt;Client-side Envoy Configuration&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Manifest Configuration for Deployment in k8s&lt;/h3&gt;
&lt;p&gt;For configuring the client-side k8s manifest, consider the following setup:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Include processes that terminate the sidecar when the main container exits.
&lt;ul&gt;
&lt;li&gt;Share an &lt;code&gt;emptyDir&lt;/code&gt; volume between the main container and the sidecar container, and create a file upon exit of the main container. The sidecar should terminate itself upon detecting this file.&lt;/li&gt;
&lt;li&gt;From k8s 1.28 onwards, use &lt;code&gt;initContainer&lt;/code&gt; and &lt;code&gt;restartPolicy:Always&lt;/code&gt; to avoid such configurations. &lt;a href=&quot;https://kubernetes.io/blog/2023/08/25/native-sidecar-containers/&quot;&gt;Kubernetes v1.28: Introducing native sidecar containers&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Ensure that the application starts after the sidecar is ready.&lt;/li&gt;
&lt;li&gt;Here&apos;s an excerpt of the configuration:&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;            - name: app
     ...
              command:
                - /bin/bash
                - -c
                # 1. Set up a trap to terminate the envoy sidecar after the batch process ends.
                # 2. Simple waiting process until Envoy starts
                - trap &quot;touch /tmp/pod/main-terminated&quot; EXIT &amp;amp;&amp;amp; while ! nc -z 0.0.0.0 2443; do echo &quot;Waiting for the envoy sidecar to be up...&quot;; sleep 1; done &amp;amp;&amp;amp; /path/your/application-command $@
                - --
              args:
                - # application args
            - name: envoy
              image: envoyproxy/envoy:v1.28-latest
              command:
                - /bin/sh
                - -c
              args:
                - |
                  envoy \
                   -c /etc/envoy/client-conf.yaml &amp;amp;
                   CHILD_PID=$!
                   (while true; do if [[ -f &quot;/tmp/pod/main-terminated&quot; ]]; then kill $CHILD_PID; echo &quot;Killed $CHILD_PID as the main container terminated.&quot;; fi; sleep 1; done) &amp;amp;
                   wait $CHILD_PID
                   if [[ -f &quot;/tmp/pod/main-terminated&quot; ]]; then exit 0; echo &quot;Job completed. Exiting...&quot;; fi
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;gRPC Load Balancing&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;gRPC communicates over an established TCP connection, which can lead to load imbalances if the connections are unevenly distributed across backend servers.&lt;/li&gt;
&lt;li&gt;Normally, k8s services function as L4 load balancers and cannot manage L7 load distribution.&lt;/li&gt;
&lt;li&gt;To address this, start k8s services in &lt;a href=&quot;https://kubernetes.io/docs/concepts/services-networking/service/#headless-services&quot;&gt;headless mode&lt;/a&gt; and let Envoy handle the load balancing. In this setup, the Envoy sidecar container on the client side plays this role.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Conclusion&lt;/h3&gt;
&lt;h4&gt;Further Developments&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;For large-scale applications of this mTLS setup, consider using &lt;a href=&quot;https://docs.aws.amazon.com/app-mesh/latest/userguide/what-is-app-mesh.html&quot;&gt;AWS AppMesh&lt;/a&gt; or &lt;a href=&quot;https://istio.io/&quot;&gt;Istio&lt;/a&gt;, which can automate configurations.
&lt;ul&gt;
&lt;li&gt;Sidecar configurations can be automatically injected using a mutating webhook.
&lt;ul&gt;
&lt;li&gt;Example with AppMesh: &lt;a href=&quot;https://aws.github.io/aws-app-mesh-controller-for-k8s/reference/injector/&quot;&gt;SidecarInjection - AWS App Mesh Controller&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Integration of Istio with Vault can automate certificate issuance.
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://istio.io/v1.2/docs/tasks/security/vault-ca/&quot;&gt;Istioldie 1.2 / Istio Vault CA Integration&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This article aimed to provide practical insights into setting up secure service-to-service communication within a Kubernetes cluster using Envoy and mTLS. Feel free to refer to the provided repository for a hands-on demonstration and detailed　configuration examples.&lt;/p&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.cloudflare.com/learning/access-management/what-is-mutual-tls/&quot;&gt;What is mTLS? | Mutual TLS | Cloudflare&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Enhancing Tools with OpenAI API: Insights from Past Projects</title><link>https://ainoya.dev/posts/chatgpt-prototyping-2023/</link><guid isPermaLink="true">https://ainoya.dev/posts/chatgpt-prototyping-2023/</guid><description>Converting natural language queries into structured formats made easy</description><pubDate>Wed, 10 Jan 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Early in the previous year, following the launch of OpenAI&apos;s GPT-3.5, I initiated a project to improve existing tools&apos; user experience using the OpenAI API. This article focuses on two significant instances from this project.&lt;/p&gt;
&lt;h2&gt;Transforming Unstructured Data into Structured, Machine-Readable Formats&lt;/h2&gt;
&lt;p&gt;One of the key features I focused on was the ability to easily convert unstructured data into structured, machine-readable formats like JSON. This capability was utilized in two distinct applications, both of which were rapidly prototyped using LangChain and Streamlit.&lt;/p&gt;
&lt;h3&gt;Example 1: &lt;a href=&quot;https://github.com/ainoya/graphql-gpt&quot;&gt;ainoya/graphql-gpt&lt;/a&gt;&lt;/h3&gt;
&lt;h4&gt;Converting Natural Language Queries into GraphQL&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;The aim was to transform natural language questions into GraphQL queries.&lt;/li&gt;
&lt;li&gt;In-context learning was employed, embedding the GraphQL schema within the prompt, to facilitate this conversion.&lt;/li&gt;
&lt;li&gt;Additionally, the implementation included executing the resulting queries and retrieving results from a GraphQL server. A protective layer for query execution would likely be necessary to prevent data retrieval beyond the intended scope in real-world applications.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Example 2: &lt;a href=&quot;https://github.com/ainoya/gpt-looker&quot;&gt;ainoya/gpt-looker&lt;/a&gt;&lt;/h3&gt;
&lt;h4&gt;Natural Language to Looker Queries&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Similar to the first example, this project involved converting natural language questions into Looker queries.&lt;/li&gt;
&lt;li&gt;LookML schemas were embedded in the prompt for in-context learning responses.&lt;/li&gt;
&lt;li&gt;Due to the size of LookML definitions, there was a challenge of context overflow when included directly in the prompt. This was addressed by preprocessing LookML into segments, converting them into embeddings, and storing them on &lt;a href=&quot;https://www.trychroma.com/&quot;&gt;Chroma&lt;/a&gt;. During a query, the most relevant schema was searched and retrieved, a technique known as Retrieval Augmented Generation (RAG).&lt;/li&gt;
&lt;li&gt;Given that Looker is a Google product, I anticipate similar functionalities may be implemented in tools like &lt;a href=&quot;https://deepmind.google/technologies/gemini/#introduction&quot;&gt;Gemini by Google DeepMind&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;The experimentation highlighted the astonishing ease with which unstructured data processing, previously demanding specialized machine learning expertise, can now be implemented. Recent developments, like the &lt;a href=&quot;https://openai.com/blog/chatgpt-plugins#code-interpreter&quot;&gt;Code Interpreter&lt;/a&gt; by OpenAI, further simplify the implementation of such AI-enhanced service experiences, promising widespread adoption and continuous improvement in the field.&lt;/p&gt;
</content:encoded></item><item><title>Developing an OpenAI Java API Client</title><link>https://ainoya.dev/posts/openai-java-generated-client/</link><guid isPermaLink="true">https://ainoya.dev/posts/openai-java-generated-client/</guid><description>A look into the development of an OpenAI Java API client using the OpenAPI Generator.</description><pubDate>Mon, 08 Jan 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Last year in November, I took on the task of developing a Java API client for OpenAI&apos;s ChatGPT Assistant API, as there was a notable absence of such a tool for Java developers. The result of this endeavor is hosted on &lt;a href=&quot;https://github.com/ainoya/openai-java-generated-client&quot;&gt;GitHub - ainoya/openai-java-generated-client&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Utilizing OpenAI&apos;s API Specification&lt;/h2&gt;
&lt;p&gt;The project was inspired by OpenAI&apos;s publicly shared API specification on &lt;a href=&quot;https://github.com/openai/openai-openapi&quot;&gt;GitHub - openai/openai-openapi&lt;/a&gt;. This led me to explore the potential of automating the client generation using the &lt;a href=&quot;https://github.com/OpenAPITools/openapi-generator&quot;&gt;OpenAPI Generator&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;Encountered Challenges&lt;/h3&gt;
&lt;h4&gt;Spec vs. Reality&lt;/h4&gt;
&lt;p&gt;A significant challenge arose from discrepancies between the OpenAI specification and its actual implementation. Mismatches in response key names necessitated direct API calls for correction, resulting in a modified API specification available at &lt;a href=&quot;https://github.com/ainoya/openai-java-generated-client/blob/main/openapi.yaml&quot;&gt;openapi.yaml&lt;/a&gt;. The Assistant API functions well now, though other parts may still require updates.&lt;/p&gt;
&lt;h4&gt;Mastering OpenAPI Generator Options&lt;/h4&gt;
&lt;p&gt;Deciphering the options available in &lt;code&gt;openapi-generator&lt;/code&gt; required thorough research and understanding, aided by the &lt;a href=&quot;https://openapi-generator.tech/docs/generators/java/&quot;&gt;official documentation&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;The Effective Docker Command&lt;/h3&gt;
&lt;p&gt;Running the OpenAPI Generator in Docker proved most efficient. The finalized command for generating the client is:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker run --rm -v ${PWD}:/local openapitools/openapi-generator-cli:v7.1.0 generate \
    -i /local/openapi.yaml --template-dir=/local/custom_template \
    -g java \
    --additional-properties=disallowAdditionalPropertiesIfNotPresent=false,artifactId=openai-java-generated-client,groupId=com.github.ainoya,invokerPackage=com.github.ainoya.client,modelPackage=com.github.ainoya.client.model,developmerName=&quot;Naoki Ainoya&quot;,developerEmail=&quot;xxx&quot;,developerOrganization=&quot;ainoya.dev&quot;,developerOrganizationUrl=&quot;https://ainoya.dev&quot;,artifactVersion=0.0.3 \
    --api-package com.github.ainoya.openai.client.api \
    -o /local/ &amp;amp;&amp;amp; \
    ./gradlew test
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Auto-Generated Unit Testing&lt;/h3&gt;
&lt;p&gt;The OpenAPI Generator&apos;s capability to auto-generate unit tests was highly beneficial. The command includes running &lt;code&gt;./gradlew test&lt;/code&gt; after generation, enabling quick identification of any issues.&lt;/p&gt;
&lt;h2&gt;Opting for JitPack over GitHub Packages&lt;/h2&gt;
&lt;p&gt;While initially considering GitHub Packages, its constraints did not align with my usage requirements. Consequently, I opted for JitPack, which proved more suitable for my needs. The API client is available at &lt;a href=&quot;https://jitpack.io/#ainoya/openai-java-generated-client&quot;&gt;JitPack - ainoya/openai-java-generated-client&lt;/a&gt;. The need for a Private Access Token (PAT) even for public packages on GitHub Packages was a deciding factor, as elaborated in &lt;a href=&quot;https://github.com/orgs/community/discussions/25629&quot;&gt;GitHub Community Discussion #25629&lt;/a&gt;.&lt;/p&gt;
</content:encoded></item><item><title>Automating LookML Code Review with lkml and GitHub Actions</title><link>https://ainoya.dev/posts/lookml-test-lkml/</link><guid isPermaLink="true">https://ainoya.dev/posts/lookml-test-lkml/</guid><description>How to automate LookML code review using the lkml Python library and GitHub Actions.</description><pubDate>Sun, 07 Jan 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;In the world of data analytics, &lt;a href=&quot;https://cloud.google.com/looker/docs&quot;&gt;Looker&lt;/a&gt; has become an indispensable tool for many teams. Its core feature, LookML, allows users to define data models and manage projects efficiently through Git. This setup facilitates team development by enabling version control and collaborative work. However, as teams grow, ensuring adherence to specific LookML coding standards becomes increasingly challenging, especially during code reviews.&lt;/p&gt;
&lt;p&gt;A particularly useful tool in this context is &lt;a href=&quot;https://www.spectacles.dev/&quot;&gt;Spectacles&lt;/a&gt;, which assists in validating LookML code. Yet, there are times when teams require custom static analysis to enforce specific coding practices. This is where the &lt;a href=&quot;https://github.com/joshtemple/lkml&quot;&gt;&lt;code&gt;lkml&lt;/code&gt;&lt;/a&gt; Python library comes into play, providing an easy way to create custom static analysis scripts.&lt;/p&gt;
&lt;p&gt;For instance, consider a team wanting to enforce a consistent data protection policy across all LookML &lt;code&gt;explore&lt;/code&gt; definitions by using &lt;code&gt;access_filter&lt;/code&gt;. The &lt;code&gt;lkml&lt;/code&gt; library enables the creation of a Python script to check this automatically.&lt;/p&gt;
&lt;p&gt;Here&apos;s an example script that parses LookML files, identifies &lt;code&gt;explore&lt;/code&gt; definitions, and checks for the presence of specific &lt;a href=&quot;https://cloud.google.com/looker/docs/reference/param-explore-access-filter&quot;&gt;&lt;code&gt;access_filter&lt;/code&gt;&lt;/a&gt; settings:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Script to parse LookML and check for issues with access_filter
# To be used in CI
import lkml
import pprint
import glob

for lkml_file_path in glob.glob(&apos;**/*.lkml&apos;, recursive=True):
    with open(lkml_file_path) as file:
        print(&quot;reading: {}&quot;.format(lkml_file_path))
        result = lkml.load(file)
        if not &apos;explores&apos; in result:
            print(&quot;{} does not contain explore definitions. Skipping check for access_filter conditions.&quot;.format(lkml_file_path))
            continue
        else:
            print(&quot;{} contains explore definitions. Checking access_filter conditions.&quot;.format(lkml_file_path))

        for explore in result[&apos;explores&apos;]:
            print(&quot;Checking access_filter conditions for explore name: {}&quot;.format(explore[&apos;name&apos;]))
            try:
                access_filter = explore[&apos;access_filters&apos;][0]
                pprint.pprint(access_filter)
                if access_filter[&apos;user_attribute&apos;] != &apos;tenant_id&apos;:
                    raise Exception(&quot;Please specify user_attribute: tenant_id in access_filter conditions. There are issues with the scope of data exposure.&quot;)
            except KeyError:
                raise Exception(&quot;Please set access_filter conditions in explore. There are issues with the scope of data exposure.&quot;)
            print(&quot;ok&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This script can be integrated into a continuous integration (CI) pipeline, offering a more efficient and reliable way to ensure coding standards than manual code reviews. For teams managing their code repositories on GitHub, this script can be easily incorporated into their workflow using GitHub Actions.&lt;/p&gt;
&lt;p&gt;Below is a sample GitHub Actions workflow definition that automates the execution of the script upon every push to the repository:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;name: lookml test

on:
  push:

jobs:
  lookml-test:
    name: lookml-test
    timeout-minutes: 10
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v3.3.0
    - name: Set up Python ${{ matrix.python-version }}
      uses: actions/setup-python@v4
      with:
        python-version: &quot;3.10&quot;
    - name: Install dependencies
      run: |
        python -m pip install --upgrade pip
        pip install lkml
    - name: Test
      run: |
        python scripts/lkml_validator/test.py
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This setup not only saves time but also enhances the reliability of code reviews by automating the validation of &lt;code&gt;access_filter&lt;/code&gt; configurations in LookML files. It&apos;s a practical example of how tools like &lt;code&gt;lkml&lt;/code&gt; and GitHub Actions can streamline development processes in the data analytics domain.&lt;/p&gt;
</content:encoded></item><item><title>Exploring AI CLI Tool Development with Rust: GIGO</title><link>https://ainoya.dev/posts/gigo/</link><guid isPermaLink="true">https://ainoya.dev/posts/gigo/</guid><description>Garbage in, garbage out</description><pubDate>Sat, 06 Jan 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;In the dynamic world of software engineering, the integration of efficient tools can significantly streamline the workflow. With the advent of AI-powered solutions like ChatGPT, there&apos;s a growing need for accessible interfaces to interact with these systems. My recent project, a command-line interface (CLI) tool named GIGO, is a testament to this requirement and my personal journey into Rust programming.&lt;/p&gt;
&lt;h2&gt;The Genesis of GIGO&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Identifying the Need&lt;/strong&gt;: As a software engineer, I often found myself in situations where I needed quick, dummy inputs for testing or various tasks. A direct, terminal-based tool to interact with ChatGPT seemed like an ideal solution.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Why Rust?&lt;/strong&gt;: Despite having no prior experience with Rust, my curiosity to explore this language was the driving force behind this project. Rust, known for its safety and performance, seemed like a fitting choice for a tool that would interact with AI models.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Building GIGO&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;The Project&lt;/strong&gt;: I embarked on creating GIGO, a minimalist CLI tool, which you can find on GitHub at &lt;a href=&quot;https://github.com/ainoya/gigo&quot;&gt;ainoya/gigo&lt;/a&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Functionality&lt;/strong&gt;: While GIGO&apos;s features are currently limited, it efficiently serves its primary function. Inspired by the phrase &quot;garbage in, garbage out,&quot; I chose this name to add a touch of humor. The tool allows users to input prompts via CLI, which then return responses from ChatGPT.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Usage Example&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;echo &apos;Please write a short story about a robot who is trying to learn how to be a human.&apos; | gigo

Once upon a time, ...
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Reflections and Future Prospects&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Rust Experience&lt;/strong&gt;: Although I&apos;ve only scratched the surface of Rust&apos;s capabilities, I found its pattern matching and type-safe error handling to be particularly appealing. An example can be seen in the project&apos;s code (&lt;a href=&quot;https://github.com/ainoya/gigo/blob/ca51c240694db5df7382a4a19c457ce369f58b2f/src/main.rs#L29&quot;&gt;here&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;GIGO&apos;s Potential&lt;/strong&gt;: Currently, GIGO interfaces solely with OpenAI&apos;s API. However, I believe enhancing its compatibility with various large language models could make it more versatile and interesting.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;My foray into developing GIGO has been both a learning curve and a step towards addressing a niche requirement in the software engineering domain. While it&apos;s a small-scale project, the potential for GIGO to evolve and integrate more features is something I look forward to exploring. This journey has not only provided me with a practical tool but also a valuable introduction to Rust programming.&lt;/p&gt;
</content:encoded></item><item><title>Absence Alert System using SwitchBot</title><link>https://ainoya.dev/posts/switchbot-absence-alert/</link><guid isPermaLink="true">https://ainoya.dev/posts/switchbot-absence-alert/</guid><description>Absence Alert System using SwitchBot Motion Sensor and Cloudflare Workers</description><pubDate>Wed, 03 Jan 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Enhancing Home Security with a Custom-Built Motion Detection Alert System using SwitchBot and Cloudflare Workers&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://ainoya.dev/posts/switchbot-absence-alert/images/diagram.svg&quot; alt=&quot;Diagram&quot; /&gt;&lt;/p&gt;
&lt;p&gt;As smart home technologies evolve, the need for more sophisticated monitoring systems becomes apparent. While standard motion sensors, like those from SwitchBot, are great at detecting movement and triggering immediate actions, they often lack the capability to alert users about the absence of motion for extended periods. This is crucial in scenarios where non-activity can be as significant as activity, such as monitoring the well-being of elderly relatives or ensuring that a seldom-used space remains undisturbed.&lt;/p&gt;
&lt;p&gt;To address this, I&apos;ve developed an innovative system using SwitchBot&apos;s motion detection capabilities, combined with the power of Cloudflare Workers, to create an alert mechanism for both motion detection and the lack of it over extended periods, specifically beyond the 30-minute mark, which is the limit in SwitchBot&apos;s default settings.&lt;/p&gt;
&lt;h2&gt;How the System Works&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Extended Absence Detection:&lt;/strong&gt; The SwitchBot sensors are excellent at motion detection, but they can&apos;t natively send alerts for periods of inactivity longer than 30 minutes. My system overcomes this by using Cloudflare Workers alongside the SwitchBot API to detect absence of motion over intervals longer than 30 minutes.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Integration with Cloudflare Workers:&lt;/strong&gt; Upon detecting motion, the SwitchBot sensor triggers a webhook request to Cloudflare Workers. These Workers then log the time of the motion event in Cloudflare&apos;s Key-Value (KV) storage system.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Scheduled Checks and Notifications:&lt;/strong&gt; A scheduled Cloudflare Worker periodically checks the last recorded motion timestamp in the KV store. If it detects that no motion has been recorded for over 6 hours, it triggers a notification.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Utilizing Pushover for Alerts:&lt;/strong&gt; For sending notifications, I chose Pushover – a convenient app for sending push notifications to smartphones. It&apos;s ideal for personal systems where you want to integrate push notifications easily.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Development and Resources&lt;/h2&gt;
&lt;p&gt;The implementation was guided by the official API documentation of SwitchBot (&lt;a href=&quot;https://github.com/OpenWonderLabs/SwitchBotAPI&quot;&gt;SwitchBot API&lt;/a&gt;) and Cloudflare Workers (&lt;a href=&quot;https://developers.cloudflare.com/workers/&quot;&gt;Cloudflare Workers Documentation&lt;/a&gt;). The ease of use of Cloudflare Workers, particularly for setting up web APIs and using the KV store for data persistence, made it an excellent choice for this project. The deployment process was streamlined thanks to well-organized Wrangler commands.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;This project showcases how combining existing smart home devices with powerful cloud services like Cloudflare Workers can result in highly functional and customized home security solutions. The ability to monitor both presence and absence of motion opens up new avenues for home monitoring, ensuring peace of mind when it comes to the safety and security of our homes and loved ones.&lt;/p&gt;
&lt;p&gt;The implementation details, code  are available on &lt;a href=&quot;https://github.com/ainoya/switchbot-absence-alert&quot;&gt;my GitHub repository&lt;/a&gt;. I welcome fellow enthusiasts to explore, fork, and adapt this system to their requirements.&lt;/p&gt;
</content:encoded></item><item><title>My first post</title><link>https://ainoya.dev/posts/my-first-post/</link><guid isPermaLink="true">https://ainoya.dev/posts/my-first-post/</guid><description>This is my first post</description><pubDate>Sat, 30 Dec 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I wanted to share a small update about some changes I&apos;ve made to my website recently.&lt;/p&gt;
&lt;h2&gt;Switching Domains to Cloudflare&lt;/h2&gt;
&lt;p&gt;I&apos;ve moved my domains, ainoya.dev and ainoya.io, over to Cloudflare. This is a step towards improving the overall performance of my website.&lt;/p&gt;
&lt;h2&gt;Hosting with Cloudflare Pages&lt;/h2&gt;
&lt;p&gt;In addition to the domain migration, my website is now hosted on Cloudflare Pages. This change is primarily for ease of maintenance and better integration with my existing setup. I&apos;m hoping that this move will make the website more efficient and user-friendly.&lt;/p&gt;
&lt;h2&gt;Built with Astro&lt;/h2&gt;
&lt;p&gt;For the website&apos;s framework, I&apos;ve chosen Astro. I&apos;m using the Cactus theme from &lt;a href=&quot;https://astro.build/themes/&quot;&gt;Astro&apos;s collection&lt;/a&gt;, which can be found here: &lt;a href=&quot;https://github.com/chrismwilliams/astro-theme-cactus&quot;&gt;Cactus Theme&lt;/a&gt; on GitHub. It&apos;s a simple yet effective theme that suits the content and structure of my site.&lt;/p&gt;
&lt;h2&gt;Content Focus&lt;/h2&gt;
&lt;p&gt;Going forward, the blog will mainly feature posts about things I learn in the tech field. This could include new programming concepts, insights into different technologies, or updates on personal projects. The idea is to share knowledge and experiences in a straightforward and accessible way.&lt;/p&gt;
&lt;p&gt;I&apos;m looking forward to this new phase and hope you find the posts informative and interesting. Feel free to reach out if you have any thoughts or questions.&lt;/p&gt;
&lt;p&gt;Thank you for reading!&lt;/p&gt;
</content:encoded></item></channel></rss>