31 Aug 2026
Planet Python
Graham Dumpleton: Introducing wrapture
For the best part of two decades, through wrapt, I have been dealing with the mechanics of monkey patching in Python. Anyone who has followed wrapt will know I am quite pedantic about correctness, to the point of caring whether a wrapper preserves every last introspectable detail of the thing it wraps. For much of that time I have wanted the same standard from the tools I use when testing code, and unittest.mock, which does what it does well enough, was never designed to give it. A fabricated Mock answers every method call and verifies nothing. A patched call records a flat list of calls, with no return values and no sense of what was called from what. The calls an object makes to itself are invisible, because the substitute never runs the real code at all. What I want a test to be sure of, that the right calls happened, in the right order, with the real code actually running, sits just outside what substitution can express.
Seeing the real calls as they happen, with the real code still doing the work, was also something I had already spent years on for a quite different purpose. I was the original author of the New Relic Python agent, written while I worked there, and that left me with a lasting interest in instrumenting Python programs. Attaching observation to code you do not control, recording what flows through it, and doing so without disturbing the program being watched, is a problem I have never really stopped thinking about.
Testing and tracing look like different problems, but from where I sat they wanted the same thing, and I had long believed that wrapt's approach of wrapping real code rather than replacing it could serve both. wrapture (the name being wrapt plus capture) is me finally getting around to finding out whether that belief held up.
Wrap anything, capture everything
The one idea everything in wrapture sits on is to wrap rather than replace. A binding names a location in code, a method of a class or a function in a module, and when applied installs a wrapt wrapper around the real callable. Unless you tell it otherwise the wrapper is transparent. The real code runs, with wrapture in a position to watch the call, change it, or answer it instead.
Take a small call graph where an order service charges a payment gateway and then records the result in a ledger:
import wrapture
class Gateway:
def charge(self, amount, currency="USD"):
return {"id": f"ch_{amount}", "amount": amount}
class Ledger:
def record(self, entry):
return f"led_{entry['id']}"
class OrderService:
def __init__(self):
self.gateway = Gateway()
self.ledger = Ledger()
def place(self, amount):
result = self.gateway.charge(amount)
self.ledger.record(result)
return result
None of these classes import wrapture or know they are about to be observed. Bindings are created by naming the methods, and a timeline opens a recording scope in which every call through them lands on a tape:
place = wrapture.binding(OrderService, "place")
charge = wrapture.binding(Gateway, "charge")
record = wrapture.binding(Ledger, "record")
with wrapture.timeline(place, charge, record) as tape:
OrderService().place(500)
print(tape.tree())
Running this the output is:
__main__:OrderService.place(amount=500) -> {'id': 'ch_500', 'amount': 500}
__main__:Gateway.charge(amount=500, currency='USD') -> {'id': 'ch_500', 'amount': 500}
__main__:Ledger.record(entry={'id': 'ch_500', 'amount': 500}) -> 'led_ch_500'
That is the call graph as it actually ran. The arguments are normalised against the real signatures (so charge(500) and charge(amount=500) look the same), the return values are the real ones, and the nesting comes from what really called what. The tape.tree() call is a convenience for debugging and for demonstrations like this one; in a test you would query the tape instead, and outside of a test the events would be going to a sink, which I will come to.
The same bindings intervene as well as observe. The real method can be stubbed out, made to fail, or left running while one thing about the call is changed on the way in or out:
gateway = Gateway()
with wrapture.binding(Gateway, "charge").on_call.raises(TimeoutError("down")):
gateway.charge(500)
Inside the block the call raises TimeoutError, and after the block exits the original method is back exactly as it was.
Three uses of one mechanism
That is the whole mechanism. What makes it interesting is that it serves three purposes which are usually handled by three different tools.
The first is plain monkey patching. wrapt's wrap_object() has always been able to patch a target, but it leaves the bookkeeping to you. wrapture adds a lifecycle and a vocabulary over the top of it. A binding declares a target without touching it, apply() installs the patch, remove() restores the original, suspend() makes it inert in place, and a group of bindings applies and removes as one unit. Behaviour is configured on the binding, with returns(), raises(), transforms_args(), transforms_result() and a few others, and can be scripted to change over time, so "succeed twice, then time out" is three lines rather than a hand-written counter. This layer is useful on its own with nothing else switched on.
The second is unit testing, which is where the recording comes in. Because the real code runs, a test can assert on how calls actually flowed through it, and the interesting cases are the error paths. Inject a failure at the gateway, then check that the ledger was never written:
with wrapture.timeline(place, charge, record) as tape:
charge.on_call.raises(TimeoutError("down"))
try:
OrderService().place(500)
except TimeoutError:
pass
record.events.assert_never()
print(tape.tree())
With the assertion passing, the tree shows where the failure was injected and how it propagated:
__main__:OrderService.place(amount=500) !! TimeoutError
__main__:Gateway.charge(amount=500, currency='USD') !! TimeoutError (injected)
When a test must supply a stand-in, because the code under test receives a collaborator rather than importing it, wrapture provides stub() for a callable and mock(Spec) for a whole object, and these record onto the same tape as everything else. Both are strict: signatures are checked and nothing is invented on first touch. There is deliberately no spec-less Mock() equivalent, and the comparison with unittest.mock in the documentation explains why, alongside a mapping of each mock idiom to its wrapture counterpart. An opt-in pytest plugin sweeps each test for patches left applied and attaches recordings to failure reports.
The third use is ad-hoc tracing of a running application, including one you cannot modify or redeploy. Take the bindings, drop the test around them, and the only remaining question is where the events go. A sink answers that. In practice this is done with a wrapture.toml file naming the targets and the sink, and no code at all. Here is one for a slightly bigger version of the shop above, where the gateway declines some cards and the order service logs a warning when it does:
[[observe]]
target = "shop:OrderService"
name = "place"
[[observe]]
target = "shop:PaymentGateway"
match = "*"
exclude = "_*"
[[observe]]
target = "shop:Ledger"
name = "record"
[[log]]
name = "shop.*"
[[sink]]
type = "printer"
Running the program as python -m wrapture main.py applies the config before the program starts, so the patches are in place before the application imports anything, and the printer sink writes the trace to stderr as it happens:
shop:OrderService.place(order_id='order-1', amount=30, card='5100-0010')
shop:PaymentGateway.charge(amount=30, card='5100-0010')
shop:PaymentGateway.charge -> 'ch_30' [7us]
shop:Ledger.record(order_id='order-1', amount=30)
shop:Ledger.record -> 'ledger:order-1:30' [5us]
shop:OrderService.place -> 'ch_30' [234us]
shop:OrderService.place(order_id='order-2', amount=240, card='5100-0020')
shop:PaymentGateway.charge(amount=240, card='5100-0020')
shop:PaymentGateway.charge -> 'ch_240' [4us]
shop:Ledger.record(order_id='order-2', amount=240)
shop:Ledger.record -> 'ledger:order-2:240' [3us]
shop:OrderService.place -> 'ch_240' [105us]
shop:OrderService.place(order_id='order-3', amount=75, card='4000-0030')
shop:PaymentGateway.charge(amount=75, card='4000-0030')
shop:PaymentGateway.charge !! PaymentDeclinedError [4us]
log shop.orders WARNING 'order order-3 declined'
shop:OrderService.place !! PaymentDeclinedError [260us]
Unlike the tidy reconstruction from tape.tree(), this is the live view, with an opening line as each call begins and a closing line with the outcome and how long it took. The [[log]] entry captures the application's ordinary logging calls as events too, so the warning appears nested inside the call that logged it rather than somewhere in a separate log file. With autowrapt installed, even the launcher is unnecessary: AUTOWRAPT_BOOTSTRAP=wrapture in the environment applies the same config at interpreter startup, so the program runs with plain python. That covers the case where something else owns the command line, like a container entry point or a WSGI server.
The printer is the simplest sink. Others stream events to disk as JSON lines, count without retaining, and compose with fan-out, sampling and filtering. Sitting on top of the tracing layer is OpenTelemetry export: with the wrapture[otel] extra installed, one [otel] table in the config sends the same events to any OTLP backend as spans, metrics and correlated logs. Every tree of events carries a W3C trace id, and the id arrives and leaves in traceparent headers, so two services both observed by wrapture join up as one distributed trace without either of them calling an OpenTelemetry API.
The point I want to land is that these are layers of one mechanism and not separate products. The binding vocabulary that stubs a method in a test is the same one that traces it in production, and the config that names methods for a printed call tree is the config that exports spans. What starts as a monkey patch or a test assertion can grow into observability without the code being rewritten along the way.
Where it sits beside what already exists
Part of why I built this is that nothing I could find did all of it. unittest.mock records a flat call list with no nesting and no return values, and a patched call returns a fabricated MagicMock rather than running the real code. Span assertion tools such as OpenTelemetry's InMemorySpanExporter require the code to already be instrumented. Tools built on sys.settrace give you a firehose with no assertion API. APM agents are all-or-nothing products rather than toolkits, and their auto-instrumentation only covers the frameworks they already know about. wrapture needs none of that. You point at your own methods by name and a trace appears, and the same pointing is how it lands in a test, a terminal, or a backend.
Just as important is what it is not. It is not a fabrication tool, and unittest.mock remains the right thing for invented objects. It is not a production APM, although it is a toolkit that APM-like things could be built on. And it is not an OpenTelemetry competitor; it emits to OpenTelemetry rather than trying to replace it.
Pre-built instrumentation
Pointing at your own methods is the core of wrapture, but for common third-party packages the pointing has already been done. The companion wrapture-instrumentation package provides ready-made instrumentation, with Flask and Jinja2 covered so far. Each records a request or a template render as one structured tree, and enabling one is an [[instrument]] entry in wrapture.toml naming the target. Installing the package brings in wrapture and nothing else; the instrumentation for a package you do not have is inert. It is being built target by target, and the instrumentation packages guide describes how to write one for a package not yet covered.
Built with AI, on purpose
Every line of code and documentation in wrapture was written by an AI assistant working under my direction. I want to be upfront about that, and equally upfront about what it was not. This was not vibe coding, where a one-shot prompt produces a pile of generated code and the person driving hopes for the best because they lack the knowledge to judge what came back. Vibe coding has earned its bad reputation. I engineered wrapture carefully from the start. I have spent a long time in this particular corner of Python and knew exactly what the result needed to be, and the AI was the means of producing it rather than the source of the design.
The experiment had two halves. The first was whether an idea I had carried around for years actually held up once built. The second, and just as much the point, was whether a library of this kind could be produced this way, with an AI doing the writing and me doing the directing, to a standard I would be happy to put my name to.
The process is what makes the result worth trusting or not, so it deserves describing. The work started well before any code, with days spent on design documents setting out the goals, the scope, the shape of the API and the layers it would be built in, which the AI and I argued over before implementation began. From there it proceeded in layers, each one specified, discussed, implemented, tested and documented before the next began. Documentation grew with the code rather than after it, and every example in the docs runs as a doctest, so the docs are continually proven against the implementation. Writing them repeatedly exposed designs that read worse than they demoed. The test suite runs against every supported Python version, including the free-threaded builds, on every change. The overhead of Python instrumentation is the usual objection to it, so the recording path was also put through a performance pass, with the cost of a call observed and exported through wrapture measured against the same call instrumented directly with the OpenTelemetry SDK and in the style of its instrumentation packages. The result was comparable per call, with the figures in the OpenTelemetry export guide.
The step I would most recommend to anyone attempting something similar came late. I took the unit test suites of well-known Python packages that lean heavily on unittest.mock and had the AI replicate their tests using wrapture instead, side by side with the originals. Every point of friction became a decision: sometimes a documented position on why wrapture deliberately differs, and sometimes a missing feature that got specified, built and documented like everything else. Several pieces of wrapture exist only because a real test suite could not be expressed cleanly without them.
Throughout, the division of labour was consistent. The AI wrote the code, the tests and the prose. I set the direction, made the design calls, reviewed what came back, and sent plenty of it back. My experience with wrapt and with Python's darker corners is all through the result, in what was asked for as much as in what was refused.
The first commit was in the middle of August and the current release is the eleventh alpha, so this all happened in a bit over two weeks. In that time it accumulated over 1000 tests and over 150 pages of documentation. The documentation is admittedly quite dense in places and needs some work still, but it is complete in the sense that every part of the package is covered. One thing a brand new library has over an old one is coherence. Mature packages accrete features one release at a time, and there is never a moment when the whole API can be redesigned to match what was learned along the way. Because wrapture arrived in a compressed period with the whole design still in view, when validation showed a design could be better it was redesigned rather than worked around.
I know some people are firmly opposed to using AI-written software. If that is you, I understand, and I am not going to argue with your position. It is a reasonable one to hold, and this post exists so you can make the call with the facts in hand rather than discover them later. If AI involvement rules wrapture out for you then wrapture is not for you, and that does not worry me one bit. This has been about finding out whether the process works, and I now have my answer to that. The longer version of all this is on the how wrapture was built page in the documentation.
What's next
wrapture is in alpha, with pre-releases on PyPI. Until 1.0.0 is final a plain pip install wrapture picks up the latest pre-release, so there is no need to pin a version. It requires Python 3.12 or later and wrapt 2.4.0 or later. The API is complete for the three uses described above and I am not expecting it to break, so code written against it today should carry forward to 1.0.0.
What it needs now is use. unittest.mock and OpenTelemetry's own instrumentation are the established tools for the two halves of what wrapture does, and the open question is whether an alternative that does both from one mechanism is something people want. Reports of it working, or not, on real code, and of what confused or was missing, are what will decide whether anything changes before a beta. They go to the issue tracker.
To be clear, wrapture was never premised on anyone else using it. I built it because I wanted to see it exist, not because I had identified a gap in the market. If people find it useful and pick it up, that is great, and I will aim to support it. If there is no interest, I will keep treating it as an experiment and work on it for my own purposes. Either way the questions got answered, and getting them answered was the point.
There is a lot more in wrapture than fits in an introduction, and I expect to write about specific parts of it in follow-up posts, starting with how it can be used for unit testing, and then tracing a Flask application through to an OpenTelemetry backend without touching the application code. For now the getting started page is the place to begin.
31 Aug 2026 1:39pm GMT
Ed Crewe: From Routing Checks to Trajectory Testing: Evaluating an Agentic Chatbot
Which Agentic Chatbot?
I have been working on a Python based AI test framework for a chatbot interface for my company's product, Postgres AI Hybrid Manager. The manager allows the setup of Postgres clusters across cloud or on-prem and attaching various AI tools such as Langflow. So a combination of more traditional Postgres backup, migration, telemetry and analytics features along with LLM workflows leveraging the data it holds.
The product already has a control plane UI for managing Postgres estates. It also has full help for the product, all Postgres versions, analytics, AI and add ons. The chatbot brings all these things together: ask a question, get the relevant help, or ask it to do something such as migrate a cluster, or evaluate telemetry that would otherwise require clicking through the UI.
That makes it a pretty handy interface, especially for the less technical. However it is not simple to test and ensure good quality responses.
A normal deterministic API test is simple. Send a request, check the status code, check the JSON body, perhaps check the database state. An LLM-backed agent does not pass or fail so clearly. It can route to the wrong capability and still return fluent text. It can pick a plausible but wrong tool. It can miss half the task and still sound confident. It can complete the first turn of a conversation and lose the plot on the second. It could get malformed or missing data from tooling that leads it to deliver a misleading conclusion. It might only provide help to something that should be from tool data or was a request for an action such as create a cluster.
So the testing problem was not "does the chatbot return a reasonable response?" It was "how do we test the whole chat path is doing the right thing?"
This is the story of how our agent-eval test framework evolved as we worked to see that our chatbot was not only getting the right answer, 42 , but whether it was asking all the right questions of the right tools to get that answer. Known as trajectory testing ...
You're Golden
Before we can tell our story we need to define some terms.
A Golden is an example of a perfect desired output from a test input. They often refer to more complex outputs that may need saving as separate files, but a simple assertable output such as 42, is a golden too!
Whilst complex goldens may be used and marked for semantic similarity against the test output. It is more common for complex outputs to be described by a rubric. A rubric is a checklist of qualitative properties a good answer must exhibit, written in plain English as opposed to a golden example of an answer.
For AI testing the tests are termed evals, ie they evaluate the tool, but not by strict assertions, because one thing you can be sure of with an LLM is that given the same input, you usually get subtly different output, ie they are non-deterministic. Which means for LLM outputs the only way to test them is to use an LLM-as-judge, ie give that LLM the test output and a rubric or golden and let it mark it against that. Then you set a pass threshold for that mark, to translate your complex output into a pass or fail.
You can also total up all the passes to give you a Task Completion Rate, TCR. So with complex AI agentic LLM interactions a 100% pass of all evals is often not realistic. Hence you set a TCR below 100% for the whole test suite of evals to pass. Start with the smallest useful test. The core principle of evals is not complicated, you want the input to give you the expected output.
But for an Agentic application this may require a sequence of LLM calls and tools: Making the final output dependent on the route that should be chosen, the tool(s) that should be called, the actions to be taken, further LLM calls that may be necessary and finally the core data that the response to the user should contain.
Our first version did not try to solve every part of that. It started with routing, simple and deterministic.
Routing is the starting point
The chatbot originally had an agent per tool. The tool being the code and API calls that performed actions or returned data or help.
Different specialist agents owned different parts of the product surface: Control-plane actions, Postgres database operations, schema design, roles and permissions, cluster reporting, migration, and so on.
Before any specialist can help, something has to choose the right specialist.
So the first eval suite asked a narrow question:
Given this user prompt, did the chatbot route to the expected tool?
That gave us a fast health check. We could keep a corpus of prompts, map each one to an expected destination, run them through either a direct model path or against the real deployment and its tools, and score whether the selected destination tool matched the golden.
- id: "core-iam-001"
prompt: "List all my projects"
expected_tool: "control-plane"
tags: ["core", "control-plane", "project"]
And the check on the other end is deliberately dumb - an equality test, not a semantic one:
self.success = tool_match(predicted_tool, expected_tool)
Agents became skills, but routing remained
The design moved away from "one agent per tool family" toward a more consolidated orchestrating agent with skills that could better compose different tool use for common tasks.
That is a better fit for how modern agent systems are evolving. A skill = instructions, constraints, and a subset of tools that are relevant for a task. It is a form of progressive disclosure. Give the model the minium it needs at each step to save tokens.
But this did not make routing irrelevant.
Instead of asking "did we transfer to the right sub-agent?", the eval asks "was the right skill made visible and selected for this task?" The labels changed but a skill could still use the wrong tool.
Routing evals stayed valuable because they were fast, explainable, and easy to run in CI. But they are limited, routing should always be correct but it doesn't mean that the final agent response is too.
TCR jumps to the endpoint, the response
Task Completion Rate, or TCR, was the next step.
The user asked for a cluster comparison, or a schema recommendation, or help diagnosing a database issue. We need to know whether the full response actually completed these tasks.
Responses are complex goldens so they need the LLM-as-a-judge pattern: run the chatbot, take the actual response, and ask a judge model to score it against expected sections.
The eval has a rubric here for judging the output:
- id: "tcr-core-014"
prompt: "Compare CPU usage between these two clusters"
expected_sections:
- "identifies which cluster has higher CPU usage"
- "cites at least one supporting metric"
- "suggests a plausible next step"
The judge gets one simple instruction: score each expected_sections between 0.0-1.0 A metric class then just thresholds it for pass / fail:
self.success = score >= 0.7
The judge must be calibrated and a consistent model used for comparing runs over time. Consistent judging enables skill and/or prompt tuning from metric trends. The rubric must be specific enough to avoid marking waffle as success. But it turns a non-deterministic complex output into a simple pass and fail. It also separated two different levels of QA:
- Can the underlying model answer the task if given the right context?
- Does the deployed chatbot complete the task through the real product path?
That led to two execution modes.
Direct mode calls the model with simulated context. It is faster and useful for prompt and rubric development.
Proxy mode calls the real chatbot. It is slower, but it exercises the production path: routing, skill selection, tool calls, guardrails, streaming responses, conversation state, and the actual service wiring.
Both matter. Direct mode tells you whether the model is capable of the answer. Proxy mode tells you whether your product is capable of really delivering it via agents running your deployment's tools.
This is the major difference from standard AI LLM testing, the model is only a small pluggable engine for the full agentic skill set that requires the actual deployment domain of data, actions and tools. Direct mode testing of only the model, is occasionally useful but E2E testing of the Chatbot deployment is required for agentic AI Chatbot QA, tuning and validation.
Multi-step conversations changed the unit of testing
Single-turn TCR is still too small for many real chatbot tasks.
Users do not always provide all required information in one message. They ask to create a cluster, then pick a project, then choose a size, then confirm. They ask for a schema review, then refine the problem, then ask for a migration path. They troubleshoot by adding information over time.
So the framework has to exercise test cases that are conversations, not just single prompts.
That sounds like a minor data-model change. It was not. Once a test has steps, the eval runner has to preserve conversation state. In proxy mode, that means carrying the real conversation_id returned by the chatbot and sending each follow-up as part of the same server-side conversation. In direct mode, it means building a synthetic conversation history so the model sees the prior turns.
In code that split is about as literal as it sounds. Proxy mode threads a real id through each call:
response = client.send_message(prompt=msg, conversation_id=conversation_id)
conversation_id = response.conversation_id # captured on turn 1, reused after
Direct mode has no server-side conversation to lean on, so it fakes one by re-rendering the transcript into the prompt itself, every turn:
full_prompt = f"## Conversation History\n{render(history)}\n\n{next_prompt}"
Same test case, same expected outcome, but a different code path depending on which half of the system is actually holding the conversation state. That's impacts multi-turn evals because conversation memory is part of the harness code for the actual deployment not just a model issue.
The scoring also becomes more interesting. You want per-step checks, because the assistant should ask the right clarifying question at the right time. You also want an overall score, because a conversation can have reasonable individual turns and still fail to complete the user's goal.
Coding it yourself: deepeval underneath
Everything above sits on top of deepeval, the open-source LLM eval library. We add a Synthesize → Execute → Evaluate pipeline, a plugin system, YAML goldens, CI wiring, and Langfuse push on top of it But the core library underneath is plain deepeval, and you do not need any of the surrounding machinery we used. Here are routing, TCR and multi-step just built directly on deepeval (simplified deepeval 3.6.9)
A test case is just an input/output pair. LLMTestCase is the base unit everything else scores:
from deepeval.test_case import LLMTestCase
test_case = LLMTestCase(
input="List all my projects",
actual_output=chatbot_response_text, # what the system under test said
expected_output="control-plane", # the golden - a skill label here, not prose
additional_metadata={"predicted_skill": predicted_skill},
)
Routing is a custom metric, not a built-in one. deepeval ships plenty of semantic metrics, but "did it route to the right skill" is an exact-match business rule, so you write your own BaseMetric. This is a simplified version of the same shape our real AgentMatch metric takes:
from deepeval.metrics import BaseMetric
from deepeval.test_case import LLMTestCase
class AgentMatch(BaseMetric):
def __init__(self, threshold: float = 1.0):
self.threshold = threshold
self.async_mode = False # routing checks are cheap; no need for async here
def measure(self, test_case: LLMTestCase) -> float:
predicted = test_case.additional_metadata["predicted_skill"]
expected = test_case.expected_output
self.score = 1.0 if tool_match(predicted, expected) else 0.0
self.success = self.score >= self.threshold
return self.score
async def a_measure(self, test_case: LLMTestCase) -> float:
return self.measure(test_case)
def is_successful(self) -> bool:
return bool(self.success)
@property
def __name__(self):
return "Agent Match"
tool_match is the check from earlier. Run it with deepeval's own runner rather than hand-rolled assertions, and you get retries, pretty output, and a result object for free:
from deepeval import evaluate
evaluate(test_cases=[test_case], metrics=[AgentMatch()])
TCR is where deepeval's built-in GEval earns its keep. GEval is deepeval's off-the-shelf LLM-as-judge metric, you give it criteria (or explicit evaluation steps) and it handles the judge prompt, the JSON parsing, and the scoring for you. Our rubric-per-line expected_sections maps onto evaluation_steps almost directly:
from deepeval.metrics import GEval
from deepeval.test_case import LLMTestCase, LLMTestCaseParams
task_completion = GEval(
name="TaskCompletion",
evaluation_steps=[
"Check whether the response identifies which cluster has higher CPU usage",
"Check whether the response cites at least one supporting metric",
"Check whether the response suggests a plausible next step",
],
evaluation_params=[LLMTestCaseParams.INPUT, LLMTestCaseParams.ACTUAL_OUTPUT],
threshold=0.7,
)
test_case = LLMTestCase(
input="Compare CPU usage between these two clusters",
actual_output=chatbot_response_text,
)
evaluate(test_cases=[test_case], metrics=[task_completion])
Multi-step conversations get their own test case type. ConversationalTestCase takes a list of Turns instead of a single input/output pair, and pairs with a BaseConversationalMetric instead of BaseMetric:
from deepeval.test_case import ConversationalTestCase, Turn
convo = ConversationalTestCase(
turns=[
Turn(role="user", content="Create a new cluster"),
Turn(role="assistant", content="Sure - which project should it go in?"),
Turn(role="user", content="acme-prod"),
Turn(role="assistant", content=final_response_text),
],
expected_outcome="A cluster is created in acme-prod after resolving the missing project name",
)
deepeval has a conversational counterpart to GEval too (ConversationalGEval), scored against the whole turn sequence rather than a single response which is the natural fit for "did the assistant ask the right clarifying question at the right time", the per-step-plus-overall shape TCR needed once prompts became conversations.
Put together, that is the whole starting kit: LLMTestCase plus a hand-written BaseMetric for hard business rules like routing, GEval for rubric-style task completion, ConversationalTestCase plus ConversationalGEval once a prompt becomes a conversation, and evaluate() to run the lot and get a result object back.
Everything else we built, the YAML goldens, the plugin architecture, the CI wiring, the Langfuse push exists to run more of these at scale and make the failures easy to find. But none of it is required to get started. If you are testing your own agentic chatbot, this is how to begin.
This is where instrumentation started to matter much more.
For a single-turn answer, a markdown report with pass/fail rows is often enough to start debugging. For multi-step conversations, that is thin. You need to know which turn failed, whether the route changed, whether the wrong tool was called, whether the tool call used correct arguments, whether the model forgot earlier context, or whether the final answer simply missed a required section.
That is why we added span-level telemetry and pushed eval traces into Langfuse.
Langfuse made the failures inspectable
The useful thing about Langfuse is not just having another pretty dashboard. Although that is important for spotting quality regressions over time via regular CI/CD automated runs.
The vital thing was being able to treat an eval run as a set of traces. A run becomes a session. Each test case becomes a trace. The trace carries the prompt, response, scores, tags, model, mode, scenario, and the spans emitted by the proxy.
For a chatbot path, those spans are where the debugging starts. You can see routing, tool execution, LLM calls, latency, and token usage where it is available. You can filter by scenario and model. You can compare runs. You can look at a failing conversation and see whether the problem began at route selection, tool selection, tool arguments, or final synthesis.
That changes the tuning loop.
Without traces, an eval failure says "this case failed". With traces, it can say why it failed.
That distinction matters because the fix lands in different places...
Is it a routing rule?
Is it a skill description?
Is it a tool schema?
Is it the judge rubric?
Is it that the eval has has an expectation that the product has never actually promised?
Trajectory testing -> knitted the pieces together
Routing and TCR started as separate signals.
Routing asked whether the right capability was selected. TCR asked whether the final task was completed. Multi-step testing asked whether that held across a conversation. Instrumentation showed what happened between those points.
Trajectory testing is the next natural step: score the path itself.
For an agentic product, the fully correct path is essential to response quality.
So trajectory tests add expectations about intermediate actions:
- which tool or flow should be used
- whether the arguments are valid
- whether the conversation reached the right state
- whether the final answer completed the task
The label-based routing tests are still useful as fast canaries. They tell us whether the classifier shape has drifted and distinguish tiers - see the next section.
But full trajectory tests judge the route by consequence: did the system actually follow the tool path that would satisfy the user?
So retain the fast determisitc routing tests, but move more user-visible behavioural coverage into trajectory and TCR.
Sovereign AI makes the eval problem tiered S/M/L/XL
There is one more constraint that makes this more than a generic chatbot-testing story.
Our chatbot has to work for sovereign and air-gapped deployments. In those environments, prompts, tool results, schema details, and operational data cannot be sent to a hosted frontier model outside the customer's trust boundary. The inference model may run inside the customer's environment.
That usually means a smaller model.
Smaller models are not just cheaper versions of larger ones. They have different context limits, weaker tool-selection behaviour, and less tolerance for an over-wide capability surface. If you show a smaller model every possible tool and skill, you have increased the chance that it chooses a bad one.
So the architecture becomes tiered. Models are effectively T-shirt sized. A small self-hosted model sees a curated subset of reliable skills. A larger model can be allowed to see more. Some experimental or complex skills only make sense for the highest tiers.
That changes the meaning of a routing eval again.
The correct visible skill set is no longer universal. It depends on the model tier. A prompt that should route to an advanced skill for an XL model may need to be dropped, refused, or handled differently for a smaller model that should not see that skill at all.
This is why trajectory testing and routing need to be tier-aware. We are not only asking whether the chatbot can complete a task. We are asking whether it can complete the task through the capability surface that a deployment's LLM size allows.
What I would keep from the journey
The final shape was not obvious at the start.
We began with routing because it was the first integration failure point and the cheapest one to isolate. We added TCR because correct routing did not prove task completion. We added multi-step cases because real users have conversations, not isolated prompts. We added telemetry because multi-step failures are otherwise too hard to debug. We moved toward trajectory testing because the route, tools, arguments, and answer need to be judged as one path.
If I were starting another agentic product eval framework, I would keep that order.
Do not start by trying to build a grand universal benchmark. Start with the smallest failure point that would embarrass the product if it regressed. Then move the signal closer to the user's actual goal.
For a chatbot wired into a real control plane, that means testing more than the output text. It means testing the route, the skill, the tool call, the arguments, the conversation state, the final answer, and the model tier that made those options visible in the first place.
That is the difference between checking that an AI system said something vaguely relevant and checking that it actually did all the things the user asked of it.
31 Aug 2026 9:28am GMT
28 Aug 2026
Django community aggregator: Community blog posts
Issue 352: PyCharm & Django Fall Fundraiser
News
PyCharm & Django Fall Fundraiser

