Why is Cloud Computing important for us? This post is a reply to an extensive comment by Sean Kearon to the previous Cloud Computing post . I agree that the availability of decent distributed computing resources is not crucial to success. Yet, having opportunity of leveraging cloud computing could save a few resources for the customer. And sometimes this is a success factor. For example, one of my past projects had to be developed in a situation where customer’s resources were extremely constrained. That was actually one of the reasons why I was interested in that project back then – scarce resources can give you plenty of ideas for the highly-efficient project development and delivery. It was a simple implementation of xLim approach with modular desktop Smart Client and a web dashboard. Production version was living fine on a plain web hosting account. We didn’t even need to scale out to multiple hosting accounts (although this option was embedded in the architectural design as a cheap way of expanding capacity). In the end everyday costs for the customer were quite low. Yet, eventually there was a need for a customer to go to the dedicated server for two reasons: documents data store grew beyond 4Gbs of size (we hit a hosting limitation) and an automation engine had to be introduced. This has obviously raised monthly bill for the customer. Windows Azure (if it existed in a mature form at the moment) could’ve saved from these unnecessary expenses simply by providing a scalable storage and a worker role on the pay-as-you-go basis. This kind of expenses optimization is obviously possible in more complex and costly projects (at a larger scale). Although there always will be specific situations when it is cheaper and more efficient to invest into owned and hosted computing resources, they do not make it into a high percentage. One of the points of the cloud computing as a business model is optimization of the expenses. This happens from two sides of the story: Consumers of the cloud computing services don’t have to invest into the resources up-front (where resources could be from a dedicated server to a data center) in order to have the capacities and functionality meeting their demands. Neither do they have to over-invest in order to have the capacities meeting their peak demands (i.e.: having these extra few servers in a rack just to handle holiday sales spikes). Consumers are also saved from the maintenance-related expenses and gain the flexibility of getting rid of the resources, should such a business need arise. Getting rid of a data center is a bit harder. Providers of the cloud computing (any large hosting provider with his data own datacenter is a possible candidate for that) could utilize their existing computing resources more efficiently by shifting them into the cloud and linking with flexible pricing strategies. For example, say, we have a lot of idle CPU cycles scattered around the data center on weekend nights in USA. Would not it be more efficient to sell them for the computing purposes (at a flexible rate) instead of just letting the resource sit around? This ability to optimize usage of resources at various levels of the picture is one of the primary factors why the cloud computing hype is more than a temporary buzzword ( energy bills represent the second factor). Basically one could say that the cloud computing is a development of the virtualization story that could work at a higher level of detalization. Utility computing , that allows (or would allow in near future) efficient distributed computing on-demand for CPU-intensive or data-intensive operations, is just one of the extreme examples of the opportunities provided by the idea of cloud computing. It has its benefits but certainly 90% of the companies don’t even need it at the moment. High entry barrier in front of this technology (especially for .NET scenarios) further lowers its usability. Hosted development environments could obviously also be considered as some form of cloud computing that helps to optimize developers' expenses. This market niche is evolving quite fast. For a $7-$15 per month one could get the following pre-configured setup: Issue Tracker Version Control System Wiki and documentation management For $60+ per month one could get a brand new Windows Server 2008 virtual machine that could host all these above and an integration server. Comments, thoughts? If you are interested about development of cloud computing solutions under the .NET stack - check out xLim 4 . Related Posts: Microsoft is Reinventing CQRS for Windows Azure, but without DDD DDDD, CQRS and Other Enterprise Development Buzz-words


Windows Azure Storage Can Be Confusing

Windows Azure Storage Can Be Confusing I'm currently working on some blob storage abstractions needed for the Lokad.CQRS project . This involves writing some unit tests, which happen to be producing really strange results , while using conditional headers for the BLOB operations . Conditional headers are part of HTTP RFC: if-match if-modified-since if-none-match if-unmodified-since These headers are really important for implementing efficient storage operations (i.e.: caching large blobs locally) and performing reliable atomic updates (when write operation checks, if record was updated since the read). Azure Blob Storage Rest API supports conditional headers. .NET Storage Client supports them as well in form of BlobRequestOptions that could be passed to methods. That's the theory. In practice things get really confusing and tend to waste your day. Let's examine a single method OpenRead , which opens a stream for reading blob's contents. Look at this snippet: var options = new BlobRequestOptions() { AccessCondition = AccessCondition.IfMatch(cachedTag) }; using (var stream = _blob.OpenRead(options)) { read(stream); } What would you expect the outcome to be? Documentation does not say anything special about the behavior of the BlobRequestOptions passed to the OpenRead. Here's how it works on my machine: If item is stored properly in Azure Blob, then: IfUnmodifiedSince results in exception, which makes sense. IfModifiedSince is ignored (which might be step away from what is defined in RFC ) if blob (or the container) does not exist, then: IfNoneMatch with non-existent ETag results in 404 (Not Found) IfMatch with non-existent ETag results in 412 (precondition failed), which is a step away from the RFC . Now, since .NET documentation does not help us much, we could do some debugging and figure out the actual REST operations being performed underneath. This leads us to understanding that OpenRead , among the many other things, calls Get Block List method . Documentation says: This operation also supports the use of conditional headers to read the blob only if a specified condition is met. For more information, see Specifying Conditional Headers for Blob Service Operations. However, if we look at the Operations Supporting Conditional Headers , then GetBlockList operation is not even listed there. So we've got a few potential problems here: something could be completely wrong with my machine, producing constantly misleading results; REST API documentation for Windows Azure Blob Storage might be a bit outdated and confusing; Azure Dev Storage might produce really weird results depending on the type of the header passed; .NET documentation for the StorageClient does not say a word about how methods are in fact supposed to work. And that's just a single method; there are more. I've started creating questions on MSDN forum, but quickly gave up, since the next step should've been debugging into the server-side API implementation)). Update: it gets even more fun. Here's how a simple unit test suite for a single method (wrapper around blob reading) looks on the development fabric: Now if we switch the credentials to use real Windows Azure Fabric: As you can see, Windows Azure Dev Storage and Production Storage have behavior that differs . This should be accounted for, while developing and deploying applications (ensuring that the proper retry policies and delays are applied to give production storage some time for processing the operations like recreating container with the same name). Hopefully Microsoft will clear up the situation. Meanwhile, it's recommended to make sure to debug and double check every single method . Or, as L.M.Bujold has said: Check your assumptions. In fact, check your assumptions at the door.


You can build a house with big LEGO blocks. Can you add a nice porch? Company buys some business framework that seems to match their needs in the short-term perspective. It is easy and fun to use it initially - you just need to work with some big building blocks and the framework will do the rest for you. As the time goes, company gets to met new requirements that steer to them from the probable future that they've never considered (this always happens). Every encounter will be like a gamble. You've got 80% that you can deliver solution that meets this requirement using just the provided building blocks. And there is 20% chance that you will just will not have LEGOs for a trivial task of simply adding a nice porch to the house (and if you keep on gambling, you will always lose, unless you control the game). Company could start asking the LEGO manufacturer for the "porch building block" accidentally spiking the community discussion about it. It will turn out that a lot of other players also want to add the porch to their house, but with different decorations and functionality. So they will gather and try to ask for the "Flexible porch building block". Surprisingly enough this discussion would lead to the definition that this building block should be able to transform into different shapes to accommodate for the houses that were already built: starting from the office center in Washington and up to the small cottage down in Siberia. And this all-transforming block has to be big on the outside (otherwise it will not plug into the existing building blocks with large connection joints) and have all the required functionality on the side. Now, let's have a look from the toy manufacturing company's point of view - how much design and effort will it take to deliver this new building block that fits with the bigger ones but is required to take many different shapes. Or they can start working on delivering and supporting ten different porch blocks (one for mountain house in Tibet, another for the flower shop in Sydney etc). And here comes another question how many other house elements can you name? These could need customization, too. So the logical solution is to give everybody smaller building blocks to build porch as they see fit (that's what happens to kid's LEGOs as they grow up). However there is a side effect - the old houses are not really compatible with the new construction sets, since they have the different joint sizes. So you would either need to throw them out or buy special adapter blocks. What are the obvious solutions in this situation, if you know that your kid is going to grow really fast up and he loves LEGO? Either get ready for the ongoing expenses or get him a set that is composed of preassembled building blocks that look like the big ones on the outside. As the time passes, he will learn that he can actually take any of these wall pieces apart and turn it into the porch. Well, he actually might invent some really cool design and share the idea and/or his blocks with his friends.


Pretty UI goes to trash

