[{"content":" AIDA: AI-Driven Applications, 4th Semester # Portfolio repository This site, live Group project, Standflow: a stall-holder application system for the Christmas market at Engestofte Gods.\nLive application Code repository Session coverage # 01: AIDA Kickoff: Bringing AI Into the Portfolio 02: RAG I: Three Ways to Build a Chatbot 03: RAG II: Making the Chatbot Keep Up With Itself 04: Code Agents: What It Actually Looks Like to Debug With One 05: Building an AI Rubric Assessor 06: Architecture of an LLM-Backed Rubric Assessor 07: Specs as the Theory Behind the Code 08/09: Standflow: From an Overflowing Inbox to a Working System 10: Building a Whole Project with a Code Agent 11: Secrets, Escaping and Safe Choices 12: The Legal and Ethical Side 13: The Excursion I Missed 14: Where MCP Would Fit in a Project Like Standflow ","date":"11 October 2026","externalUrl":null,"permalink":"/exam-portfolio/","section":"Stephanie Tess van Slyck","summary":"","title":"Exam Portfolio","type":"page"},{"content":"Datamatiker student, 4th semester. This portfolio documents how I build with AI as a component in real systems: RAG pipelines, code agents, LLM APIs, including the parts that broke and what the error messages actually said.\n","date":"11 October 2026","externalUrl":null,"permalink":"/","section":"Stephanie Tess van Slyck","summary":"","title":"Stephanie Tess van Slyck","type":"page"},{"content":"","date":"2 October 2026","externalUrl":null,"permalink":"/tags/aida/","section":"Tags","summary":"","title":"AIDA","type":"tags"},{"content":"","date":"2 October 2026","externalUrl":null,"permalink":"/tags/architecture/","section":"Tags","summary":"","title":"Architecture","type":"tags"},{"content":"","date":"2 October 2026","externalUrl":null,"permalink":"/categories/","section":"Categories","summary":"","title":"Categories","type":"categories"},{"content":"This journal documents my coursework in public: what I built, why I chose it, and what broke along the way.\nThe current focus is AIDA (AI-Driven Applications), the 4th-semester elective, covering RAG pipelines, code agents, LLM API integration, and the AI features on this site itself. Those posts are tagged AIDA.\nEarlier entries, from January to April 2026, cover JavaJolt, my 3rd-semester backend exam project: a RESTful Java API built over twelve weeks, where I was tech lead, scrum master and the whole development team at once. Those stay here as the record of that semester and are tagged JavaJolt.\nOne site, two semesters, one continuous professional record rather than a new repo per course.\n","date":"2 October 2026","externalUrl":null,"permalink":"/posts/","section":"Journal","summary":"","title":"Journal","type":"posts"},{"content":"","date":"2 October 2026","externalUrl":null,"permalink":"/tags/mcp/","section":"Tags","summary":"","title":"MCP","type":"tags"},{"content":"","date":"2 October 2026","externalUrl":null,"permalink":"/categories/reflection/","section":"Categories","summary":"","title":"Reflection","type":"categories"},{"content":"","date":"2 October 2026","externalUrl":null,"permalink":"/tags/reflection/","section":"Tags","summary":"","title":"Reflection","type":"tags"},{"content":"","date":"2 October 2026","externalUrl":null,"permalink":"/tags/","section":"Tags","summary":"","title":"Tags","type":"tags"},{"content":" What MCP is # A standard way for a model or agent to talk to external capabilities. A server exposes tools (actions it can take), resources (data it can read) and prompts (templates), over stdio for local use or streamable HTTP for remote.\nThe framing that made it click: it does not replace a REST API. REST is frontend-to-backend. MCP is model-to-tools. A real system can expose both, serving its own frontend over REST and its capabilities to an assistant over MCP.\nWhere it would fit in Standflow # An MCP tool wrapping existing endpoints, \u0026ldquo;list pending applications\u0026rdquo;, \u0026ldquo;draft a reply to this applicant\u0026rdquo;, so an assistant could help the organiser triage without a bespoke integration written for each action.\nAn MCP resource exposing the template library and FAQ, the non-personal, consented material, so a model reads the real current source rather than a stale copy pasted into a prompt.\nAn MCP prompt template for newsletter tone, so the phrasing stays consistent whoever is drafting.\nEach of those is a small capability behind a standard interface, which is the same boundary discipline that made working with a code agent safe: narrow, described, and explicitly scoped.\nHonest caveat # I have not built an MCP server for this project. This is a reflection on where it would go, not a report on something I shipped. What the lesson gave me is a clearer mental model, REST for app-to-backend and MCP for model-to-tools, and a concrete reason to keep AI features behind narrow, described tools rather than wiring a model straight into the database.\n","date":"2 October 2026","externalUrl":null,"permalink":"/posts/2026-10-02-where-mcp-would-fit/","section":"Journal","summary":"Model Context Protocol is not a replacement for a REST API. A short reflection on what it is, and where it would actually belong in a system I have already built.","title":"Where MCP Would Fit in a Project Like Standflow","type":"posts"},{"content":"","date":"29 September 2026","externalUrl":null,"permalink":"/tags/requirements/","section":"Tags","summary":"","title":"Requirements","type":"tags"},{"content":"","date":"29 September 2026","externalUrl":null,"permalink":"/tags/standflow/","section":"Tags","summary":"","title":"Standflow","type":"tags"},{"content":"","date":"29 September 2026","externalUrl":null,"permalink":"/tags/teamwork/","section":"Tags","summary":"","title":"Teamwork","type":"tags"},{"content":"The class visited the customer site, Engestofte Gods. I could not attend, so I will be straight about that rather than reconstruct a day I was not part of.\nBuilding for a context I knew second-hand # The team came back with a feel for the setting: the scale of the market, the kind of stall-holders who apply, practical constraints that do not show up in a requirements document. My job was to turn that into software without the first-hand impression, which meant leaning harder on the written material and the user stories, and asking the team the questions a site visit would have answered for me directly.\nWhat the trip fed in # The visit is where the framing came from: a real organiser, a real overload of email applications, a real need for a public approved-list and segmented communication. Even second-hand, that grounding is why Standflow solved an actual problem rather than an imagined one.\nWhere the project stands # All seven user stories are built and merged, the backend suite passes, and the app is deployed and reachable at standflow.roneu.dk. Since the excursion the work has been integration and polish rather than new features: the admin control panel now has a per-application detail view, search, filtering, sorting and CSV export, and there is an internal stall-holder overview alongside the public one. What remains on the list is file upload for stall photos and site plans, which needs the type and size validation described in the security post before it goes anywhere near production.\nTakeaway # Not being there made me more dependent on good specs and good team communication, which, given that half this course is about exactly that, turned out to be a useful constraint rather than only a missed day.\n","date":"29 September 2026","externalUrl":null,"permalink":"/posts/2026-09-29-excursion-i-missed/","section":"Journal","summary":"The class visited the customer site. I could not attend, so this is about building for a context I only knew second-hand, and where the project stands.","title":"The Excursion I Missed, and Building for a Place I Had Not Seen","type":"posts"},{"content":"","date":"25 September 2026","externalUrl":null,"permalink":"/tags/copyright/","section":"Tags","summary":"","title":"Copyright","type":"tags"},{"content":"","date":"25 September 2026","externalUrl":null,"permalink":"/tags/ethics/","section":"Tags","summary":"","title":"Ethics","type":"tags"},{"content":"","date":"25 September 2026","externalUrl":null,"permalink":"/tags/eu-ai-act/","section":"Tags","summary":"","title":"EU AI Act","type":"tags"},{"content":"","date":"25 September 2026","externalUrl":null,"permalink":"/tags/sustainability/","section":"Tags","summary":"","title":"Sustainability","type":"tags"},{"content":" The EU AI Act and my rubric assessor # The Act is risk-based, and AI used in education and assessment is explicitly high-risk. That lands directly on the rubric assessor I built.\nIt shaped a design decision rather than staying an abstract concern: the tool never returns a grade or a verdict. Every response is labelled as guiding, AI-generated feedback, and every response produces follow-up questions intended for a human conversation. That is human-in-the-loop and transparency made concrete, not a disclaimer bolted on afterwards.\nCopyright and RAG # RAG retrieves real documents and can reproduce them in its answers. For a bot built on my own portfolio that is fine, because the material is mine. The line worth stating: only index material you have the right to use, and ideally cite what was retrieved so a reader can check the source. Your own documents are the clean case. Someone else\u0026rsquo;s corpus without permission is not.\nSustainability # Generative AI has a real energy and water footprint. The lever a developer actually controls is inference: minimise what is sent per call, reuse prior results, and do not re-run unchanged jobs. The content-hash change detection in my RAG sync exists for cost reasons, but it has the same effect on energy. The cheapest call is also the greenest one.\nWhere I land # \u0026ldquo;High-risk\u0026rdquo; sounded heavy when I first read it, and then I built the thing and it stopped being an abstract category. A language model assessing an internship report has no access to the internship. It cannot know whether the student actually did what the report claims, only whether the text reads as though they did. Once that is clear, returning advisory feedback and follow-up questions instead of a grade is not a compromise made to satisfy a regulation. It is the only output the tool can honestly produce.\nThat is the pattern I keep finding in this part of the course: the responsible choice and the good-engineering choice usually turn out to be the same choice, arrived at from different directions. Only index material you have the right to use is also how you avoid a knowledge base full of content you cannot vouch for. Do not re-run unchanged jobs is both a token-cost argument and an energy argument. Do not let the model be the final authority is ethics, and it is also just an accurate description of what the model can do.\nWhere I am less comfortable is the energy question, because it is the one where my own incentives do not help me. I can justify every individual call I make. \u0026ldquo;It is only one call\u0026rdquo; is exactly the reasoning that scales badly, and I do not have a good answer for it yet beyond not being wasteful on purpose.\n","date":"25 September 2026","externalUrl":null,"permalink":"/posts/2026-09-25-legal-and-ethical/","section":"Journal","summary":"The EU AI Act puts educational assessment in the high-risk category, which lands directly on the rubric assessor I built. Plus copyright in RAG, and the energy cost nobody sees.","title":"The Legal and Ethical Side","type":"posts"},{"content":"","date":"21 September 2026","externalUrl":null,"permalink":"/tags/api-keys/","section":"Tags","summary":"","title":"API Keys","type":"tags"},{"content":"","date":"21 September 2026","externalUrl":null,"permalink":"/tags/gdpr/","section":"Tags","summary":"","title":"GDPR","type":"tags"},{"content":" Keys never touch the client # The frontend never calls the LLM provider directly, because anything in the browser is readable. The flow is frontend → my own backend → external API. The key sits in a backend .env, never committed, never sent to the client. Because every call goes through my backend, that is also where validation, rate limiting and logging live.\nXSS: handled by construction, and provable # Standflow\u0026rsquo;s chat cannot be injected, and I can say why rather than hoping. React escapes interpolated text automatically and there is no dangerouslySetInnerHTML anywhere in the codebase. Notification emails are sent as plain text, not HTML. Message fields are validated server-side: length limits, unknown fields rejected, strict JSON parsing.\nThe protection is not a filter we hope catches everything. The dangerous sinks, raw-HTML rendering and HTML email, simply do not exist in the data path.\nAccess control # Role-based, with four roles (ANYONE, USER, ADMIN, OWNER), enforced centrally before each endpoint runs rather than checked ad hoc inside handlers. Internal notes are never exposed publicly, and the public vendor page serves only non-identifying fields through its own DTO: no email, no phone, no CVR number.\nPasswords are hashed with BCrypt, sessions use JWT with a token version so tokens can be invalidated, and tokens are stored hashed with SHA-256.\nGDPR, built in # Three separate consents: terms and privacy are mandatory, marketing is optional, and the distinction is enforced in code. Only users who gave marketing consent receive newsletters; transactional mail is exempt because it is necessary to the service. Unsubscribe works through a secure token link without requiring login, and account deletion implements the right to be forgotten.\nStill on the list # File-upload hardening is designed but not built: magic-byte type validation so only PDFs and images are accepted regardless of file extension, a size limit, image compression, and SHA-256 deduplication. It becomes necessary as soon as stall-holders upload photos and the organiser distributes site-plan PDFs, so it is the next security work rather than a finished claim.\nWhat I learned # Good security here was mostly about removing the unsafe option rather than guarding it. The key is safe because the client cannot read it. The chat is safe because there is no raw-HTML sink. Marketing consent is safe because the code checks it before sending. Structural choices are the ones you can still explain, and prove, weeks later, which is a lower bar to clear than remembering every filter you wrote.\n","date":"21 September 2026","externalUrl":null,"permalink":"/posts/2026-09-21-secrets-escaping-and-safe-choices/","section":"Journal","summary":"How keys, escaping and consent are handled across my two projects, and why the safest choices were the ones that removed the unsafe option rather than guarding it.","title":"Secrets, Escaping and Safe Choices","type":"posts"},{"content":"","date":"21 September 2026","externalUrl":null,"permalink":"/tags/security/","section":"Tags","summary":"","title":"Security","type":"tags"},{"content":"","date":"21 September 2026","externalUrl":null,"permalink":"/categories/technical/","section":"Categories","summary":"","title":"Technical","type":"categories"},{"content":"","date":"21 September 2026","externalUrl":null,"permalink":"/tags/xss/","section":"Tags","summary":"","title":"XSS","type":"tags"},{"content":"The assignment behind this post was about collaborative agentic development: working with AI agents, and with each other, to build real software. I did not just try it on a toy task. I used an AI code agent to take over and finish an entire group project, Standflow, a stall-holder application system for a Christmas market, after a teammate ran out of credits mid-sprint and the remaining user stories had nobody on them.\nThis is the honest version of how that went.\nThe situation # Standflow had seven user stories spread across the group. Partway through, work stalled: branches were half-merged, one teammate was out, and the stories for newsletters, the public vendor page and the admin control panel were unfinished. With the tech lead\u0026rsquo;s go-ahead, I became the integration lead and used a code agent to close everything out: merge every open feature branch into our Review branch, resolve the conflicts, finish the newsletter frontend, wire the admin panel and the public vendor page to live data, fix a backend bug, and get the test suite green.\nThe thing I want to be precise about is the division of labour, because \u0026ldquo;I used AI\u0026rdquo; is not a reflection, it is a slogan.\nWhat the agent did, and what I kept # The agent read the code, wrote and edited files, made local commits, and explained what it was doing. I did the thinking that decides what \u0026ldquo;done\u0026rdquo; means, and I kept three things firmly in my own hands.\nI kept every irreversible git action. The agent was never allowed to push or merge. It made commits locally; I reviewed them and ran git push myself, in my own terminal. That one rule meant the agent could move fast without ever being able to put something into shared history that I had not looked at.\nI kept the conventions. Partway through I decided our commits should not carry AI co-author trailers, that there should be no comments left in the code, and that repo-facing text should be in English. The agent applied those going forward and amended the commits that had already broken the rules. The point is that the human owns the house style; the agent follows it.\nI kept verification honest. The agent\u0026rsquo;s sandbox could not reach the package registries, so it could not actually run our frontend build or the backend tests. It fell back to static checks: confirming every import resolved to a real file and that the syntax balanced. The real verification, mvn test and npm run dev, happened on my machine. I think this distinction matters more than anything else in the post: an agent saying \u0026ldquo;I checked it\u0026rdquo; can mean \u0026ldquo;I statically checked it,\u0026rdquo; which is not the same as \u0026ldquo;the tests pass.\u0026rdquo; Knowing where verification actually happens is part of the job now.\nThe guardrails that made it work # Our repo already shipped an AGENTS.md: read the architecture and code-standard docs first, only touch the files the issue needs, no unrelated refactors, no architecture changes without approval, tests and lint must pass, and a list of protected files. On top of that, our user stories were written as guardrail tickets, each with explicit acceptance criteria.\nThat combination is what let an agent work inside a real repo instead of generating a plausible-looking mess. The ticket said what \u0026ldquo;done\u0026rdquo; looked like; AGENTS.md said what the agent was not allowed to touch on the way there. When the agent proposed a new feature that needed an architecture decision, where to store uploaded files, it stopped and asked instead of guessing, because the guardrails told it to. A code agent without guardrails is a very fast junior who has never read your codebase. With guardrails, it is a very fast junior who respects your boundaries.\nThe near-disaster # It was not all smooth, and pretending it was would make this a worse reflection. At one point a pull request that should have gone into Review was merged into main instead. That is exactly the kind of mistake that is easy to make when you are moving quickly across a lot of branches.\nThe recovery was itself a lesson: reset main back to the last known-good commit with git reset --hard, push it back with git push --force-with-lease (the \u0026ldquo;with-lease\u0026rdquo; part is what stops you from clobbering someone else\u0026rsquo;s work you have not seen), and then re-merge the branch into Review the correct way. After that we were strict about base branches. The takeaway is not \u0026ldquo;AI caused a bug,\u0026rdquo; it did not, a human merge did, it is that speed without branch discipline is how you end up rewriting history at eleven at night.\nThere was also a very ordinary layer of friction underneath all of this: a new Windows laptop where Git, Node, Java and Maven all had to be installed from scratch, a Danish keyboard being read as a US layout, and, best of all, OneDrive quietly fighting Git because the repo lived inside a syncing folder, which caused lock conflicts and files that reappeared after I stashed them. The fix was to move the repo out of OneDrive entirely. None of that is glamorous, but it is the real texture of building software, and it is worth saying out loud that the agent could not fix my environment for me; it could only work once the environment worked.\nWhat I actually learned # A code agent changes where your effort goes, not how much of it you need. I spent almost no time typing boilerplate and almost all of it on three things: deciding what each ticket really required, reviewing what came back, and owning the git and verification steps the agent was not allowed to own. The quality of the output tracked the quality of the guardrails and the review, not the cleverness of any single prompt.\nIf I did it again I would write the guardrail tickets even more tightly, protect the base branches at the repository level so a wrong merge is impossible rather than just discouraged, and be even more explicit, from the first commit, about the difference between \u0026ldquo;statically checked\u0026rdquo; and \u0026ldquo;tested.\u0026rdquo; The project shipped and is deployed. The part I am proudest of is not that an AI wrote a lot of it, it is that I can tell you exactly which parts I verified, how, and where the gaps still are.\n","date":"18 September 2026","externalUrl":null,"permalink":"/posts/2026-09-18-building-a-whole-project-with-a-code-agent/","section":"Journal","summary":"I used an AI code agent to take over and finish an entire group project, Standflow. This is the honest version: what the agent did, what I kept for myself, the guardrails that made it safe, the near-disaster, and what actually makes agentic development work.","title":"Building a Whole Project with a Code Agent","type":"posts"},{"content":"","date":"18 September 2026","externalUrl":null,"permalink":"/tags/code-agents/","section":"Tags","summary":"","title":"Code Agents","type":"tags"},{"content":"","date":"18 September 2026","externalUrl":null,"permalink":"/tags/collaborative-agentic-development/","section":"Tags","summary":"","title":"Collaborative Agentic Development","type":"tags"},{"content":"","date":"18 September 2026","externalUrl":null,"permalink":"/tags/git/","section":"Tags","summary":"","title":"Git","type":"tags"},{"content":"","date":"18 September 2026","externalUrl":null,"permalink":"/tags/software-architecture/","section":"Tags","summary":"","title":"Software Architecture","type":"tags"},{"content":"","date":"14 September 2026","externalUrl":null,"permalink":"/tags/naur/","section":"Tags","summary":"","title":"Naur","type":"tags"},{"content":"","date":"14 September 2026","externalUrl":null,"permalink":"/tags/spec-driven-development/","section":"Tags","summary":"","title":"Spec-Driven Development","type":"tags"},{"content":"This lesson was about working with specifications in AI-driven development, anchored in Peter Naur\u0026rsquo;s essay Programming as Theory Building.\nNaur\u0026rsquo;s point, louder now # Naur argued the real product of programming is not the code but the theory the programmers hold: why the system is shaped the way it is, which changes are safe. Code is the residue. A code agent sharpens this: it can produce correct-looking code without anyone holding the theory, so the gap Naur warned about can open on day one. The defence is the specification. If the \u0026ldquo;why\u0026rdquo; is written down before the agent writes the \u0026ldquo;how,\u0026rdquo; the theory lives somewhere durable.\nIn practice # On Standflow every user story was written as a spec before any code, with a goal, acceptance criteria and boundaries, and AGENTS.md encoded the architectural theory. When a feature needed a decision the spec did not cover, such as how to store uploaded files, the work stopped and asked instead of inventing an architecture nobody had agreed to.\nWhat I will carry forward # Writing the spec is where I find the holes. Half the decisions I assumed were obvious turned out not to be, and I only discovered that because something had to be written down before any code existed. The clearest case was file storage: the ticket said stall-holders upload photos, and it took the work stopping and asking \u0026ldquo;where do these actually go, database or filesystem?\u0026rdquo; to show that nobody had decided. A ticket that vague would have been answered by whoever typed first.\nSo the specification is not paperwork I do before the real work. It is the real work, and it is the part that is actually mine once an agent is doing the typing. It is also the only part that still makes sense when I come back to this project in a month.\n","date":"14 September 2026","externalUrl":null,"permalink":"/posts/2026-09-14-specs-as-theory/","section":"Journal","summary":"Peter Naur argued the real product of programming is the theory the programmers hold, not the code. Working with a code agent makes that argument louder.","title":"Specs as the Theory Behind the Code","type":"posts"},{"content":"","date":"14 September 2026","externalUrl":null,"permalink":"/categories/project/","section":"Categories","summary":"","title":"Project","type":"categories"},{"content":"","date":"14 September 2026","externalUrl":null,"permalink":"/tags/project/","section":"Tags","summary":"","title":"Project","type":"tags"},{"content":"This post covers two lessons at once: the idea proposal that came out of a company visit, and the project description and system sketch that followed. The company was Engestofte Gods, a manor running a Christmas market, and the project became Standflow.\nI should say up front that I was not on that visit. The rest of the team went and came back with the brief, so everything below starts from their account and from the written material rather than from anything I saw myself.\nThe problem, in the customer\u0026rsquo;s own terms # The organiser runs a Christmas market with more than 80 stalls. Every stall-holder applies by email. That sounds manageable until you picture the inbox: applications arrive in no particular order, half of them are missing a piece of information, and there is no single place that answers the three questions the organiser actually has, who is still waiting on a decision, who is approved, and how do I send one message to a whole group of them without doing it by hand eighty times.\nSo the problem was never \u0026ldquo;build a website.\u0026rdquo; It was \u0026ldquo;replace an inbox that has stopped scaling.\u0026rdquo;\nThe idea we proposed # A customer register, not a form. One place where the organiser can read an application, approve it, reject it, or ask for the missing piece, and attach a private internal note to a troublesome one. On top of that, a way to message stall-holders, individually or in segments (same stall category, new this year, previous years), using reusable templates with merge-fields so a hundred personalised emails are written once. And a public side that shows only the approved stall-holders, so the market\u0026rsquo;s visitors see a clean list and the applicants\u0026rsquo; private data never leaks.\nThe system sketch # The architecture is a straightforward three-layer web application, and keeping it straightforward was a deliberate choice.\nThe frontend is React. It never talks to the database; it only makes HTTP/JSON calls to our own backend. The backend is Java with Javalin exposing a REST API, business logic in a service layer, and Hibernate mapping to PostgreSQL (with an in-memory H2 database standing in during tests). Authentication is JWT with BCrypt-hashed passwords, and every endpoint is gated by a role, ANYONE, USER, ADMIN or OWNER, checked centrally before the endpoint runs rather than scattered through each handler.\nThe data flows one way and is easy to narrate, which matters for an oral exam: a component in the browser calls the API, a controller validates the request, the service does the work, Hibernate persists it, and a DTO shapes what goes back out so internal fields never reach the client. That last detail is what lets the public vendor page exist safely next to the private admin panel: they read from the same data but through different DTOs.\nWhat it turned into # The sketch became seven user stories, each written as a ticket with acceptance criteria: login and consent, application handling, customer and admin chat, templates and variables, newsletters with recipient segmentation, GDPR (consent, secure-token unsubscribe, account deletion), and the paginated public vendor page. All seven are built, the backend test suite passes, and the app is deployed.\nThe lesson I took from the idea-to-system step is that the hardest part was not the architecture, it was being honest about what the customer\u0026rsquo;s problem actually was. \u0026ldquo;Too many emails\u0026rdquo; is not a feature request. Turning it into \u0026ldquo;a register with status, notes, segmented messaging and a public view\u0026rdquo; was the real design work; everything after that was execution.\n","date":"14 September 2026","externalUrl":null,"permalink":"/posts/2026-09-14-standflow-manor-house-to-system/","section":"Journal","summary":"The project case: a real customer with a real problem, the idea we proposed, and the system we sketched and then built to solve it. I was not on the company visit, so my part started from what the team brought back.","title":"Standflow: From an Overflowing Inbox to a Working System","type":"posts"},{"content":"This is the technical companion to the reflective post about the same project. This one is about the how: the data flow, the prompt contract, the failure modes, and the testing strategy.\nThe problem in one sentence # Take an unstructured internship report, apply a fixed rubric to it through an LLM, and return structured, machine checkable feedback, without ever letting the model wander outside the rubric or return something the client cannot parse.\nSystem overview # The request path is: client posts reportText to the Express server; the server loads rubric.json and builds a system prompt and a user prompt from it; the LLM client sends those to the chat completions API; the response comes back as a JSON string; validation checks it against the expected schema; and the server returns either the validated assessment or a controlled error.\nFour modules, each with one job:\nModule Responsibility rubric.json The single source of truth for criteria, weights, and level descriptors prompts.js Turns the rubric and the report text into a system prompt and a user prompt llmClient.js Owns the network call: timeout, error mapping, JSON parsing validate.js Checks the model\u0026rsquo;s JSON against the exact schema the client expects No module reaches past its neighbour. The server never talks to the model provider directly, and the LLM client never sees Express. That boundary is what made the whole thing testable without a real network call.\nThe validation step # response_format: { type: \u0026quot;json_object\u0026quot; } guarantees the model returns parseable JSON. It does not guarantee the JSON has the fields you asked for, in the shape you asked for, with values from the set you allowed. Those are two different failure classes and only one of them is solved by the API parameter. validate.js catches the second one before it reaches a client that might otherwise crash trying to read assessment.criteriaFeedback[3].level.\nPrompt design # Two prompts, built fresh on every request from the same rubric object, so the rubric and the prompt can never drift out of sync.\nSystem prompt sets the model\u0026rsquo;s role (an experienced, constructive advisor, not a judge), and does three things worth calling out: it forbids the model from using any criterion not in the rubric and forbids inventing quotes; it requires the model to say so explicitly when the report does not give it enough to go on; and it specifies the exact JSON shape expected in return, field by field.\nUser prompt carries two things and nothing else: the rubric serialised as JSON, and the report text wrapped in triple quotes as a clear boundary. Keeping them in the same message keeps the request stateless.\nTwo small settings do a lot of the stability work: temperature defaults to 0.2, low enough to keep repeated runs close to each other, and the request timeout defaults to 30 seconds with an AbortController, so a hung request fails loudly with a 504.\nError handling # Situation Status reportText missing or under 50 characters 400 API key not set 500 Request to the provider exceeds timeout 504 Network failure reaching the provider 502 Provider responds with a non-2xx status 502 Model response is not valid JSON 502 Model response is valid JSON but does not match the expected schema 502 That last row is the one most tutorials skip. A model that returns syntactically valid JSON with a missing disclaimer field, or a level value outside the rubric\u0026rsquo;s three allowed levels, is a real failure mode with real language models, and it deserves the same explicit handling as a network timeout.\nTesting without a real API call # The environment this was built in has no route to the npm registry, so express and dotenv could not be installed. Rather than treat that as a dead end, it turned into a forcing function for a cleaner separation of concerns.\nrubric.json, prompts.js, and validate.js have zero external dependencies. That made it possible to write a dry run that loads the rubric and example reports, builds the real prompts, runs a small mock model standing in for the provider, runs the mock response through the same validation the real server uses, and writes the result for inspection.\nAll three example reports produced schema valid output in this dry run. That proves the plumbing: rubric loading, prompt construction, and schema validation all work correctly end to end. It does not prove the model gives good feedback. The honest way to say this: the dry run tests the pipe, not the water flowing through it.\nWhat I would build next # A numeric score derived from the existing per criterion weights; a small script that runs the same report through the real API multiple times to measure how stable temperature 0.2 is in practice; and basic request authentication before this ever runs anywhere reachable by more than one person.\n","date":"4 September 2026","externalUrl":null,"permalink":"/posts/2026-09-04-rubric-assessor-architecture/","section":"Journal","summary":"How a small Express service turns a rubric and a student report into structured, schema-validated AI feedback: the data flow, the prompt contract, the error handling, and how it was tested without a real API call.","title":"Architecture of an LLM-Backed Rubric Assessor","type":"posts"},{"content":"","date":"4 September 2026","externalUrl":null,"permalink":"/tags/express/","section":"Tags","summary":"","title":"Express","type":"tags"},{"content":"","date":"4 September 2026","externalUrl":null,"permalink":"/tags/llm/","section":"Tags","summary":"","title":"LLM","type":"tags"},{"content":"","date":"4 September 2026","externalUrl":null,"permalink":"/tags/node.js/","section":"Tags","summary":"","title":"Node.js","type":"tags"},{"content":"","date":"4 September 2026","externalUrl":null,"permalink":"/tags/prompt-engineering/","section":"Tags","summary":"","title":"Prompt Engineering","type":"tags"},{"content":"The day started messy, the way most of my good days do. I had three unrelated thoughts sitting in my notes before I even opened the actual assignment: how do you build theory, not just text, when you are working with code agents all day, and who actually owns that theory once the agent has written it. A half formed idea about a screenscraper that only needs to run once a week, and whether that result should live in a JSON file or in a database. And a genuinely boring but necessary question: how do you schedule a Linux cron job to hit an API endpoint with curl once a week, without babysitting it.\nNone of those made it into today\u0026rsquo;s build directly, but I want to keep them here because they are the kind of question that only shows up when you are actually building things regularly, not when you are studying for an exam.\nThe real task was this: build a small AI powered application that gives a first, guiding assessment of an internship report using a rubric, and a call to an external LLM API. Not \u0026ldquo;get an answer from a model.\u0026rdquo; Design a small application where an LLM is one piece of a real data flow.\nThe part that felt obvious and wasn\u0026rsquo;t # I assumed the rubric would be the easy part. Read the requirements document, turn the bullet points into criteria, done in fifteen minutes. It was not fifteen minutes.\nThe requirements document for a praktikrapport is short. Company description, how you met the learning objectives, the tasks you did and how they connect to theory from school, reflection on your personal development, reflection on what both you and the company got out of it, and proof you filled in the evaluation form. That is five requirements, not six criteria, and definitely not six criteria with three distinguishable difficulty levels each.\nTurning \u0026ldquo;reflect on your personal development\u0026rdquo; into something a model can actually score consistently forced me to be honest about what \u0026ldquo;good\u0026rdquo; looks like here. It is not enough to say a student reflected. A low score report barely mentions it. A middle score report gives an episode but only shows one dimension of growth. A high score report shows the DARE, SHARE, and CARE values we are actually graded on, through something the student lived through, not something they claim.\nWhere I got stuck # I wanted to test the whole thing end to end against a real OpenAI call, and I could not. The sandbox this was built in has no route to the npm registry, so npm install failed on the very first dependency. No Express, no dotenv, nothing.\nMy first instinct was frustration, but the fix ended up teaching me something about designing for testability that I would not have learned if the install had just worked. I split the logic that has nothing to do with the network, rubric loading, prompt construction, and response validation, into plain Node functions with zero external dependencies. Then I wrote a tiny mock model that returns a schema shaped response using simple keyword matching instead of a real LLM call. That let me prove the entire data flow works without ever touching the network.\nIt is not a substitute for testing against a real model. The mock cannot tell me if the model actually gives useful, stable feedback. But it proved something that matters just as much before spending a single token: the plumbing does not lie to you.\nThe thing I keep circling back to # Building this made me think about my own practicum reports differently. If a version of this tool had graded mine, would it have caught what actually mattered, or would it have rewarded whichever report happened to be the most fluently written.\nThat is the honest limitation I wrote into the disclaimer field of every response this app returns: this is a guiding, AI based assessment, not truth. A language model has no access to what actually happened during someone\u0026rsquo;s internship. It can only judge the text in front of it, and text can be well written without being true, or true without being well written. The rubric narrows how much room the model has to freelance, it does not remove the gap between \u0026ldquo;sounds reflective\u0026rdquo; and \u0026ldquo;is reflective.\u0026rdquo;\nI do not think that makes the tool useless. I think it makes it exactly what the assignment asked for: a first pass, a set of follow up questions to bring into an actual conversation with a real person, not a verdict.\nWhat I am taking into tomorrow # Test the boring parts first, especially when the exciting part (the actual model call) is the one thing you cannot fully control today. A rubric is not a checklist, it is a definition of what \u0026ldquo;good\u0026rdquo; means, and writing one honestly is harder than reading one.\nNext up: the technical writeup, with the architecture, the prompt design choices, and the actual JSON schema, for anyone who wants the how instead of the why.\n","date":"31 August 2026","externalUrl":null,"permalink":"/posts/2026-08-31-rubric-assessor-reflection/","section":"Journal","summary":"A personal walkthrough of building a small AI grading tool: the loose thoughts I started with, the wall I hit, and what it taught me about trusting a model to judge someone else’s work.","title":"Building an AI Rubric Assessor: What Today Actually Taught Me","type":"posts"},{"content":"","date":"31 August 2026","externalUrl":null,"permalink":"/categories/journal/","section":"Categories","summary":"","title":"Journal","type":"categories"},{"content":"","date":"31 August 2026","externalUrl":null,"permalink":"/tags/prompt-design/","section":"Tags","summary":"","title":"Prompt Design","type":"tags"},{"content":"","date":"31 August 2026","externalUrl":null,"permalink":"/tags/rubric/","section":"Tags","summary":"","title":"Rubric","type":"tags"},{"content":"Session 4 (Kodeagenter, meaning code agents) has an exercise that is simple to state: solve a programming task with a code agent, and document the workflow. I missed presenting this one live in class, but the honest version is that I spent the day actually doing the exercise instead, on a real task rather than a toy one: getting the RAG chatbot from session 3 from \u0026ldquo;architecture decided\u0026rdquo; to \u0026ldquo;actually working end to end.\u0026rdquo; An AI code agent did the hands-on work throughout. This is that log.\nThe point of this exercise isn\u0026rsquo;t \u0026ldquo;the agent got it right first try.\u0026rdquo; It\u0026rsquo;s the opposite, every one of the friction points below is a place where the agent\u0026rsquo;s first assumption was wrong in a specific, diagnosable way, and fixing it was a real back-and-forth, not a black box. That\u0026rsquo;s a more honest and more useful thing to document than a clean success story would be.\nFriction 1: the agent can\u0026rsquo;t reach the internet the way you\u0026rsquo;d assume # First surprise: the code agent\u0026rsquo;s own execution environment, and its bridge to my laptop, turned out to be sandboxed behind a network allowlist. Calls to Dify\u0026rsquo;s API came back 403 Forbidden from an internal proxy, not from Dify itself. The same restriction had already quietly affected an earlier git push.\nThe fix wasn\u0026rsquo;t clever, it was just correct: run the actual API-calling script from my own real Terminal, where the network access is normal, and have the agent write and hand me the code rather than trying to execute it live. Worth naming as a real lesson, a code agent\u0026rsquo;s own sandbox is part of the system you\u0026rsquo;re debugging, not a neutral wrapper around your machine.\nFriction 2: the rate limit wasn\u0026rsquo;t the rate limit we expected # Dify\u0026rsquo;s free Sandbox tier caps knowledge-base calls at 10/minute. Standard practice is to detect that via an HTTP 429. Dify doesn\u0026rsquo;t do that, it returns a 403 with the real reason buried in a JSON message ({\u0026quot;code\u0026quot;:\u0026quot;forbidden\u0026quot;,\u0026quot;message\u0026quot;:\u0026quot;...rate limit...\u0026quot;}). The first version of the sync script\u0026rsquo;s retry/backoff logic only watched for 429, so the first ten documents synced fine and then the rest failed immediately with no retry at all.\nDiagnosed straight from the actual error text in the logs, then patched: detect this specific 403-plus-message pattern as a rate limit too, and self-throttle to roughly one call every 6.5 seconds so we mostly avoid tripping the limit in the first place, rather than only reacting after the fact.\nFriction 3: the docs didn\u0026rsquo;t match the UI # Dify\u0026rsquo;s own documentation page for finding your API key didn\u0026rsquo;t match the actual Cloud dashboard layout. It took a few rounds of screenshots back and forth before finding the real control (a \u0026ldquo;Service API\u0026rdquo; link, top-right of the Knowledge list page, not where the docs implied it\u0026rsquo;d be). Small, but a good reminder that an agent\u0026rsquo;s training data can be stale against a product\u0026rsquo;s current UI in ways that are only findable by actually looking at a screenshot together.\nFriction 4: the embedding model ran out of runway, twice # Once documents were syncing, the knowledge base itself wouldn\u0026rsquo;t come online, the chat app kept showing it as \u0026ldquo;Unavailable.\u0026rdquo; Cause: Dify\u0026rsquo;s Sandbox plan shares one pool of 200 free AI credits across every default model provider, for both chat and embeddings. That pool was already spent.\nChasing a free embedding provider turned into its own small saga. Google AI Studio was blocked by a regional account restriction. Jina AI\u0026rsquo;s dashboard kept dropping me onto an interactive request-builder page instead of anywhere to actually get a key. Hugging Face worked, but not on the first try, the first model I picked was tagged sentence-similarity when Dify\u0026rsquo;s integration specifically requires feature-extraction, and after switching models, the first API token I generated didn\u0026rsquo;t have the right permission scope (it needed a fine-grained token with \u0026ldquo;Make calls to Inference Providers\u0026rdquo; explicitly checked, not the default read-only kind).\nThen, once the new embedding model was finally live: switching an existing, already-populated dataset to a differently-sized embedding model doesn\u0026rsquo;t work. OpenAI\u0026rsquo;s model produces 1536-dimension vectors; the Hugging Face one produces 384. Dify\u0026rsquo;s vector index is fixed to whatever dimension it was created with, and every document errored out with a flat \u0026ldquo;vectors dimensions does not fit.\u0026rdquo; The real fix wasn\u0026rsquo;t a config change, it was structural: build a brand-new knowledge base from the start with the new embedding model selected, so its vector index is the right shape from day one. Or better, sidestep embeddings entirely by choosing Dify\u0026rsquo;s \u0026ldquo;Economy\u0026rdquo; indexing mode, which matches by keyword instead of by vector and needs no embedding model at all. Given how much of the day had already gone to chasing free embedding credits, that felt like the right call rather than a compromise.\nFriction 5: even the \u0026ldquo;no embeddings needed\u0026rdquo; path had its own graph to untangle # Dify\u0026rsquo;s newer knowledge bases are built as visual pipelines, a small node graph (data source, extractor, chunker, knowledge base) rather than a single form. The template came pre-wired with data-source nodes for Notion, Google Drive, Jina Reader, and Firecrawl, none of which I use, and Dify refused to publish the pipeline until every node in it, used or not, was either configured or removed. Deleting the unused ones then left two \u0026ldquo;Variable Aggregator\u0026rdquo; nodes with dangling references to the data sources that no longer existed, which had to be found and cleared by hand, one validation error at a time, before publish would succeed.\nFriction 6: the last mile was mundane, and mundane things still break # Getting from \u0026ldquo;it works when I run it manually\u0026rdquo; to \u0026ldquo;it runs itself\u0026rdquo; surfaced two very ordinary problems that had nothing to do with AI at all: a stale .git/index.lock file from an earlier interrupted commit blocking every subsequent git commit, and, after wiring the sync into GitHub Actions with repository secrets, a \u0026ldquo;document limit reached\u0026rdquo; error, because an old, permanently broken dataset from the failed embedding-model attempts was still sitting in the workspace, counting against the same free-tier document quota as the new, working one. Deleting it fixed the sync in one shot. Neither problem was interesting, and both were exactly the kind of thing that eats an afternoon if you don\u0026rsquo;t know to look for them.\nWhere it ended up # By the end of the day the chatbot\u0026rsquo;s knowledge base syncs itself automatically: pushing a content change to main triggers a GitHub Actions workflow that diffs what changed, calls Dify\u0026rsquo;s API directly (from GitHub\u0026rsquo;s own runners, which don\u0026rsquo;t have the sandboxing problem from friction 1), and keeps the mapping file that makes reruns idempotent. No manual re-upload, ever again, for as long as that workflow keeps running.\nAs a small bonus once the core pipeline was solid, we used the same collaborative pattern, agent proposes, human reviews, to build a second, smaller agent: a GitHub Actions step that reads each push\u0026rsquo;s commit messages and diff, asks an LLM to draft a devlog entry about it in my own established style, and opens a pull request with the draft rather than publishing anything automatically. It\u0026rsquo;s a fitting note to end a session about code agents on: an agent that helps document the work of other agents, with a human still firmly in the loop before anything goes live.\nThe honest takeaway # Every one of these six friction points had a plausible, wrong first answer. Sandboxing looked like a permissions bug, the rate limit looked like it wasn\u0026rsquo;t happening at all, the dimension mismatch looked like a settings problem, the pipeline errors looked unrelated to what I\u0026rsquo;d actually changed. None of them were solved by the agent guessing harder; they were solved by reading the actual error text, checking the actual UI, and treating each wrong assumption as data rather than trying the same fix twice. That back-and-forth, propose, verify, correct, retry, is the actual skill this exercise is about, more than any individual line of code that came out of it.\n","date":"28 August 2026","externalUrl":null,"permalink":"/posts/2026-08-28-debugging-with-a-code-agent/","section":"Journal","summary":"","title":"Code Agents: What It Actually Looks Like to Debug With One","type":"posts"},{"content":"","date":"28 August 2026","externalUrl":null,"permalink":"/tags/dify/","section":"Tags","summary":"","title":"Dify","type":"tags"},{"content":"","date":"28 August 2026","externalUrl":null,"permalink":"/tags/github-actions/","section":"Tags","summary":"","title":"GitHub Actions","type":"tags"},{"content":"","date":"24 August 2026","externalUrl":null,"permalink":"/tags/ci/cd/","section":"Tags","summary":"","title":"CI/CD","type":"tags"},{"content":"","date":"24 August 2026","externalUrl":null,"permalink":"/tags/rag/","section":"Tags","summary":"","title":"RAG","type":"tags"},{"content":"Today\u0026rsquo;s session picked up exactly where the last one left off: a RAG chatbot is only as good as its last upload, and manually re-uploading content every time a post changes isn\u0026rsquo;t a real solution, it\u0026rsquo;s just a chore waiting to be forgotten. The actual task was getting a chatbot onto this portfolio site that answers questions about the content on it, and automating it so it updates itself whenever a post is added or changed. No manual step, no logging into a dashboard.\nBefore touching code, the plan was to actually lay out the options and their tradeoffs rather than pick the first thing that sounded reasonable, a habit worth carrying over from last semester\u0026rsquo;s more process-heavy systems development work, where picking and documenting a method mattered as much as the code itself. So: brainstorm first, Kanban board and pull requests to track it, decision written down before implementation.\nRoutes considered, roughly in order of how tempting they looked before the cost/benefit math caught up:\nA ChatGPT Custom GPT or CustomGPT.ai, ruled out for the same reasons as last session: no real automation path, or priced for a company, not a student. Dify\u0026rsquo;s built-in website/sitemap import, crawling the live site via a third-party scraper (Jina Reader or Firecrawl), genuinely less code to write, since Dify does the crawling itself. But it looks like every resync re-crawls and re-embeds the entire site rather than just what changed, and it adds a dependency on yet another external service. I even found a working example of exactly this approach from a reference repo: sitemap in, Jina Reader converts each page to markdown, pushes it to Dify, and it confirmed the concern directly, no change detection at all, every URL gets re-uploaded on every run. Self-hosting Dify on the DigitalOcean droplet instead of using their Cloud tier, no document limits, but a real Docker Compose stack to maintain (Postgres, Redis, TLS) for a portfolio-scale chatbot. Filed as a phase-two idea, not a today problem. A fully custom-built RAG pipeline, no platform at all, the most impressive on paper, and the least realistic given the timeline. Worth naming as considered and rejected, since that\u0026rsquo;s an honest engineering call, not a shortcut. Landed on: Dify Cloud, with a GitHub Action that pushes only what changed. Dify\u0026rsquo;s Knowledge Base API supports creating and updating documents by text directly, separately from its chat API, which means the sync doesn\u0026rsquo;t need Dify to read the repo at all, no GitHub token, no scraper. The script hashes each markdown file\u0026rsquo;s content and only calls the API when the hash has actually changed, which makes the whole thing idempotent, re-running it never creates duplicate documents, and keeps it well inside the free tier\u0026rsquo;s rate limit instead of re-embedding the entire site on every push.\nWhat\u0026rsquo;s actually built as of today: the Python sync script and the GitHub Actions workflow are written, tested for logic, and sitting in an open pull request against this repo. Dify account and knowledge base setup is in progress. The chat widget isn\u0026rsquo;t embedded yet, and end-to-end testing, edit a post, push, watch it show up in the bot\u0026rsquo;s answers, hasn\u0026rsquo;t happened yet either. That\u0026rsquo;s genuinely where it stands: decided, mostly built, not yet proven. Next entry should either confirm it works or explain what broke.\n","date":"24 August 2026","externalUrl":null,"permalink":"/posts/2026-08-24-rag-automation/","section":"Journal","summary":"","title":"RAG II: Making the Chatbot Keep Up With Itself","type":"posts"},{"content":"Second session was the actual introduction to RAG, retrieval-augmented generation, or in plain terms: how to connect an LLM to documents it wasn\u0026rsquo;t trained on, so it can answer questions grounded in your material instead of whatever it happened to learn during training. We talked through when RAG is worth the complexity (you have a real, changing body of domain content you want answered from) and when it isn\u0026rsquo;t (a static FAQ doesn\u0026rsquo;t need a vector database).\nThe useful part of the session was comparing three concrete ways to actually build one:\nChatGPT\u0026rsquo;s Custom GPT, fastest to a working demo, zero infrastructure, but it lives inside ChatGPT. No real API for automating what\u0026rsquo;s in its knowledge, and nothing you can embed as a widget on your own site.\nCustomGPT.ai, a proper hosted RAG platform with a real API, closer to production-grade. The catch: no meaningful free tier, plans start around $99/month, which is a hard sell for a student project.\nDify.ai, open source, has a genuinely free Cloud tier, and exposes both a chat API and a separate Knowledge Base API for managing documents programmatically. That combination, free plus scriptable, is what made it the one worth actually building with.\nWe used Dify for the mini-project: uploaded the study curriculum (studieordning) PDF, converted to markdown first since markdown chunks and indexes far more predictably than raw PDF text, and had a working chatbot answering questions about the programme within the session. It\u0026rsquo;s a small example, but it made the abstract \u0026ldquo;embeddings and retrieval\u0026rdquo; explanation from earlier concrete, you could watch it retrieve the actual paragraph an answer came from, which does a lot for trusting (or correctly not trusting) what it says.\nThe obvious next problem, which session three is apparently going to be entirely about: this only works if someone remembers to re-upload the document every time it changes. For a study curriculum that\u0026rsquo;s rare. For a portfolio blog that updates weekly, that\u0026rsquo;s not going to hold up.\n","date":"21 August 2026","externalUrl":null,"permalink":"/posts/2026-08-21-rag-first-attempt/","section":"Journal","summary":"","title":"RAG I: Three Ways to Build a Chatbot, One Afternoon to Compare Them","type":"posts"},{"content":"","date":"17 August 2026","externalUrl":null,"permalink":"/tags/ai/","section":"Tags","summary":"","title":"AI","type":"tags"},{"content":"New semester, new elective: AI Drevne Applikationer (AIDA). First session was the usual kickoff, covering course structure, exam expectations, what \u0026ldquo;AI-driven applications\u0026rdquo; actually covers, and how LLMs fit into modern software architecture rather than just being a chat window bolted onto the side of an app.\nThe practical task was getting a portfolio website running with Hugo and the Blowfish theme. I already had mine, this same site, originally built for the JavaJolt backend project last semester, so instead of setting one up from scratch, session one became a chance to look at it with a different lens. One portfolio, multiple courses, and now a second discipline layered on top of the first: REST APIs and Hibernate on one side, LLMs and agentic tooling on the other. That\u0026rsquo;s honestly the more interesting outcome than a fresh repo would have been, since it\u0026rsquo;s one continuous professional site instead of a new one per class, which is what I\u0026rsquo;d actually want to hand someone with a CV.\nOn expectations for AI in software development: coming from a semester that was almost entirely \u0026ldquo;build the backend correctly by hand,\u0026rdquo; I\u0026rsquo;m curious where the line actually sits between AI as a tool that removes grunt work and AI as something that changes what the work even is. Writing DAOs and JPA mappings by hand taught me the mechanics; I don\u0026rsquo;t think I\u0026rsquo;d have understood why an agent\u0026rsquo;s suggestion was right or wrong without having done that first. So my honest expectation going in is that this course is less about learning to use AI and more about learning to supervise it, reading its output critically, knowing when to trust a retrieval result, and documenting the reasoning behind the choices it\u0026rsquo;s involved in, not just the code it produced. First real test of that starts next session with RAG.\n","date":"17 August 2026","externalUrl":null,"permalink":"/posts/2026-08-17-aida-kickoff/","section":"Journal","summary":"","title":"AIDA Kickoff: Bringing AI Into the Portfolio","type":"posts"},{"content":"","date":"17 August 2026","externalUrl":null,"permalink":"/tags/portfolio/","section":"Tags","summary":"","title":"Portfolio","type":"tags"},{"content":"","externalUrl":null,"permalink":"/authors/","section":"Authors","summary":"","title":"Authors","type":"authors"},{"content":"","externalUrl":null,"permalink":"/projects/","section":"Projects","summary":"","title":"Projects","type":"projects"},{"content":"","externalUrl":null,"permalink":"/series/","section":"Series","summary":"","title":"Series","type":"series"}]