Buy or renew an annual PyCharm Professional license through the campaign link and you get 30% off while JetBrains donates a matching amount to the DSF, which is working to close the gap between the roughly $300,000 it raises each year and the $500,000 that would make a full-time Executive Director sustainable. The campaign runs through September 10, 2026, so buy or renew before then. A renewal adds 12 months to your existing subscription.
Django Software Foundation
DEP 0020: Annual Release Cycle
Django moves to one feature release each January under YYYY.N calendar versioning, starting with 2028.0 in place of what would have been 7.0. Every release now gets a year of mainstream support and two years of security fixes, which retires the LTS label.
The Block and Tackle of Django's Code of Conduct Working Group
The machinery behind Django's move to Contributor Covenant 3.0: a 30-day public comment period enforced by a GitHub workflow, CODEOWNERS gating changes to the CoC text, and an automated decision changelog. The working group is releasing its case-tracking templates under CC BY 3.0 for other projects to adopt.
Python Software Foundation
RISC-V is now officially supported by CPython!
CPython now supports RISC-V at tier 3, the entry level for new platforms, which means the open instruction set architecture gets ongoing testing on real hardware through buildbots donated by the RISE Project. Stan Ulbrych led the work with Ludovic Henry, Furkan Onder, and Emma Smith, backed by a Sovereign Tech Agency fellowship.
Wagtail CMS News
Wagtail 8.0
A read-and-write v3 REST API, custom base page models, a global permission policy registry, and formalized Django 6.1 support. Two smaller wins: StreamField block IDs are now available as template context variables, and AVIF and WebP images are no longer converted to PNG by default.
Wagtail security releases: 7.0.9, 7.3.4, and 7.4.3
7.0.9, 7.3.4, and 7.4.3 carry the same five fixes as 8.0: permission handling in the Pages, Documents, Images, and translation APIs, document identification by SHA1, and snippet copying. Upgrade to the patch release matching your version.
Streamlining content ops with LLMs: Wagtail user guide
Google Summer of Code contributor Raghad Dahi rebuilt the user guide site, retiring a versioning scheme that made editors duplicate the whole site per release in favor of blocks readers can filter by version. For translations, an evaluation suite scored LLM providers on cost and quality before settling on DeepSeek V4 Flash, now covering 52 languages plus right-to-left support.
CMS with AI, not AI CMS: Wagtail 8.0's new API
The thinking behind that API: rather than bolting AI buttons onto the admin, Wagtail exposes 50+ admin operations with OpenAPI docs and Markdown rich text, drivable from curl, a script, or an MCP implementation. Worked examples include fixing SEO descriptions with an LLM and a generic content importer in about 100 lines.
Django Fellow Reports
Django Fellow Report - Jacob
Jacob Tyler Walls filed an early report before heading to DjangoCon US, with his usual prolificacy triaging three tickets, reviewing nine, and authoring seven. In addition, engaged with regular security reports and provided 1-1 mentoring to his GSoC mentee, Pravin.
Editors Note
All 3 Fellows are at DjangoCon US this week, so no formal report from Sarah or Natalia. All 3 also gave excellent talks that we will link to when the videos are available later this year.
Articles
Fuzzy String Matching in Django and PostgreSQL
Four ways to match misspelled and variant names, with the trade-offs spelled out. It expands on the author's DjangoCon US 2026 talk on search-as-you-type across 54 million names.
Modern Django Deployments in 2026: My DjangoCon US 2026 Conference Talk
The slides and notes from Will Vincent's recent talk on deployments.
Nifty Django Feature: Third-Party Packages
A helpful overview of Django's third-party package ecosystem, where to look, and how to apply it in your Django projects.
When Python is Too Slow
An opinionated guide on where to turn when Python feels too slow in your application (the answer isn't always switch to Rust).
The Move to Python 3 Begins!
CCP is moving EVE Online's 2.4 million lines of Python off Stackless Python 2.7, in place since 2010, with the first changes deployed on August 25. Stage one leans on automated tools to make the code compile under both versions: 95.9% of roughly 20,000 files already do, leaving about 3,300 blocking lines (1,500 print statements, 800 long literals like 123L, 600 old-style except clauses) and another 20,000 lines that compile but behave differently and need a human to look at each one.
Core Dispatch #10
A roundup of CPython development from August 5 to 27: Python 3.12.14, 3.11.16, and 3.10.21 shipped on the 12th, 3.15.0 release candidate 2 is due September 1, and six PEPs moved, including PEP 805 on safe parallel execution and PEP 833 reaching Final for the simple repository API. Most of the discussion energy went to the competing module export proposals in PEPs 842, 843, and 844.
Events
DjangoCon Europe 2027 in Austria!
Five days of Django, Python, and community in Innsbruck, Austria, February 17-21, 2027.
DjangoCon US 2027 in Riverside, California
Join us for five days of inspiration, education, and networking at the Riverside Convention Center in beautiful Riverside, California, September 13-17, 2027.
Call for Organizers: DjangoCon US 2027
That Riverside conference needs people to run it, and more than 15 committees are recruiting, from program and sponsorship to Code of Conduct, A/V, website, and sprints. Leadership roles carry the heaviest load, with weekly check-ins and monthly board reports, but most positions do not require previous organizing experience: email hello@djangocon.us to volunteer.
Django Forum
Public thanks to our 3 Fellows
A short forum thread appreciating Django's Fellows.
Podcasts
Django Chat #205: Django Developers Survey 2026
A special summer episode on the just-released 2026 Django Developers Survey, working through what it says about Django 6.1, HTMX, async, AI, deployment, testing, and Python tooling.
Django Job Board
Two foundation roles anchor the board this week, with the DSF hiring its first Executive Director and the PSF looking for a Security Developer, alongside two full stack engineering openings.
Full Stack Software Engineer (Hybrid) at Provision
Executive Director at Django Software Foundation
AI-Assisted Software Engineer, Web Applications at Logical Media Group
Security Developer at Python Software Foundation
Projects
15r10nk/matchify
Converts eligible if/elif/else chains into Python 3.10+ match statements, preserving runtime behavior and source formatting, including isinstance checks that become class patterns with attributes.
matiasb/django-tasks-fennel
A Django Tasks backend which uses Celery as its underlying queue. Mentioned as part of the author's talk at DjangoCon US this week: Teach Django Tasks to speak Celery: Building a Celery backend for Django Tasks.
28 Aug 2026 3:00pm GMT
Planet Python
Django Weblog: Django Developers Survey 2026 results
The results from the 2026 Django Developers Survey are now available. This is the fifth annual report conducted from May to July 2026 by the Django Software Foundation in collaboration with JetBrains PyCharm.
The full report includes infographics, quotes, and dedicated sections so you can easily navigate the results. There is also a The State of Django 2026: Boring is so back blog post highlighting key Django trends in 2026 and actionable ideas for your own Django development.
The Django Chat podcast also covers the survey in a special summer episode, from Django 6.1 and HTMX to async, AI, deployment, testing, and Python tooling.
28 Aug 2026 2:55pm GMT
27 Aug 2026
Django community aggregator: Community blog posts
Building large features for Django
It's been another month of slow writing and not for want, but in between holidays and the same amount of work, the blog post just got squeezed out each week. Additionally the GSoC project that I have been mentoring took a slight turn into more of a research based thing rather than trying to push anything into Django right now.
However with the GSoC research itself and the recent features of tasks, the email updates, my own prodserver package, I'm beginning to solidify in my head what a modern Django feature looks like. One thing to clarify here is when I use the word feature, I'm referencing a concept that Django can represent, such as Databases, Tasks, Emails & Storage.
These features while different in what they achieve have a very similar architecture within Django, marked by some common characteristics:
- A common API for the rest of Django to use
- A single settings configuration, typically a dictionary
- Pluggable backends that do the actual implementation and specified in the above settings
- Minimal backend implementations inside Django (except Databases), with extra implementations provided as community packages
This architecture leans into Django being an API layer for various concepts that all tie together to become a website or web app. I'm taking this route with prodserver and this approach also taken by by mentee for GSoC when producing django-experimental and django-featurevault.
Django Experimental is a proof of concept around how an experimental features may be added to Django at some point. Django Feature Vault is a similar package specifically targeting an API for feature flags native to Django. Please do give them a read and a spin on a project if you like and raise issues. Both also have associated draft DEPs (Experimental, Feature Flags) available to review and comment.
I wonder if we can more formally codify this architecture (perhaps a copier package template?) to continue to smooth the on-ramp for those that want to contribute new features and ideas to Django and the community.
27 Aug 2026 5:00am GMT
26 Aug 2026
Django community aggregator: Community blog posts
Modern Django Deployments in 2026: My DjangoCon US 2026 Conference Talk
A written guide to my talk on deploying Django and why 90% of it is the same.
26 Aug 2026 11:57am GMT
06 Aug 2026
Planet Twisted
Hynek Schlawack: Production-ready Python Docker Containers with uv
Starting with 0.3.0, Astral's uv brought many great features, including support for cross-platform lock files uv.lock. Together with subsequent fixes, it has become Python's finest workflow tool for my (non-scientific) use cases. Here's how I build production-ready containers, as fast as possible.
06 Aug 2026 12:00am GMT
23 Jun 2026
Planet Twisted
Glyph Lefkowitz: Adversarial Communication
As I have discussed in previous posts, "AIs" can make mistakes. In fact, they do make mistakes, and their mistake-making patterns are such that where and how they will make mistakes is both uncertain and constantly changing.
Thus, in any scenario where you want to attempt to make "productive" use of "AI", you must have a system in place for checking every result. Not checking some results; checking every result. If each result might have a consequence for you (and if it didn't have a consequence, why bother automating it?) and you cannot predict in advance which kinds of results will need verification, then verification is always required.
The verification often ends up being just as expensive as doing the work in the first place, which means that if you want your usage of "AI" to be personally profitable, you have to find someone else to externalize the cost of verification onto. This person becomes your adversary, and, if you are successful, your "AI's" victim.
The Ladder-Climber And Their Reverse-Centaur Rungs
One way that this constellation of facts can straightforwardly assemble themselves into a dystopian nightmare is the phenomenon, described by Cory Doctorow, of the reverse centaur. This is when your employer non-consensually turns you into the verification system. The "AI" does the fun part of initially performing the work, and then you do the boring part where you check if the robot is right and clean up its messes, even if everyone already knows that it would, in aggregate, be cheaper for you to do the work in the first place.
Reverse centaurs can be made from any automation, not only "AI" automation. I think that there is a reason that this term happens to have emerged in the "age of AI", though, and not with earlier automation technologies (even those which were considerably more viscerally horrific). That reason is: the wrongness of "AI" output is not merely a technical feature that must be compensated for, it is a generalized externality.
As I mentioned above, if you are responsible for the entirety of the work, both extruding the "AI" output and checking it, it's usually cheaper to have humans do the entirety of the work to begin with. When humans do the writing directly, we can check as we go, and thus verification doesn't need to be as comprehensive.
When "AI" coding advocates say "code review is the bottleneck", what they are observing is that the LLM is still rolling the dice for each PR, and a human is still necessary to verify that each of those rolls is a winner. But calling this process "code review" is a bit of a misnomer; it's not really "code review" in the traditional sense, it's human understanding.
Before the advent of "AI", the human understanding was implicit in the process of writing the code in the first place1, and the code review was a way of diffusing and extending that understanding. Now that the code can be authored with no initial understanding taking place, that cost has not gone away, it has moved.
Human understanding was always the bottleneck.
However, this is taking a collaborative view of a software project, where satisfying the needs and solving the problems of your customers are the goals. We can see that "AI" is a bad tool to satisfy those goals, because all it's doing is converting the first half of the work, that of understanding the code as you write it, to understanding the agent's output as you read it.
What if, instead, we were to take the view that every software company is a Hobbesian nightmare, red in tooth and claw? In this view, the only goal of a software project is for the individual developers to make their promo cycles and get their bonuses. Given that there is only a certain amount of money to go around, this is a zero-sum game where each programmer wants to look more productive than their colleagues.
Pretty much every organization finds it easy to reward "productivity" as expressed by lines of code emitted, but the benefits of doing thorough and thoughtful design, analysis, and code review very difficult to reward. In this world, an LLM is an invaluable tool for the sociopathic ladder-climber, particularly if your legacy organization is still structuring their workflows as if the person prompting the bot is "writing" the code, and then they get to foist off the act of "reviewing" the code onto someone else.
Here, the prompter effectively externalizes the cost of the LLM's failures but internalizes any benefits. The prompter will vibe-code a big feature, so large that the assigned reviewer can't possibly comprehend it all effectively. When this happens, the reviewer will, eventually, be pressured to approve it, even if they can try to spot a few problems along the way. The reviewer has their own work to get back to, after all, the obligation to review the prompter's (read: the bot's) code is a drain on their time that they are not going to get rewarded for.
If this feature is a big success, the prompter gets a promotion. If it causes a big issue, well, the reviewer must not have been careful enough.
This is why LLMs are "good for coding", and also why their biggest promoters keep having outages.
The Generative Gish Galloper
Coding is the biggest "success story" of this type of adversarial communication, but it is by far not the only instance of such a thing. LLMs create a new form of leverage that can turn Brandolini's law from a linear advantage into an exponential one. If you are engaged in a political debate where you want to overwhelm the other side in nonsense, an LLM can generate bullshit faster than it is physically possible for a human being to type, let alone respond thoughtfully. There is an asymmetry to the utility of this weapon as well: only one side of the political spectrum wants to flood the zone and destroy trust in institutions and the concept of truth. There's a good reason that the fascists love it.
Straightforward Spam and Fraud
This is kind of obvious, but LLMs can generate lightly-customized, plausible-looking text much more quickly than any human being. This facilitates their use in fraud, spam, and scams. In a spamming or fraudulent interaction, once again, the costs are externalized onto the victim: the recipient of a spam message has to do all the work of "checking" the LLM's output. Spammers already expect very low hit rates from boilerplate, and if the LLM can increase those percentages from 1% to 5% the technology will pay for itself; they don't need anything like reliable accuracy.
Customer "Support"
If you have any kind of commercial relationship with a company, I probably don't even need to mention this: customer "support" bots are a misery. Everybody knows it at this point. But customer support is usually conceptualized by businesses as an adversarial interaction, because it is a cost center. They maintain internal metrics on time-to-resolution and try to optimize them. Implicitly, this creates a dynamic where the goal of the customer service agent's job is not to solve your problem, but to emit noise that will cause you to think your problem is resolved, or to give up, as fast as possible. Unsurprisingly, LLMs can emit this noise faster than humans can, getting those customers off the phone. But those customers will remember those interactions, and the story outside the TTR metrics is horrible.
Similarly to the situation in software development, LLMs can look very good on paper for customer support, but mostly what they are doing is illuminating the problems with the industry's existing metrics, by turning "winning the metrics battle against the customer" into a more obvious and immediate defeat for the company's long term reputation.
"Education"
In 2026 it is sadly a fact of life that students cheat all the time using "AI", and that this cheating is very successful, in that the teachers find it very hard to detect.
LLMs are great for cheating on schoolwork because the student is externalizing the work of the checking onto the teachers, who are often starting at a disadvantage to begin with, at least in the US.
My view is that this is happening because of a divergence in the way that students vs. teachers (or, more accurately, "the broader educational system") view grading.
When a student is asked to write an essay, the teachers see the effort as both intrinsically worthwhile for the student, as well as useful as a pedagogical tool to evaluate and react to the student's progress. The student, by contrast, sees a stumbling block designed to knock them off the path to success and into a permanent underclass. It is no wonder that the student sees "AI" as useful to their own goals and has no compunction about deploying it.
There is a bitter irony that the ability to understand the inherent value of actually writing the essay on their own is the sort of thing that students can really only learn by writing a bunch of essays. There's no way that I can think of which makes the benefit legible as long as a shortcut is available.
The net effect here is a downward spiral, where the already-wobbling educational system is sustaining an attack that it doesn't have the resources to recover from. The individual students' attacks against their teachers and their schools' grading systems might appear to momentarily succeed, but they will win the battle and lose the war.
Spamming "For Good"?
Usually when we talk about someone unilaterally choosing to enter into an adversarial relationship, that's an "attack" and for good reasons we have a negative impression of the attacker. However, I would be remiss if I did not point out that there are some cases where the relationship was already adversarial; just because you're the attacker doesn't mean that you are evil.
For example we might imagine use-cases like automatically filing appeals for prior authorizations against health insurance. It's relatively well-known at this point that the main way for-profit insurers maintain their margins is by denying claims right up to the line of the policies themselves being fraud, so using a spamming tool to fight them might be entirely justifiable2 in that case.
Similarly, using an LLM could be justified in a fight against a company refusing to honor a warranty. One could imagine using an LLM to immediately generate replies and escalations.
However, even in imagined cases like these, the underlying problem is that the insurers and the vendors already have a tremendous amount of structural power, so it is more likely that they will have the advantage in deploying a communications weapon like an LLM, as well as enacting policies to simply ignore any LLM-based communication that you might submit. Worse, if these strategies were to become widespread, they might provide an excuse to reject any communications by feeding them into an unreliable "LLM detector" and issuing an automated "computer says no" even to hand-written correspondence.
It is also worth stressing that these cases are imagined, as compared to the very real coworker-abuse, spam, scam, fraud, and disinformation campaigns being waged in real life today.
Therefore, while legitimate uses might exist, it's hard to imagine that there's anywhere they would be genuinely valuable and sustainable. In the best case "AI" will provide a temporary advantage for underdogs that will provoke an arms race which the resource-advantaged adversaries will win in the long run, in the worst case the arms race itself will cement permanent structural change that will make things worse.
"Search" By Stealing
Most of the adversarial utility of "AI" is on the "write" side, since write-amplification is more obviously aggressive than reading. But the "read" side of LLMs - summarization and question-answering - can be a form of attack as well.
To begin with, the act of reading itself is currently enormously destructive, but that's arguably not a fundamental aspect of this technology. They could set reasonable rate-limits and respect things like robots.txt, as search engines have for decades now. They could also refrain from committing criminal levels of copyright infringement. But, today, using "AI" tools does suborn this sort of out-of-control crawling.
More insidiously, consider the scenario described in this YouTube video. The LTT Bros decided to try Linux again, and in the course of so doing, they had problems. When trying to solve these problems, they were faced with a choice: they could consult Reddit, or they could ask an LLM. Asking an LLM would "gaslight the heck out of" them, but they still found it preferable, because they would at least get an answer without getting yelled at.
Initially this sounds great. But it also means that you want to extract knowledge from a community, while mechanically eliding any values or norms that the community may want to impart as part of offering that knowledge. As someone who spent many years in a community tech support role, this is worrying. Many requests for support are people asking how to do things that will momentarily solve a superficial problem but create a long-term reliability problem or even an immediate security risk, that the question-asker doesn't want to hear about. Consider the question "I'm tired of entering my password so much, how do I make it so my laptop unlocks automatically". An obsequious chatbot will helpfully tell you how to do this without pushback.
But, this is also a sort of ethically murky area. The Linux community is somewhat famously, for many years now, a toxic cesspool of general hostility, misogyny, etc. It is certainly a good thing that people can get access to this knowledge without subjecting themselves to abuse. But it also means that the people with the power and the privilege to change the community for the better can just quietly withdraw, rather than fixing the problems. It also means that the positive elements of culture cannot be transmitted, and people will have no opportunity to learn about unknown unknowns.
In this case, the "adversarial" communication is with society. The thing that using an LLM for search lets you do is withdraw from society and avoid forming any personal connections. There are some personal connections which are painful and annoying, and so that can feel like a momentary balm. But the need to make connections in general is, like, the concept of society itself.
Who Am I Hurting?
LLMs are good at adversarial communication. They are so good at it, relative to their other benefits, that they will tend to make communications adversarial if you are not remaining vigilant about the possibility that it might do so. My request to you, dear reader, if you are going to use such tools, is to always ask yourself, "who might I be hurting, if I use an LLM for this?"
If you're using an "AI", who is its adversary? If you haven't given it one yet, who might the "AI" turn into an adversary? Who might you overwhelm with an asymmetric amount of output, or, if you're receiving information and not sending it, who are you taking that information from without consulting?
Figure out the answers to these questions and conduct yourself accordingly; the answer might be "yourself".
Acknowledgments
Thank you to my patrons who are supporting my writing on this blog. If you like what you've read here and you'd like to read more of it, or you'd like to support my various open-source endeavors, you can support my work as a sponsor!
-
One of the reasons that software developers tend to prefer greenfield development is that when you are given a blank page, you can project your own specific understanding onto it. You can structure the codebase in a way that works for your brain, down to the variable naming conventions and the module layouts. LLM-assisted development makes everything into instant brownfield work, which makes developers instantly miserable; even those who are excited about the technology will frequently complain about how it feels like their agency has been stolen and their joy in the work has been diminished. But I digress. ↩
-
Modulo the massive amount of other externalities involved in using LLMs, of course, but I don't have the time or energy to get into those here. ↩
23 Jun 2026 8:06pm GMT
09 Jun 2026
Planet Twisted
Hynek Schlawack: How to Ditch Codecov for Python Projects
Codecov's unreliability breaking CI on my open source projects has been a constant source of frustration for me for years. I have found a way to enforce coverage over a whole GitHub Actions build matrix that doesn't rely on third-party services.
09 Jun 2026 12:00am GMT