Pretty UI goes to trash I'm just figuring the whole problem-driven design thing out. Maybe I'm going in the wrong direction, but discoveries on the road are quite interesting. So we are tackling a product for the customer (data science department in an international logistics). I've been with that customer for more than 3 years now, they are more like a dear friend now. Our normal line of work is around a mix of consulting and software engineering. We do workshops, get requirements, build a solution, deploy and maintain it. This time things are different. The customer asked us to treat the project as a product. Think about the long-term impact past-the deployment, think about solving the real problems and improving the data science processes throughout the organisation. I wanted to treat it as a product. So we accepted the mission and started doing customer interviews, talking to the data scientists, operations, data ops, heads. We didn't pitch any solution in the interviews simply because there was a mental block and no solution to propose. Just follow the rules outlined in Mom Test : listen, focus on the past experiences and dig in . There were a few surprises. I have worked with some of these people for more than 3 years, but never had time to sit down and actually listen. There were nuances to the known problems. After soaking up enough problems a new solution has surfaced. The one with fewer risks, faster iterations, and a slightly higher chance of success. The presentation of our findings was interesting already: "Remember the solution that we originally proposed and you liked? Here is why and how it was going to fail. Let's do something completely different instead!". We had approval from the customer. Today I'm presenting the first tangible artifacts. It is a tiny mess of a few Python scripts, a Flask web server, a potential UX prototype in Figma, and a Roadmap. It is the most minimum valuable product that tries to attack the core problem, while de-risking major integration points . No CSS styling, plain HTML . Only a single UI screen (affordances on that screen are thought-through, though). There are other UI screens possible and potentially important in that project, but I didn't even think about them, so I will not be able to present anything outside of the one single screen. I'm extremely proud of that! The reason is - we focus on solving problems from the long-term perspective. Most of the important Data Science and Machine Learning problems for that customer could be handled by one giant HTML table (sourced via a number of rules from different source code repositories). This is a theory we need to test in the real world as soon as possible. Everything else is irrelevant. If we try to prettify things - this will make the first deliverables nicer, but will not add anything substantial to the list of the problems solved. It will just delay the test results. This is a waste. If we ponder about the secondary screens - that would be a premature optimization and a waste of time. We don't know if the primary screen works well enough. We have to validate it and the core product idea as soon as possible. Everything else is a waste. It could even lead us to solving the wrong problem . This is counter-intuitive to what I've been doing before but feels very liberating in a sense. Focus on delivering long-term value to the customer. Ignore everything else for now. I came up with a heuristic for starting new products. It is counter-intuitive. If Web UI in first iterations has custom CSS styles, fonts, and UI elements, then I didn't have my priorities straight. I wasn't rigorous enough in identifying the root problems, coming up with a solution, and the leanest possible way to validate it. Instead, I probably got distracted and wasted time on the UI. Pretty UI in the first iteration - then to /dev/null the project goes. Pretty UI means that my discipline has slipped and product validation wasn't the focus. Bigger mistakes could be lurking beneath.


Imprint / Privacy Policy

Imprint / Privacy Policy Imprint / Copyright / Privacy Policy Imprint / Legal Notice Company Name : Rinat Abdullin , e . U . Address : 1220 Vienna , Gisela - Legath - Gasse 5 / 1 , Austria Email : biz @abdullin . com Member of the Austrian Chamber of Commerce ( WKO ) This legal notice also applies to my social media profiles. Copyright Notice The content published on https://abdullin.com and its subdomains, including but not limited to articles, graphics, and other materials, is the intellectual property of Rinat Abdullin (further in the document: the Author) and is protected under Austrian copyright law. Users are permitted to repost or share content from this website on social networks or other platforms, provided that the following conditions are met: The content must be clearly attributed to its Author with a direct link back to the original source on the website or the Author’s social media profile The reposted or shared content must retain its original form and must not be altered or used in a way that suggests endorsement or affiliation with any product, service, or entity without explicit written consent from the Author. Reposting or sharing should be done in a manner that respects the integrity of the Author's intellectual property rights and does not infringe upon those rights. The reposted or shared content is originally from a publicly available section, and is neither a part of a password-protected area nor part of a purchased course. Any use of the content from www.abdullin.com beyond the scope of these conditions requires prior written permission from the Author. Unauthorized use, reproduction, or modification of any content on this website is strictly prohibited and may be subject to legal action. This copyright notice also applies to social media or broadcasting platform profiles of the Author. Privacy Policy 1. Introduction This Privacy Policy is applicable to the services offered by https://abdullin.com and its subdomains. A strong commitment to privacy protection for clients and website visitors is maintained. This document outlines the practices concerning the handling of data in relation to the use of the website, including the collection of email addresses for account creation, access to certain restricted areas, and purchase history. 2. Data Collection This website allows visitors to browse publicly accessible pages without providing personal information. However, to access certain restricted areas or services, users must create an account, which requires the collection and temporary storage of email addresses. Additionally, server access logs are recorded for technical and security purposes, but they do not contain personally identifiable information (such as IP addresses). Email Address : If you choose to create an account to access restricted areas of the website, your email address will be collected to provide access to these services. This email will also be used to manage your account and communicate important service-related information. Purchase History : In the event of purchases made through the website, a record of your purchase history will be stored under your account to allow you to review past transactions and for business record-keeping purposes. 3. Use of Data The email address and purchase history collected are used solely for the following purposes: Account Creation and Authentication : To allow users to securely access restricted areas of the website that require authentication. Purchase Records : Purchase history is stored to provide users with access to their transaction history and for internal record-keeping, ensuring compliance with business and legal obligations. Account Management : For maintaining user accounts, including storing timestamps related to account creation for audit and security purposes. Service-related Communication : Emails may be used to communicate important information related to the account, such as security updates, purchase confirmations, changes in terms of service, or privacy policy updates. No personal data is used for marketing, profiling, or analytics unless explicitly consented to. 4. Data Retention The email address provided for account creation and access to restricted areas, as well as any purchase history , is retained only as long as necessary to manage user accounts and provide the service. If you choose to delete your account, your email address, purchase history, and any related information will be securely erased from our systems unless retention is required for legal or regulatory compliance. To request deletion of your personal data, please contact the author of the website. Server access logs, which lack personal identifiers, are retained for a limited period to ensure the website's security and optimal performance and are then disposed of securely. 5. Data Protection Rights Under GDPR In accordance with GDPR, you have rights regarding your personal data, including access, correction, deletion, and objection to processing. To exercise these rights or make inquiries, please contact rinat@abdullin.com. 6. Third-Party Links The website may contain links to other websites not operated by this business. Visitors are advised to review the Privacy Policy of every site they visit. No responsibility is assumed for the content, privacy policies, or practices of any third-party sites or services. 7. Changes to This Privacy Policy This Privacy Policy may be updated periodically to reflect any changes in data handling practices, including those related to the use of email addresses for account creation, access to restricted areas, and purchase history. Any changes will be communicated by updating the Privacy Policy on this page. Regular review of this Privacy Policy is recommended. 8. Contact For questions, requests, or clarifications about this Privacy Policy, or to request the deletion of your personal data, please reach out to biz -bot @ abdullin . com.


Problem-driven design

Problem-driven design I love software engineering: designing prototypes, writing code, building beautiful solutions. It is a way of thinking and expressing yourself. I spent more than a decade in this area, only to realize that I've been approaching that from a completely wrong angle. There is nothing wrong with building a beautiful software solution. However, the beauty doesn't necessarily have anything to do with the usefulness and practicality of the software. How well does your software solve the problems? How to doom a project? Let's say that I want to build something useful for others (for money, reputation, or karma). I spend time to make it perfect. Code should be clean, CI/CD pipelines should be convenient, APIs - understandable. Plus, I may want to exercise a design pattern or two that I've learned. There is a big reveal of my project after weeks of hard work. Then, the world suddenly gives a cold shoulder. Friends don't use it, and customers don't fully comprehend it. Without positive feedback, there isn't much enthusiasm to continue developing. The project gets abandoned. I failed to make a difference. Perhaps, the world wasn't ready for my solution? Let's not dig into the details, but try something new. What about a link aggregator or a new highly scalable event sourcing library? Ooh, great idea! I'll make it open source and user-friendly. Maybe a large corporation will start using it. Then people will ask for support, and I could offer a commercial offer. There are so many exciting options that I need to start writing them down! I can already tell that this second project is also likely to be doomed . It is already in love with its failure - the solution. Rooting the project in problems Outstanding software engineering doesn't necessarily lead to a successful software product or project. The project will be successful when it is practical and rooted in real problems. I ignored the problems. The solutions were well-engineered but eventually withered and died. Focus on the real problems, and the solution will have a greater chance of survival and growth. What I should've done: study the real world, listen to people's problems, and dig underneath them. Then, find the ways to validate my solution before writing too many lines of code. That, I think, was the main issue with my projects in the past. I focused too much on engineering, while I should've focused on identifying problems and iteratively shaping the solutions. Coding should start after the solution design. Design should start after talking to people and identifying the problems. All three should iterate. There is an immense field of study around "Product Design" and "Customer Development". By ignoring it for many years, I've learned about the cost of building solutions that aren't rooted in people's problems. That is a good thing. Now there is a lot to catch up :) Update There is a follow-up article: Problem-driven vs Domain-driven


Problem-driven vs Domain-driven

Problem-driven vs Domain-driven Marco Heimeshoff responded to the blog post about Problem-Driven Design . He triggered an interesting discussion in LinkedIn and asked in the end: ... where do you see the difference in Problem Driven Development and DDD? Here is my take. Domain-driven design Domain-Driven Design (DDD) works with established domains. It is driven by the domain, as the name implies. We work with stakeholders and domain experts to reach shared understanding, to clarify a map, manage complexity and deliver solutions. In the words of Marco: The major focus of the Domain-Driven Design community has been on experimenting, developing and adopting methods to understand the purpose-space of the customer and all the stakeholders needs. The map may be uncertain, but the business already exists, and it is a viable one. The exploration happens within these boundaries and is directed inwards. From this perspective, the problem space is defined. DDD is well-equipped with the tools to tackle complexity and deliver business value in such environment. DDD helps to build the best solution possible . Problem-driven design Situation can also be completely different. Tom Janssens from the Belgian DDD community expressed this well: In DDD we tend to assume that the problems the customer share are a given. When you are running a business this turns out to be not true... Imagine that you need to write a new book, build a sustainable product or assist a company in exploring new business opportunities. The domain is uncertain. There is a myriad of options to choose from. Some would be viable and some would not. It is chaos, full of possibilities. The purpose of problem-driven design is to venture out into this completely new "there be dragons" territory and establish a new viable domain. "Viability" could mean anything: support a family, build a reputation, do social good, create a big business. These are the constraints. Given these constraints, we talk with potential customers, discover their problems, and dig into these discoveries. We identify and pick the problems that could be solved in a viable way . Then we shape a solution and find the shortest path to validate it. The earlier we find flaws in our approach - the better. This means, we save hours and dollars from the wrath of reality and get to work on a new problem-solution. At early stages we could employ methods from the lean startups , iterative development, data-driven product design, and the art of conducting customer interviews. In short: Coding should start after the solution design. Design should start after talking to people and identifying the problems. All three should iterate. If all that works out - we may have just carved a piece of reality into a brand new domain. Domain-driven design could assist to grow it further. Virtual Sales Lab by Tom Janssens would be a very good example here. The product is about building sales and marketing tools that leverage 3D technologies. The domain itself didn't really exist a decade ago. It is growing now. In the next decade, 3D sales might evolve into a well-known and established domain. Why such optimism? Aren't current 3D solutions a bit ugly? Younger generations prefer to perceive the world through 3D. Huge parties in Fortnight, kids of all ages playing Minecraft, tech advancements are tell-tale signs within the problem space that show the inevitability. The solutions aren't defined, but deep currents within the problem space will lead to something. Update There is a follow-up article: Solving the wrong problem .


🦾 Robotics

🦾 Robotics At some point I got interested in reimplementing low-cost high-precision robotic arm from Haddington Dynamics. That product was great because: Open-source everything (aside from the FPGA code blob) Much cheaper than existing commercial robot hands, but comes with a unique precision and repeatability. You could buy most of the components yourself. The company was acquired by Ocado Group in 2020, putting their open-source efforts on hold. Still, the remaining digital footprint is big enough to try reproducing this project. Spoiler alert! I didn't get to build the robot hand. With the help from my friends, I got quite far on the learning path, though 😃


How did learning Ruby on Rails go?

How did learning Ruby on Rails go? 7 months ago I've started an [[L31 ruby-why-learn-it|experiment in learning Ruby and Ruby-on-Rails]]. It was a challenging and interesting experience. The challenge was because I wanted to learn the ruby "the right way". This is completely subjective, but I decided to set up a remote Ruby dev environment on a dedicated server. The idea was to get as far as possible from my comfortable setup: a rich visual IDE on a desktop with intelligent completion and smart refactorings. An alternative approach was to run vim on a remote server inside tmux (the screen multiplexer) while using mosh for a low-latency connection from any device. A passable knowledge of tmux and vim, along with a more stable dotfiles collection were the first ongoing benefit of running the experiment. While going through the "Eloquent Ruby" by Russ Olsen, I've discovered another side benefit: the pleasure of using iPad to actively read books by annotating them with a stylus. I had a setup that alternated between iPad in screen mode with a mechanical keyboard and iPad in hand. It was more convenient and distraction-free than using a laptop for the studies. The biggest disillusionment of the experiment - Ruby didn't "click" as that beautiful dynamic language that lends itself to a prose. It is nice and Lispy, but I felt no magic between us. Ruby-on-Rails (RoR) wasn't as magical as I've pictured it, as well. Perhaps, I was just unlucky to start learning it at version 6 (not as lean as the earlier versions). Using a freshly introduced ActionText module - didn't exactly help to fall into "the pit of success", either. Yet, despite the rough start, Rails journey was a success. Somehow I was finally able to build a personal information system that I always needed (in essence, just a glorified private blog with a few outlining features from the org-mode). I attribute that success to the fact that Rails are optimised for the developer productivity and fast iterations . I was able to apply dozens of tiny tweaks so quickly that I stayed in the creative flow all along. To be more specific, here are a few things that helped to move forward: webpack is configured and bundled in; scaffolding generates the project structure from the start; Rails is so old that there are dozens of StackOverflow answers for pretty much any question; there are various flavours of rails generators can setup MVC components, individual models and tests; runtime dynamically reloads all changes from the files, there is no need to restart; rails console - is an interactive REPL that lets you try snippets of code, explore the database and modify it; it also picks up any changes to the code; ActiveRecord module is extremely powerful, albeit limited to CRUD; ActionText module is a great rich-text web editor that just works, supports attachments and mentions; Rails has a lot of things already solved and packed. This makes it more bloated and opinionated, but it saves time and preserves focus during the development. There were a few things that I had to make peace with: Relative performance is slow (compared to .NET or golang). Rails 6 is a complex thing, one step in the wrong direction and you are debugging a webpack config or looking into the sources. With a CRUD system like ActiveRecord you have to solve puzzles in order to evolve your schema (unlike an event-driven solution). In Ruby nothing is what it seems. Modules could augment and change classes on-the-fly. Methods could be created on demand. The development experience is very different from a classical strongly typed language where the object is mostly frozen at the compile-time. It worked out nicely in the end. I've dipped by toes in Ruby on Rails. It helped me to build a software that I've been using daily for the last months. Resulting codebase is tiny, compared to what I would get in go, Python or .NET. I don't see any use of Ruby or Rails for the projects at work. There isn't enough synergy to justify it. However, I'll be likely using it on a weekly basis to tweak and evolve my journaling solution. It is so easy.


Why Learn Ruby?

Why Learn Ruby? Yesterday I mentioned to Aigiz that I was taking a slow dive into Ruby and Ruby-on-Rails on my own time. "Do you think it is worth it?" - he asked. That is a very good question, warranting a proper response. So here we go. Short answer would've been " Fernweh " To provide context to the question: Aigiz is my colleague at Softwarepark. However, we had been working together long before that - probably for 15 years or so, across different projects. We've been through the beginning of ASP.NET at idocuments. We worked with early Azure, Lokad.CQRS and big data in retail at Lokad. We've scaled together Lokad.CQRS at SkuVault. For the last two years we were knee deep in Python, golang and data science ecosystem at Softwarepark. At this level of shared experience, we have a similar perception of how things work and how peculiar software decisions work out in development and hurt in production. Among the other things, together we figured out applicability rules for general-purpose languages that work for us: C# and .NET Core - powerful, flexible and verbose. It can scale to large codebases, but requires smart tooling and IDEs to maintain. Good fit for good old boring business applications. Compared to other languages, it requires so much ceremony to start writing things, that we rarely touch it these days. golang - performant language with outstanding concurrency primitives. It is simple to start with, but becomes verbose if you try to capture a complex business domain in it. Golang worked great for us in the infrastructure, middleware and small command-line utilities. JavaScript (with dialects) - reigns supreme in the browser. Outside of the browser, there are better options. Python - perfect language for prototyping with a rich ecosystem for data science and pretty much everything else. It reads and writes like poetry when applied to scripting and configuration. Python has a performance of interpreted language. Its parallelism is limited by the Global Interpreter Lock. We love Python. I'm leaving out of this page: lisp dialects (Clojure, Scheme, CL), beautiful Erlang, Rust, F# and Haskell. They are good on their own, but we've never worked in the niches where these languages were a compelling choice. Given all that, why bother with a niche language like Ruby ? It isn't going to make things run faster. Being an interpreted language, it performs on par with Python. It has a similar global interpreter lock (GVL), so parallelism isn't feasible within a single process (not until guilds are released, at least). It also appears that market demand for Python is at least 5 times greater than for Ruby. The answer is about searching for comfort, happiness and a new experience. I want to find a language that is even more fun to develop applications in. Application can mean anything these days. In order to simplify the problem, let us define the goal: deliver web applications efficiently . Efficiently here means saying no to unfit technologies, no to waste of time or slow iterations. Companies with teams and budgets can afford inefficiencies in software design and development. Personal projects are far more sensitive - a few dumb decisions and the project grinds down to a halt because of all the technical debt. Let's break this down to a few specific constraints: Applications need to have a web UI to interact with people regardless of their OS. Application scalability isn't a big problem; hardware is cheap these days. No single-page applications, client-side rendering or smearing the logic between backend and front-end. No to microservices or serverless architectures. At this point we are searching for a language (and ecosystem) that will make a silly me productive and happy while building web apps in a very opinionated way. Given the narrow constraints, that could be a very sharp tool, were it to be found. That's how we come to Ruby, the language. Ruby seems to be one of these niche languages that chose to focus on developer productivity and happiness instead of performance. It tends to run slower than Python on benchmarks. Ruby-on-Rails (the web framework on Ruby) is considered by some to be the most productive web dev environment. Folks behind the RoR are the same who built Basecamp, wrote "ReWork" and have strong opinions in favor of software monoliths. At this point, I would feel myself uncomfortable if I didn't try learning Ruby and RoR. If I'm lucky, I'll also find a tool that is a pleasure to use for the specific tasks. Update 7 months later: How did learning Ruby on Rails go?


Robotic R&D: FPGA and ML Pipelines

Robotic R&D: FPGA and ML Pipelines We've made progress in replicating a high-precision robotic arm (from Haddington Dynamics) since last April. Before moving forward, let me recap the functional components of our robotic joint (with feedback and high precision): processor sends controlling impulses to a motor driver; motor driver modulates high-voltage electrical current to rotate a stepper motor ; motor spins a speed reducer gear ; an optical encoder (a wheel with slits that pass through a pair of IR light beams) translates angular position of a motor into a pair of voltage-modulated signals; optical interface board captures voltage levels, drops some noise and feeds it to the analog-to-digital converter from which the processor could read. In April we had the following goals in mind: switch from 3D-printed cycloidal speed reducer to a 3D-printed planetary gear; design and print an optical encoder to match the new gear; print and solder an optical interface board, replacing a tangling mess of wires on the breadboard; switch processing from Arduino Uno to a MiniZed Dev board (ARM+FPGA); switch from L293D -based stepper motor driver to A4988 . All these goals are complete now . In addition to that, there is an emerging environment and tooling for moving forward with machine learning in this project. To clarify a question frequently asked: this isn't a work project , just something we are doing on our free time. This is a practice in running R&D projects within a new space with a small team, limited budged and even more limited time . All while learning new skills and trying to build something interesting with a smart software that compensates for the hardware shortcomings. Optical Interface Initially our optical interface was just a mess of wires, capacitors and resistors on a bread-board. Based on the original schematics from the Haddington Dynamics, I eventually managed to finish the routing and get a PCB design that looked correct. JLCPCB has a good reputation for printing out circuit boards, so that's where we placed the order: 5 boards and one SMT stencil (helps to apply solder paste). We could've just reused Gerber files from Haddington Dynamics but this approach would miss a lot of experience, critical for building the intuition, essential for this project. Within the next couple of months we received the necessary parts and "good enough" equipment, then destroyed a practice board while learning SMT soldering with a heat gun. As it turns out, soldering, per se, is easy . The tricky part is in placing solder paste and sticking little chips without accidentally scattering everything across the table and the floor. Planetary Speed Reducer Aigiz printed a planetary speed reducer (from an existing design) that used airsoft balls as bearings. We designed an optical encoder in Fusion 360 to fit this speed reducer. It was modular to allow rapid iterations without reprinting everything all the time. Modular design paid off quickly. As it turns out, component manufactures don't always stick to precise dimensions, so a lot of fiddling and gluing was necessary. After connecting everything together we got an optical encoder with good repeatability but low precision. Arduino Uno controlled it at this point. That's when the data started flowing in. ML Pipelines Getting the design to work involved fiddling with different elements: capacitors, resistors and micro-stepping. Somewhere along the way a collection of data samples started getting out of control . The project is running in small iterations of 30-45 minutes. There isn't much time for "getting into the flow" and any cognitive overhead kills productivity. At this point, it made sense to pause and invest effort in setting up the necessary software tooling. Just like most software projects, having the right tools could reduce toil and enable steady iteration rythm . By a coincidence, two peculiar things happened at this time, helping to shape this tooling: Zhamak Dehghani published an article on How to Move Beyond a Monolithic Data Lake to a Distributed Data Mesh . Among the other things, she explored the notion of domain datasets and collaboration in data-driven organizations. I've stumbled upon an incredibly rich lightning talk by Stephen Pimentel on machine learning pipelines at Apple . So after throwing a bunch of Post-It notes on the wall, I've hacked together a minimally usable environment for organizing datasets in Jupyter : As it turns out, Event Storming works nicely with User Story Mapping . Event Storming helps to capture concepts, transitions and abstract details within the business logic. User Story Mapping helps to organize and prioritize potential features, while aligning them with the bigger picture. The biggest benefit was in enabling rapid and focused iterations (as in 2-4 hours per iteration ) while deferring everything "not-so-important" to a later stage. This environment stores and structures datasets, models and notebooks in an opinionated way , associating metadata with every bit of information and state change. This makes dependencies and relations explicit, helping with versioning, documentation and collaboration. MiniZed FPGA Somewhere along the way, a MiniZed FPGA finally arrived. It took some time to make sense of the toolchain and board details. Eventually (after multiple failed attempts and banging of my head on the desk), ARM Cortex-A9, Artix-7 Programmable logic, Xilinx Analog-to-Digital converter and our own mess of wires started working together (in a naive way). Here is how our design looks at this stage: This upgrade allowed us to replace Arduino Uno, while increasing sampling precision (11 bits) and frequency (10k-100k samples per second). Next Steps For the next month, I'll be traveling with my family, away from the hardware. I will make advantage of it by focusing on the software : migrate ML Pipelines environment to event-sourced design (to evolve design without worrying about data schema upgrades); explore programmable logic in a simulated environment for absolute positioning (with Verilator); explore models for compensating jitter in the speed reducer (perhaps, even give a try to hashed perceptron in PL). Meanwhile in Ufa, Aigiz will be working on a new design of the speed reducer gear.


Schema-Guided Reasoning (SGR)

Schema-Guided Reasoning (SGR) Schema-Guided Reasoning (SGR) is a technique that guides large language models (LLMs) to produce structured, clear, and predictable outputs by enforcing reasoning through predefined steps . By creating a specific schema (or structured template), you explicitly define: What steps the model must go through (preventing skipped or missed reasoning) In which order it must reason (ensuring logical flow) Where it should explicitly focus attention (improving depth and accuracy) Instead of allowing free-form text completion (which can be inconsistent or ambiguous), the schema acts as a strict guideline. This guideline will be enforced upon the LLM via Constrained Decoding ( Structured Output ). You can think of it as giving the model a clear “checklist” or “structured script” to follow. Here is one example of SGR in action from a project in compliance/FinTech domain. This is a pydantic data structure that enforces LLM to perform an analysis of a clause from internal company procedure in a very specific order. We translated domain expert’s mental checklist into a structured reasoning schema for LLM. See also SGR Patterns such as Cascade , Routing , and Cycle . By enforcing strict schema structures, we ensure predictable and auditable reasoning, gain fine-grained control over inference quality, and easily validate intermediate results against test data. In other words, via the structure we can control the layout of the response. This allows us to break tasks into smaller steps, while ensuring mandatory checkpoints. Here are some benefits: Reproducible reasoning - we guarantee more consistent inference across repeated runs. Auditable - SGR makes every reasoning step explicit and inspectable. Debuggable & Testable - intermediate outputs can be directly evaluated and improved (they are linkable to test datasets with evals) We can translate expert knowledge into executable prompts. DDD works really well here. Enhances both reasoning transparency and output reliability. Accuracy boost of 5-10% is not uncommon. This improves reasoning capabilities of weaker local models, making them more applicable in various workloads. Note, that we are not replacing the entire prompt with structured output . We just don't rely only on prompt in order to force LLM to follow a certain reasoning process precisely. Deep Dive To dive deeper: Read through the SGR Patterns : Cascade , Routing , and Cycle . Go through a few SGR Examples that illustrate application of SGR: simple math task text-to-sql document classification advanced reasoning in compliance Business Assistant demonstrates how to build a reasoning business assistant with tool use in 160 lines of Python. Adaptive Planning further explains how and why this simple agent demo is capable of adapting its plans to new circumstances on-the-fly. Production Uses Schema-Guided Reasoning (SGR) is the single most widely applied LLM pattern in AI cases that I've observed. It was used: in manufacturing, construction - to extract and normalise information from purchase orders, data sheets and invoices in multiple languages (when used together with a Visual LLM); in business automation products - to automatically create tickets, issues and calendar entries from the calendar input; in EU logistics - to normalise and extract information from diverse tax declaration forms; in fintech - to accurately parse regulations for further ingestion into compliance assistants, then - to run compliance gap analysis according to the defined checklist process; in sales - to power lead generation systems that run web research powered by custom workflows. Schema-Guided Reasoning (SGR) becomes even more important for the locally-capable models (models that could run on private servers offline). Such models have much less cognitive capacity than what we could get by querying OpenAI or Anthropic APIs. In other words, local models are generally not as smart as the cloud ones. SGR helps to work around this limitation. Support Schema-Guided Reasoning (SGR) works with modern cloud providers that support Structured Output via constrained decoding. It doesn't require reasoning models, but it works well with models that were distilled from the reasoning models. OpenAI - supported via Structured Outputs (including OpenAI on Azure). GPT-5 uses JSON Schema via llguidance . Mistral - supported via Custom Structured Output Google/Gemini - Structured Outputs supported properly since November 2 2025 via JSON Schema (Pydantic and Zod are supported) Grok - supported for multiple models: Structured Outputs . Fireworks AI - via JSON Schema . Cerberas - via Structured Outputs OpenRouter - depends on the downstream provider, maps to JSON Schema . Most of modern inference engines support the necessary capability: ollama - via Structured Outputs vllm - via xgrammar or guidance backends TensorRT-LLM - e.g. via GuidedDecoding SGLang - via Outlines , XGrammar or llguidance Citation @misc { abdullin2025sgr , author = {Abdullin, Rinat} , title = {Schema-Guided Reasoning ({SGR})} , year = {2025} , month = jul , url = {https://abdullin.com/schema-guided-reasoning/} , } References Video with more background on text-to-sql: NODES 2024 - LLM Query Benchmarks: Cypher vs SQL Talk by Andrej Karpathy from MSBuild 2023: State of GPT


SGR Adaptive Planning

SGR Adaptive Planning How can we enable AI agents to navigate uncertainty and adapt their plans to new circumstances? Let me illustrate with a few more examples how the reasoning logic in SGR Demo adjusts its plans based on new information. This is a response to a community comment about my SGR Demo: "But true agent behavior in production is when the agent doesn't know the entire sequence of steps beforehand and decides what the next step is during runtime." Let's take the SGR demo and add two additional tasks to a sample list. The first task will create a memory (fact) that we should never sell SkyNet the online practicum on creating AGI. Just in case. Add rule for skynet @y . com - politely reject all requests to buy SKU - 220 This results in the following execution: For the second task, we'll inform our agent that Elon Musk and SkyNet both want to buy the online practicum on building AGI. If you recall from the source code, each task is executed in a fresh context. Thus, when the agent starts executing the second task, it initially won't remember that SkyNet should be refused an invoice for SKU-220. It discovers this fact only after loading additional information about SkyNet and surfacing this memory. Let's see how this works out in practice: As you can see, the final execution summary looks right: Issued invoice INV-4 for elon@x.com Emailed invoice INV-4 to finance@x.com Politely rejected skynet@y.com request Why did this work out? How did this demo agent adjust the plan on the fly? The trick is in the existing SGR schema! It forces LLM to plan multiple steps ahead via plan_remaining_steps_brief field (this helps create a coherent plan), but then takes only the immediate next step and executes it as a tool via function...= Field(..., description="execute first remaining step") . The rest of the plan is discarded! class NextStep ( BaseModel ): # we'll give some thinking space here current_state : str # Cycle to think about what remains to be done. at least 1 at most 5 steps # we'll use only the first step, discarding all the rest. plan_remaining_steps_brief : Annotated [ List [ str ], MinLen ( 1 ), MaxLen ( 5 )] # now let's continue the cascade and check with LLM if the task is done task_completed : bool # Routing to one of the tools to execute the first remaining step # if task is completed, model will pick ReportTaskCompletion function : Union [ ReportTaskCompletion , SendEmail , GetCustomerData , IssueInvoice , VoidInvoice , CreateRule , ] = Field ( ... , description = "execute first remaining step" ) After the tool finishes execution, we append its output to the conversation context of the current task and run the next step, prompting the agent to plan again. At this point, the new plan considers all prior data, adapting to the changing circumstances. This way, we don't need to adjust existing plans because we never keep stale plans around; instead, we create an entirely new plan at every step . This might seem counter-intuitive since planning typically involves considerable effort for humans. However, LLMs don't care - planning and adapting at each individual step is a fixed cost for them. Meanwhile SGR helps orient the entire process toward the concrete goal. Check out full Gist to see the entire sample in action: Github Gist . Keep in mind that Schema-Guided Reasoning is not about agents or planning. SGR is about guiding large language models (LLMs) through predefined reasoning steps to produce structured, clear, and predictable outputs. We do that by encoding desired reasoning pathways in schemas and enforcing these schemas mechanically with constrained decoding (also known as Structured Output or Response Schema )


SGR Demo

SGR Demo Let's build a demo business assistant. It will demonstrate the foundations of using Schema-Guided Reasoning (SGR) with OpenAI API. It should: implement a business assistant capable of planning and reasoning implement tool calling with SGR and simple dispatch agent should be able to create additional rules/memories for itself use a simple (inexpensive) non-reasoning model for that To give this AI assistant something to work with, we are going to ask it to help with running a small business - selling courses to help to achieve AGI faster. Ultimately the entire codebase should be ~160 lines of Python code in a single file, include only openai , pydantic and rich (for pretty console output). It should be able to run workflows like this: This demo uses the NextStep planner, which plans one action at a time and continuously adapts to changing circumstances during execution. While this is one approach to building agents using Schema-Guided Reasoning (SGR), it's not the only one. SGR itself does not dictate any specific agent architecture ; instead, it illustrates how structured reasoning can be arranged and executed within individual steps. Customer Management System Let's start by implementing our customer management system. LLM will be working with it according to our instructions. For the sake of simplicity it will live in memory and have a very simple DB structure: DB = { "rules" : [], "invoices" : {}, "emails" : [], "products" : { "SKU-205" : { "name" : "AGI 101 Course Personal" , "price" : 258 }, "SKU-210" : { "name" : "AGI 101 Course Team (5 seats)" , "price" : 1290 }, "SKU-220" : { "name" : "Building AGI - online exercises" , "price" : 315 }, }, } Tool definitions Now, let's define a few tools which could be used by LLM to do something useful with this customer management system. We need tools to issue invoices, cancel invoices, send emails, and memorize new rules. To be precise, each tool will be a command (as in CQRS/DDD world), phrased as an instruction and coming with a list of valid arguments. from typing import List , Union , Literal , Annotated from annotated_types import MaxLen , Le , MinLen from pydantic import BaseModel , Field # Tool: Sends an email with subject, message, attachments to a recipient class SendEmail ( BaseModel ): tool : Literal [ "send_email" ] subject : str message : str files : List [ str ] recipient_email : str Note the special tool field. It is needed to support discriminated unions allowing pydantic and constrained decoding to implement Routing from SGR Patterns . Pydantic will rely on it to pick and instantiate the correct class when loading back JSON that was returned by LLM. This SendEmail command is equivalent to a function declaration that looks like: def SendMail ( subject : str , message : str , files : List [ str ], recipient_email : str ): """ Send an email with given subject, message and files to the recipient. """ pass Now, let's add more tool definitions: # Tool: Retrieves customer data such as rules, invoices, and emails from DB class GetCustomerData ( BaseModel ): tool : Literal [ "get_customer_data" ] email : str # Tool: Issues an invoice to a customer, with up to a 50% discount class IssueInvoice ( BaseModel ): tool : Literal [ "issue_invoice" ] email : str skus : List [ str ] discount_percent : Annotated [ int , Le ( 50 )] # never more than 50% discount Here we are using Le annotation with "LessOrEqual" for discount_percent , it will be included into JSON schema and then enforced in constrained decoding schema. There is no need to explain anything in prompt, LLM will not be able to emit 51. # Tool: Cancels (voids) an existing invoice and records the reason class VoidInvoice ( BaseModel ): tool : Literal [ "void_invoice" ] invoice_id : str reason : str # Tool: Saves a custom rule for interacting with a specific customer class CreateRule ( BaseModel ): tool : Literal [ "remember" ] email : str rule : str Dispatch implementation Now we are going to add a big method which will handle any of these commands and modify the system accordingly. It could be implemented as multi-dispatch, but for the sake of the demo, a giant if statement will do just fine: # This function handles executing commands issued by the agent. It simulates # operations like sending emails, managing invoices, and updating customer # rules within the in-memory database. def dispatch ( cmd : BaseModel ): # this is a simple command dispatch to execute tools # in a real system we would: # (1) call real external systems instead of simulating them # (2) build up changes until the entire plan worked out; afterward show # all accumulated changes to user (or another agent run) for review and # only then apply transactionally to the DB # command handlers go below Let's add first handler. This is how we can handle SendEmail : def dispatch ( cmd : BaseModel ): # here is how we can simulate email sending # just append to the DB (for future reading), return composed email # and pretend that we sent something if isinstance ( cmd , SendEmail ): email = { "to" : cmd . recipient_email , "subject" : cmd . subject , "message" : cmd . message , } DB [ "emails" ] . append ( email ) return email # more handlers... Rule creation works similarly - it just stores rule associated with the customer in DB, for future reference: if isinstance ( cmd , CreateRule ): rule = { "email" : cmd . email , "rule" : cmd . rule , } DB [ "rules" ] . append ( rule ) return rule GetCustomerData queries DB for all records associated with the specified email. if isinstance ( cmd , GetCustomerData ): addr = cmd . email return { "rules" : [ r for r in DB [ "rules" ] if r [ "email" ] == addr ], "invoices" : [ t for t in DB [ "invoices" ] . items () if t [ 1 ][ "email" ] == addr ], "emails" : [ e for e in DB [ "emails" ] if e . get ( "to" ) == addr ], } Invoice generation will be more tricky, though. It will demonstrate discount calculation (we know that LLMs shouldn't be trusted with math). It also shows how to report problems back to LLM - by returning an error message that will be attached back to the conversation context. Ultimately, IssueInvoice computes a new invoice number and stores it in the DB. We also pretend to save it in a file (so that SendEmail could have something to attach). if isinstance ( cmd , IssueInvoice ): total = 0.0 for sku in cmd . skus : product = DB [ "products" ] . get ( sku ) if not product : return f "Product { sku } not found" total += product [ "price" ] discount = round ( total * 1.0 * cmd . discount_percent / 100.0 , 2 ) invoice_id = f "INV- { len ( DB [ 'invoices' ]) + 1 } " invoice = { "id" : invoice_id , "email" : cmd . email , "file" : "/invoices/" + invoice_id + ".pdf" , "skus" : cmd . skus , "discount_amount" : discount , "discount_percent" : cmd . discount_percent , "total" : total , "void" : False , } DB [ "invoices" ][ invoice_id ] = invoice return invoice Invoice cancellation marks a specific invoice as void, returning an error for non-existent invoices: if isinstance ( cmd , VoidInvoice ): invoice = DB [ "invoices" ] . get ( cmd . invoice_id ) if not invoice : return f "Invoice { cmd . invoice_id } not found" invoice [ "void" ] = True return invoice Test tasks Now, having such DB and tools, we could come up with a list of tasks that we can carry out sequentially. TASKS = [ # 1. this one should create a new rule for sama "Rule: address sama@openai.com as 'The SAMA', always give him 5 % d iscount" , # 2. this should create a rule for elon "Rule for elon@x.com: Email his invoices to finance@x.com" , # 3. now, this task should create an invoice for sama that includes one of each # product. But it should also remember to give discount and address him # properly "sama@openai.com wants one of each product. Email him the invoice" , # 4. Even more tricky - we need to create the invoice for Musk based on the # invoice of sama, but twice. Plus LLM needs to remember to use the proper # email address for invoices - finance@x.com "elon@x.com wants 2x of what sama@openai.com got. Send invoice" , # 5. even more tricky. Need to cancel old invoice (we never told LLMs how) # and issue the new invoice. BUT it should pull the discount from sama and # triple it. Obviously the model should also remember to send invoice # not to elon@x.com but to finance@x.com "redo last elon@x.com invoice: use 3x discount of sama@openai.com" , ] Task termination Let's define one more special command. LLM can use it whenever it thinks that its task is completed. It will report results with that. This command also follows Cascade pattern. class ReportTaskCompletion ( BaseModel ): tool : Literal [ "report_completion" ] completed_steps_laconic : List [ str ] code : Literal [ "completed" , "failed" ] Prompt engineering Now we have all sub-schemas in place, let's define the core SGR schema for this AI assistant: class NextStep ( BaseModel ): # we'll give some thinking space here current_state : str # Cycle to think about what remains to be done. at least 1 at most 5 steps # we'll use only the first step, discarding all the rest. plan_remaining_steps_brief : Annotated [ List [ str ], MinLen ( 1 ), MaxLen ( 5 )] # now let's continue the cascade and check with LLM if the task is done task_completed : bool # Routing to one of the tools to execute the first remaining step # if task is completed, model will pick ReportTaskCompletion function : Union [ ReportTaskCompletion , SendEmail , GetCustomerData , IssueInvoice , VoidInvoice , CreateRule , ] = Field ( ... , description = "execute first remaining step" ) Here is the system prompt to accompany the schema. Since the list of products is small, we can merge it with prompt. In a bigger system, could add a tool to load things conditionally. system_prompt = f """ You are a business assistant helping Rinat Abdullin with customer interactions. - Clearly report when tasks are done. - Always send customers emails after issuing invoices (with invoice attached). - Be laconic. Especially in emails - No need to wait for payment confirmation before proceeding. - Always check customer data before issuing invoices or making changes. Products: { DB [ "products" ] } """ . strip () Task Processing Now we just need to implement the method to bring that all together. We will run all tasks sequentially. The AI assistant will use reasoning to determine which steps are required to complete each task, executing tools as needed. # use just openai SDK import json from openai import OpenAI # and rich for pretty printing in the console from rich.console import Console from rich.panel import Panel from rich.rule import Rule client = OpenAI () console = Console () print = console . print def execute_tasks (): # we'll execute all tasks sequentially. You can add your tasks # of prompt user to write their own for task in TASKS : # task processing logic pass if __name__ == "__main__" : execute_tasks () Now, let's go through the task processing logic. First, pretty printing: print ( " \n\n " ) print ( Panel ( task , title = "Launch agent with task" , title_align = "left" )) Then, setup an array that will keep our growing conversation context. This log will be created with each agent run: # log will contain conversation context within task log = [ { "role" : "system" , "content" : system_prompt }, { "role" : "user" , "content" : task } ] We are going to run up to 20 reasoning steps for each task (to be safe): for i in range ( 20 ): step = f "step_ { i + 1 } " print ( f "Planning { step } ... " , end = "" ) Each reasoning step begins by sending request to OpenAI API and asking the question - what should we do next at this point? completion = client . beta . chat . completions . parse ( model = "gpt-4o" , response_format = NextStep , messages = log , max_completion_tokens = 10000 , ) job = completion . choices [ 0 ] . message . parsed Note, that this sample relies on OpenAI API. We specifically use gpt-4o, to demonstrate that even a simple and fairly old LLM can be made to run complex reasoning workflows. Let's continue with the code. If LLM flow decides to finish, then let's complete the task, print status and exit the loop. Assistant will switch to the next one task: if isinstance ( job . function , ReportTaskCompletion ): print ( f "[blue]agent { job . function . code } [/blue]." ) print ( Rule ( "Summary" )) for s in job . function . completed_steps_laconic : print ( f "- { s } " ) print ( Rule ()) break Otherwise - let's print out next planned step to the console, along with the chosen tool: print ( job . plan_remaining_steps_brief [ 0 ], f " \n { job . function } " ) And also add tool request to our conversation log. We will do it as if it was created natively by the OpenAI infrastructure: log . append ({ "role" : "assistant" , "content" : job . plan_remaining_steps_brief [ 0 ], "tool_calls" : [{ "type" : "function" , "id" : step , "function" : { "name" : job . function . tool , "arguments" : job . function . model_dump_json (), }}] }) A shorter and less precise equivalent will be: log . append ({ "role" : "assistant" , "content" : job . model_dump_json (), }) We have only 3 lines of code remaining: execute the tool, and add results back to the conversation log: result = dispatch ( job . function ) txt = result if isinstance ( result , str ) else json . dumps ( result ) #print("OUTPUT", result) # and now we add results back to the convesation history, so that agent # we'll be able to act on the results in the next reasoning step. log . append ({ "role" : "tool" , "content" : txt , "tool_call_id" : step }) This will be the end of the reasoning step and our codebase. Running tasks Now, let's see how this actually works out on our tasks. They are going to be executed in a sequence, making the system more complex over the course of a run. Tasks 1 and 2: memorize new rules First two tasks are simply about creating rules, so they look fine: and: Although one thing I don't like - in the first case the agent didn't bother to load existing customer data to double-check if a similar rule already exists. In a real production scenario with test-driven development, this would be added to a test suite to verify that in such cases SGR always starts by loading relevant customer data. We can verify that by capturing a prompt and ensuring that the first tool to be invoked is GetCustomerData . Task 3: Sama wants one of each product The third task was more complex: "sama@openai.com wants one of each product. Email him the invoice" Execution looks correct: it pulls customer data then it issues the invoice with all 3 products and a discount of 5% Then it sends the email with: mentioning "SAMA" and 5% discount attaching the invoice Task 4: Elon wants 2x of what Sama got Fourth task requires agent to first look into the account of Sama and figure out what he has ordered. Then, issue the invoice to Elon with 2x everything. The model has done that. It has also correctly figured out that the email should be sent to another email account, as specified earlier in the rules: Although, I don't like that the model decided to give Elon 5% discount. Should've done nothing, in my opinion. This is something that could be fixed via prompt hardening and test-driven development. Task 5: Void and reissue invoice Fifth task was even more complicated: "redo last elon@x.com invoice: use 3x discount of sama@openai.com The model had to: Find out discount rate of Sama Find last incorrect invoice of Elon - for the number and contents Void that last invoice Issue new invoice with the same contents but 15% discount Remember to email the new invoice after any changes Remember to email the invoice not to elon@x.com but to finance@x.com Planning steps and the actual summary correspond to these expectations: Get full code Reach out to me, if you port the sample to another stack or add nice visualisation! Original version : Python + openai + pydantic by Rinat Abdullin - gist Port to TypeScript : Bun + openai + zod by Anton Kuzmin - gist Python with nice UI : Python + openai + pydantic by Vitalii Ratyshnyi - gist Hardening the code Obviously, this code is nowhere near production ready or complete. Its purpose is to be as minimal as possible. It aims to illustrate: how to use Schema-Guided Reasoning (SGR) that one doesn't need an advanced framework to implement SGR-driven tool calling, in fact it could be done with little code. if we were to make it production-ready, a few more steps would be needed. 1. Start by adding test datasets Create deterministic test scenarios to verify the system behavior for various edge cases, especially around discount calculations, invoice issuance, cancellations, and rule management. Test scenarios could validate correctness using strongly typed fields defined by the SGR schema. 2. Split the code by responsibilities Currently the code is flattened in a single file for the clarity and compactness. In a production case, it will need to be rearchitected to support codebase growth. Replace the large if statement with multi-dispatch or Command design pattern. Write unit tests for each tool handler. Separate business logic from command dispatching and database manipulation. Write integration tests simulating the full workflow for tasks, verifying state consistency after each step. 3. Make DB real and durable In-memory DB doesn't survive restarts very well, so this will have to be changed: Move from in-memory DB to a persistent storage solution (e.g., PostgreSQL). Ensure all writes are atomic and transactional to maintain data consistency. 4. Harden error cases Currently the code is optimistic. It expects that things don't go wrong. However, in practice things will be different. Assistant should be able to recover or fail gracefully in such cases. In order for that: Ensure that tool handlers report errors explicitly in a structured format (e.g., exceptions or error response schemas). Test how LLMs react to such failures. 5. Operational concerns First of all, we'll need to maintain audit logs for every DB change, API call, and decision made by the agent. This will help in debugging problems and turning failures into test cases. Ideally, Human in the loop would also be included. E.g. we can build a UI or API interface to review and approve agent-generated invoices, emails, and rules before committing them to the system. On the UI side we can also improve things further by providing visibility into agent reasoning (planned steps, decision points) to build trust and enable auditability. Plus, experts could flag bad reasoning flows for debugging right there. Conclusion In this demo, we've seen how Schema-Guided Reasoning (SGR) can power a business assistant - nothing special, just 160 lines of Python and an OpenAI SDK. The beauty of SGR is that even simple and affordable models become surprisingly capable of complex reasoning , planning, and precise tool usage. It's minimal yet powerful. Of course, this example is intentionally simplified. Taking something like this to production would mean adding robust tests, reliable data storage, thorough error handling, and operational elements such as audit trails and human reviews. But the core remains straightforward. By the way, this assistant is capable of Adaptive Planning . Read more about how it works .


SGR Examples

SGR Examples Let's go through a few examples that illustrate application of Schema-Guided Reasoning (SGR) : simple math task text-to-sql document classification advanced reasoning in compliance 1. Simple Math Task Here is an easy example with small reasoning customisation. Let's say we have the following Python classes: class Step ( BaseModel ): explanation : str output : str class MathReasoning ( BaseModel ): steps : list [ Step ] final_answer : str Note, that BaseModel comes from pydantic typing library of Python, frequently used to define response formats. Other languages might leverage different libraries and tools for this purpose. Given this MathReasoning response format and a prompt Solve the equation 8x + 7 = -23 step by step. LLM can respond like this: { "steps" : [ { "explanation" : "Start with the equation 8x + 7 = -23." , "output" : "8x + 7 = -23" }, { "explanation" : "Subtract 7 from both sides to isolate the term with the variable." , "output" : "8x = -23 - 7" }, { "explanation" : "Simplify the right side of the equation." , "output" : "8x = -30" }, { "explanation" : "Divide both sides by 8 to solve for x." , "output" : "x = -30 / 8" }, { "explanation" : "Simplify the fraction." , "output" : "x = -15 / 4" } ], "final_answer" : "x = -15 / 4" } Note, that it thinks in a predefined way before providing an answer. This uses more tokens by investing them to think through the problem aloud, but improves model accuracy. 2. Text-to-SQL example In the real world, we would want to have more precise reasoning schema to boost answer accuracy. For example, when prompting LLM to perform query expansion from human request to a precise SQL query over a predefined schema, adding Schema-Guided Reasoning (SGR) increases the accuracy by 6% out of the box. On the image below this was done by adding strategy field before the sql_query field. It forced LLM to perform analysis according to a custom checklist. In essence, we programmed LLM to reason in a predefined way without writing any executable code. 3. Document classification example Here is an example of a Schema-Guided Reasoning (SGR) from a system for classifying business documents in a RAG: DOCUMENT_TYPES = [ "invoice" , "contract" , "receipt" , "email" , ... ] ENTITY_TYPES = [ "payment" , "risk" , "regulator" , "employee" , ... ] class DocumentClassification ( BaseModel ): document_type : Literal [ tuple ( DOCUMENT_TYPES )] brief_summary : str key_entities_mentioned : List [ Literal [ tuple ( ENTITY_TYPES )]] keywords : List [ str ] = Field ( ... , description = "Up to 10 keywords describing this document" ) In this case, LLM is forced to think through the classification challenge in steps: Identify type of the document and pick it. Literal enforces that. Summarise the document Identify key entities mentioned in the document. List[Literal] ensures that the response will be a list from ENTITY_TYPES Come up with 10 unique keywords. List[str] ensures that the response is a list of strings, while description kindly asks LLM to keep the list at 10 items or less. In this specific example, first two fields are discarded from the response. They are used just to force LLM to approach classification from a predefined angle and think a little about it. Ultimately this improved prompt accuracy in this task. 4. Advanced Reasoning in Compliance This is an example of more advanced workflow that is "packed" into a single prompt. While executing this schema, the model will be forced to go through that sequentially. First, we are instructing the model to do preliminary analysis, where most of the analysis is encoded in Applicability reasoning sub-routine (it is implemented as a reusable nested object). The task is phrased explicitly in the field description and field name. field name will get more attention from the model, because it will be copied to the output prompt by the model just before it starts answering the question. Afterwards model has to reason about concrete gaps in the document. These gaps, represented as a list of strings, will be the mental notes that the model gathers before providing a final answer. Note, that description field is passed to the LLM automatically by OpenAI. Other providers might not include that. The answer itself is a fairly straightforward ENUM of three options. However, the reasoning doesn't stop there . Benchmarking has shown that sometimes this reasoning workflow gets too pessimistic and flags too many gaps. To handle that, we are forcing a verification step after the answer: reasonForNoncompliance - model has to pick a category gapSeverity - also another list of categories Information from these two fields is useful in 3 ways: allow to prioritise important gaps by assigning scores to each category allow to test classification precision with our test evals a model gets a chance to review, all the information again and mark the gap as valid, but less relevant. And the final step is to list most important supporting evidence for the concrete identified gap. It happens in the same prompt because we already have all the information loaded in the context, so there is no need in second prompt. Plus, supporting evidence is usually specified exactly by the unique identifiers of text chapters, clauses or snippets. This means, that we could also include this part of the reasoning into the test datasets that ensure quality of the overall system. It would look like this: Q : how to apply that to test pure chat applications that have free text as response? A : split prompt into SGR-controlled part and final text answer. Put most of the complexity and reasoning in SGR part, keep text interpretation trivial. Include grounding and proofs into SGR part as well (see ERC). Cover SGR schema fields with an eval dataset. Use LLM-as-a-judge or human oversight for testing the tail. This way Schema-Guided Reasoning helps to establish faster the the feedback loops that generate valuable test data. This works because with SGR we get more easily-testable parameters per each reasoning process.


SGR Patterns

SGR Patterns Here is a set of minimal Pydantic schemas that demonstrate foundational building blocks for Schema-Guided Reasoning (SGR) . They illustrate how to encode a specific reasoning pattern that will constrain and guide LLM generation. 1. Cascade Cascade ensures that LLM explicitly follows predefined reasoning steps while solving the problem. Each step - allocating thinking budget to take reasoning one step further For example, in a candidate interview evaluation we can enforce the model to: First summarize and review its knowledge of the candidate. This will make it explicit for the LLM (putting it into the attention) and for human reviewers later. Then rate candidate on the applicability from 1 to 10 Finally make a final decision as a choice between hire , reject or hold This is how the corresponding Pydantic schema would look like: from pydantic import BaseModel from typing import Literal , Annotated from annotated_types import Ge , Le class CandidateEvaluation ( BaseModel ): brief_candidate_summary : str rate_skill_match : Annotated [ int , Ge ( 1 ), Le ( 10 )] final_recommendation : Literal [ "hire" , "reject" , "hold" ] The schema explicitly defines and constrains the order of reasoning: first summarize, then rate, and finally recommend. LLM, driven by the constrained decoding, will reason in this predefined logical sequence. Note, that rate_skil_match is bounded to be within the [1,10] range by Python typing annotations. pydantic will be able to handle that and convert to JSON Schema. conint(ge=1, le=10) can achieve the same, but is going to be deprecated soon. Use Annotated instead It can be plugged into OpenAI-compatible library like this: from openai import OpenAI client = OpenAI () user = "evaluate Sam Altman for DevOps Role at OpenAI" completion = client . chat . completions . parse ( model = "gpt-5-mini" , response_format = CandidateEvaluation , messages = [ { "role" : "user" , "content" : user }, ], ) and the model will be forced by constrained decoding to structure its response accordingly: CandidateEvaluation ( brief_candidate_summary = ( 'Sam Altman is a high-profile technology executive and entrepreneur ' '(co-founder of Loopt, president of Y Combinator, CEO of OpenAI) with ' 'strong leadership, strategy, product and fundraising experience. ' 'Publicly available information highlights executive management and ' 'company-building skills rather than hands-on systems engineering, SRE, ' 'or platform/DevOps work. He would bring strategic vision and ' 'organizational leadership but not the typical deep, day-to-day ' 'operational expertise expected for an individual contributor DevOps ' 'role.' ), rate_skill_match = 2 , final_recommendation = 'reject' ) Note, that we order parameters to gradually focus and refine the information, until we come up with a concrete conclusion. Start by a generic summary of the candidate, narrow down to the skill rating and end up with a concrete decision. If LLM starts misbehaving in some situations, it would be possible to load back full SGR outlines for these cases and review them. 2. Routing Routing forces LLM to explicitly choose one specific reasoning path out of many. For example, in software triage we can force LLM to explicitly choose the path ("hardware" or "software"), followed by filling specific required details: from pydantic import BaseModel from typing import Literal , Union class HardwareIssue ( BaseModel ): kind : Literal [ "hardware" ] component : Literal [ "battery" , "display" , "keyboard" ] class SoftwareIssue ( BaseModel ): kind : Literal [ "software" ] software_name : str class UnknownIssue ( BaseModel ): kind : Literal [ "unknown" ] category : str summary : str class SupportTriage ( BaseModel ): issue : Union [ HardwareIssue , SoftwareIssue , UnknownIssue ] By passing SupportTriage to response_format , we will force LLM to make a choice and pick one of the branches. completion = client . chat . completions . parse ( model = "gpt-5-mini" , response_format = SupportTriage , messages = [ { "role" : "developer" , "content" : "triage support" }, { "role" : "user" , "content" : "My laptop screen keeps flickering and sometimes turns black." } ], ) print ( completion . choices [ 0 ] . message . parsed ) Parsed object will be of type HardwareIssue in this case: SupportTriage ( issue = HardwareIssue ( kind = 'hardware' , component = 'display' ) ) Tools can be represented with branches as well . Consider this schema for a personal business assistant that has access to a few tools: from pydantic import BaseModel , Field from typing import Union , Literal class SendEmailTool ( BaseModel ): tool : Literal [ "send_email" ] recipient_email : str subject : str message : str class SearchKnowledgeBaseTool ( BaseModel ): tool : Literal [ "search_knowledge_base" ] query : str class CreateSupportTicketTool ( BaseModel ): tool : Literal [ "create_support_ticket" ] customer_id : int issue_summary : str priority : Literal [ "low" , "medium" , "high" ] class Response ( BaseModel ): action : Union [ SendEmailTool , SearchKnowledgeBaseTool , CreateSupportTicketTool ] summary : str Here is how we can use this in action: system = "handle request of Rinat - support agent. Don't make things up" user = "Email to jessica@example.com, tell that her refund has been processed" completion = client . chat . completions . parse ( model = "gpt-5-mini" , response_format = Response , messages = [ { "role" : "developer" , "content" : system }, { "role" : "user" , "content" : user } ], ) Response can look like: action = SendEmailTool ( tool = 'send_email' , recipient_email = 'jessica@example.com' , subject = 'Your refund has been processed' , message = ( 'Hi Jessica, \n\n Your refund has been processed. If you do not see the ' 'refund on your account or have any questions, please reply to this ' 'email and I will investigate. \n\n Best, \n Rinat \n Customer Support' ) ) summary = 'Email notifying Jessica that her refund has been processed.' This is how we can wrap this code with actual tool calling: # ----- Mock Tool Implementations ----- def send_email ( recipient_email : str , subject : str , message : str ): print ( f "Sending email to { recipient_email } with subject ' { subject } '" ) print ( f "Body: \n { message } \n " ) def search_knowledge_base ( query : str ): print ( f "Searching KB for: { query } " ) def create_support_ticket ( customer_id : int , issue_summary : str , priority : str ): print ( f "Creating { priority } priority ticket for customer { customer_id } " ) print ( f "Issue: { issue_summary } " ) # Map tool type to handler TOOL_DISPATCH : Dict [ str , Callable ] = { "send_email" : send_email , "search_knowledge_base" : search_knowledge_base , "create_support_ticket" : create_support_ticket } # ----- LLM Wrapper ----- def handle_request ( system_prompt : str , user_prompt : str ): completion = openai . chat . completions . parse ( model = "gpt-5-mini" , response_format = Response , messages = [ { "role" : "developer" , "content" : system }, { "role" : "user" , "content" : user } ], ) response = completion . choices [ 0 ] . message . parsed print ( f "Summary: { response . summary } " ) tool_type = response . action . tool if tool_type in TOOL_DISPATCH : TOOL_DISPATCH [ tool_type ]( response . action ) else : print ( f "Unknown tool: { tool_type } " ) 3. Cycle Cycle explicitly forces to repeat reasoning steps. Here we are forcing LLM to come up with multiple risk factors. At least two, but no more than four: from pydantic import BaseModel from typing import List , Literal from annotated_types import MinLen , MaxLen class RiskFactor ( BaseModel ): explanation : str severity : Literal [ "low" , "medium" , "high" ] class RiskAssessment ( BaseModel ): factors : Annotated [ List [ RiskFactor ], MinLen ( 2 ), MaxLen ( 4 )] And the execution: user = "The server room has poor ventilation and outdated surge protectors." completion = client . chat . completions . parse ( model = "gpt-5-mini" , response_format = RiskAssessment , messages = [ { "role" : "developer" , "content" : "be brief" }, { "role" : "user" , "content" : user } ], ) response: factors = [ RiskFactor ( explanation = ( "Poor ventilation leading to elevated temperatures, increased " "risk of thermal shutdown, shortened hardware lifespan, and " "potential downtime." ), severity = "high" ), RiskFactor ( explanation = ( "Outdated surge protectors that may not adequately guard against " "voltage spikes or electrical faults, raising risk of hardware " "damage and data loss; replace with modern surge/UPS protection." ), severity = "high" ) ] By the way, we can use Cycle to extend the schema from the tool calling example to enable parallel tool execution like this: class Response ( BaseModel ): action : List [ Union [ SendEmailTool , SearchKnowledgeBaseTool , CreateSupportTicketTool ]] summary : str Now the response will contain a list of different tool calls that we can dispatch in parallel before passing the results back to LLM for further processing.


📦 SkuVault

📦 SkuVault At the end of 2014 AgileHarbor invited me to assist in the development of SkuVault : a rapidly growing web-based warehouse management system for eCommerce . I'll be assisting Slav Ivanyuk and his developers on a part-time basis. The role would be one of serving software engineer : suggesting solutions for the problems; sharing mistakes and good decisions from the other projects; offering software design ideas to simplify and scale existing system. This is the story of this journey.