Lessons from the AI Engineer World Fair 2026

Lessons from the AI Engineer World Fair 2026

Four days of talks at the AI Engineer’s World Fair 2026 gave me lots to think about (evidenced by the length of time it’s taken me to write this blog post) and changed quite a bit about how I think about and use AI. The bottom line is that AI is not removing the software-engineering job. It is forcing us to relearn where the difficult parts are.

This piece sits across two things I’m doing right now in the AI engineering space: writing code with coding agents, and building an agent – Safari Assistant. Some of what follows is squarely about the former, some about the latter, and a few apply to both.

The short version

  1. The software-engineering job is moving up a level, and we are learning the new job while doing it. AI agents can take over more implementation, but engineers still have to decide what matters, design the system around the model, gather evidence, and answer for what ships.
  2. Verification is becoming more valuable than generation. Human review is vulnerable to borrowed confidence, and the model that wrote the code should not be the only thing checking it. We need independent tools, different models, static analysis, and evals that continue against real production behaviour.
  3. More generated code creates a larger and stranger attack surface. Agents can invent package names, follow malicious instructions hidden in their input, and receive more access than they need. Security, correctness, and performance need separate checks.
  4. Cheap tokens do not make agentic work cheap. Falling prices have allowed for bigger tasks, more retries, more review loops. What matters more is the strategy.
  5. Faster implementation makes product judgement more important. None of these systems can tell us whether something deserves to exist. We still need to understand what users are trying to accomplish and question our skeuomorphic assumptions about what software should look like.
  6. Agents are bound by what we think to tell them, not by what actually matters — and by how many instructions they can hold before compliance breaks down in stranger ways than simply refusing.
  7. A symbolic layer – an ontology – can turn a business rule into something the system checks and enforces, rather than a request the model agrees with. Thin agents sitting on that shared substrate inherit trust instead of rebuilding it every time.

The long version

I don’t have a neat theory tying every talk together. What I can see in my own work is that using coding agents has changed where I spend my attention. I write less of some code myself, but spend more time explaining the goal, preserving context, reviewing evidence, and deciding whether the result is worth shipping. 

Here are some thoughts I’ve grouped together into themes, trying to run a thread, but somewhat failing.

The software-engineering job is moving up a level

Addy Osmani named that shift in his closing keynote – he estimated that 42% of committed code is now AI-generated. His argument was not that engineers are becoming unnecessary, it is that the job moves from producing every line to owning the verdict. We need to decide what is worth doing, gather evidence that the result is correct, understand the risk, and remain answerable when it is wrong.

He named three traps:

  1. Cognitive debt is losing your own understanding of a system by delegating too deeply. AKA vibe coding.
  2. Cognitive surrender is accepting borrowed confidence from a plausible AI output. Basically the AI equivalent of the LGTM code review with no accountability.
  3. Orchestration tax is the cost of managing more agents than your attention can support. Agents may run in parallel but your ability to understand and approve their work does not.

His phrase for the alternative was “high agency,” which he said is knowing when to delegate, when to inspect, when to stop, and when you are willing to put your name on the result. An agent can run a playbook, but it cannot inherit the consequences if (and when) failures occur.

Benoit Schillings (from Google DeepMind) offered a more optimistic version. He said that engineers once distrusted compiler-generated code – but now we rarely inspect it – and he expects AI-generated code to follow a similar trust curve. His conclusion was not that engineering disappears. He said if implementation becomes cheap, our attention shifts towards defining correctness, designing precise constraints, and auditing security boundaries. Basically the job moves up a level, just like the compiler did for assembler.

He also floated a radical idea – a coding language designed specifically for LLMs, mathematically precise rather than optimised for human readability (which would make sense if he’s right on his analogy with the compiler).

All of this boils down to that we are currently in a limbo – the old work has not disappeared, the new practices are not settled, and the tools change while we are still learning them. We are being asked to write less code ourselves without understanding less of the system. That is a more demanding job, not an easier one, made worse by the fact nobody has the answers yet.

Verification is becoming more valuable than generation

Tariq Shaukat’s (Sonar) keynote put a big number on cognitive surrender. Reviewers followed AI code-review advice 92.7% of the time when it was correct. They still followed it 79.8% of the time when it was wrong. Yikes! But, yes – we’ve all fallen prey to a plausible suggestion … and were humans any good at review anyway?

His answer was zero-trust, multilayered verification: zero-trust means the origin of the code does not entitle it to confidence, and the model that generated the code should not be its only judge. Multilayered means no single kind of review is expected to find every problem.

We need automated tests, static analysis, and runtime evidence even more than before – so that when the human does look at the PR, it’s already verified as working and acceptable code without obvious security or performance defects. That’s how we’ll ease bottlenecks at code review and testing.

Moving on from verifying code to verifying your agents in the wild; an Arize observability talk gave me another insight on verification. Previously I’d thought of evals as a one-and-done event, but their observation was that many agent failures do not throw an error because the run returns an answer and looks complete. Their team created Signal, a long-running reviewer agent that samples production traces, finds recurring failure patterns, groups related cases, and attaches the evidence. Their line was: “the best thing to watch an agent is another agent.”

The attack surface got bigger and stranger

Verification assumes the code is honest and just might be wrong, but what about code built to deceive the reviewer on purpose? Steve Yegge opened his agentic-security talk with two words: be scared. I don’t plan to turn that into a working methodology, but the examples justified taking the new failure modes seriously. His argument was that AI will increase the volume of code without holding the defect rate steady, saying “it got bigger, and it got stranger.”

Shaukat’s Sonar leaderboard backed up what Yegge was saying – on a benchmark of more than 4,000 coding tasks, the three models shown had substantially different rates of bugs, complexity, and security issues. Claude Opus 4.7 Thinking had the highest complexity and bug rate per million lines of code. Its security-issue rate was 291 per MLOC, compared with 214 for Gemini 3.1 Pro High and 68 for GPT-5.5 Medium. I don’t want to turn this vendor benchmark into a universal ranking – the point is that we aren’t imagining it, there are more bugs, and it’s not just because there’s more code. Knowing models’ strengths and weaknesses allows us to start to mitigate this.

Yegge also warned about hallucinated APIs, poisoned packages, and sleeper backdoors, and said they are more likely to slip through when code volume grows and review capacity does not. He gave the example of “slopsquatting,” where someone notices that coding agents frequently hallucinate a convincing package name. So they register the package with malicious code, and then wait for another agent to install it. The model invented its own bait. Yegge’s conclusion was that the build surface is now part of the attack surface, with no upstream source that deserves automatic trust.

His practical advice on all this was to separate review jobs. A model asked to check security, correctness, performance, and coding standards all at once will probably do each less well. Some of those checks should not be model judgements at all – if a test, linter, static-analysis tool, or CI rule can enforce something consistently, use it. Yegge also recommended giving agents real security tools such as Snyk and Chainguard instead of assuming they will reach for them by themselves.

Cheap tokens do not make agentic work cheap

The elephant in the room is tokenomics. I had been thinking of falling token prices as a straightforward cost reduction, so it was strange that AI costs kept increasing. But it’s obvious when you realise that reasoning models use tools, and tools generate output which becomes input. This is Jevons paradox in real-time – when a resource becomes cheaper, we often use more of it. Lower token prices let us attempt larger tasks, add more review steps, and run more retries. So the price of an individual token can fall while the cost of completing a task rises.

As OpenAI and Anthropic need to increase revenue to recoup costs, we need to understand our return on investment. How can we 10x ourselves and maximise results without just spending more?

Katelyn Lesse and Angela Jiang’s Tokens Should Have Jobs talk tested whether the strategy used to spend tokens matters more than the size of the budget. They worked with four strategies: plain Execute attempted the task directly; Advise consulted another agent; Grade checked an attempt against a rubric and retried; Dream recorded lessons for the next attempt.

On their complex financial task, Execute spent the fewest tokens in the open-budget comparison and performed worst, Dream was the best but spent the most tokens. They ran another experiment when the budget was held constant, and while Execute increased accuracy from 15% to 76%, Dream was still the best at 92% accuracy.

In these tests, Advise made sense when token efficiency mattered because it had the cheapest cost per perfect answer. Grade or Dream made sense when correctness justified the extra cost. 

There is no universally best strategy because the decision depends on what failure costs and how correct the result needs to be (financial tasks obviously need close to 100% accuracy, for example). We can’t just look at tokens per run – we need to look at token cost per successful run, and we can’t forget the human cost either.

Faster implementation makes product judgement more important

The Build the Right Thing workshop by Kent C. Dodds was a useful interruption to all the agent infrastructure and software engineering talks. And an important one, because if engineers can prototype almost anything in an afternoon, it becomes easier to build the wrong thing very efficiently.

He spoke about the various tools we can use to understand what the right thing is.

The Mom Test starts with evidence of past behaviour rather than asking whether someone likes an idea or might use it. What happened the last time they encountered the problem? What did they do instead? What did it cost, and who was involved? Conversations with users help us change the shape of the idea and what to build.

Jobs to Be Done then separates a request from the progress someone is trying to make. Functional jobs need shape, state, and workflow. Social needs affect visibility, permissions, and approval history. Emotional needs show up in preview, undo, and recovery.

The Kano Model helps decide how much engineering rigor a feature deserves – listing features along a scale of “must-have” basics to “delight,” and understanding the state of the product by plotting those features on a graph across not-implemented vs. fully-implemented and satisfied/dissatisfied.

What this boils down to is that product work isn’t just something that happens before engineering – it changes the system the engineer should build.

Theo Browne, founder of t3.gg, pushed the product question further. He described today’s AI tools as being in a “skeuomorphic phase,” like early iPhone apps that copied the appearance of the physical objects they replaced before software found its own visual language. We are still carrying habits, interfaces, team structures, and ideas from the world we already know.

His challenge was not to discard every old rule. It was to remember why the rule exists and check whether it still serves that purpose. Work that used to be too large for a side project may now fit inside a Markdown skill handed to an agent. His test was: “if it doesn’t feel stupid, it’s not big enough.”

But thinking bigger is not the same as generating more. The conference left me less interested in proving that agents can write code. They plainly can. The harder questions are what deserves to be built, what the agent needs to know, what it may touch, what evidence earns our trust, and who owns the consequences. That is the thread through all of it.

Context is the map

Knowing what to build is one kind of judgement a model can’t do for you. Knowing what it needs to know to build it is another. Thariq Shihipar, Anthropic, described the gap between the map in our heads and the territory of the actual codebase. A model is constrained by “the harness we put them in, and the way we prompt them,” but the person writing the prompt is constrained by what they know to mention. He showed us his unknowns matrix:

  • Known knowns — what you know you want.
  • Known unknowns — what you know you haven’t worked out.
  • Unknown knowns — what feels too obvious to mention.
  • Unknown unknowns — what you haven’t considered.

He shared a couple of helpful prompts, such as the “blindspot pass,” which asks an agent to inspect unfamiliar code before touching it and helps surface the questions we do not know to ask. And the “interview” prompt, which has the agent interview you one question at a time, prioritising answers that could change the architecture. Both are ways of finding missing context before it becomes a bad implementation.

Shihipar spoke about how things have changed with working with Fable, and told us that Anthropic had removed roughly 80% of Claude’s system prompt. Tests have shown that replacing a large prompt and many examples with a smaller prompt and tool search perform better with newer models. That is not evidence that shorter prompts always win – it’s that endlessly accumulating instructions is not a durable context strategy.

Conversely, Laurie Voss’s IFScale instruction-following benchmark was fascinating. The older reference point was that models could handle roughly 200 instructions. His tests showed that newer models maintained compliance across thousands, with some tests extending to around 10,000 instructions. But the failures became stranger – Anthropic models refused the test as dangerous, Gemini models used its whole thinking budget and returned nothing, and OpenAI models began the task then decided it was pointless so stopped.

But remember, just because they can handle more instructions, less is always more.

Ontology was the secret buzz

Everyone thought the conference theme was loops, but I’d like to disagree and say the secret buzz was ontology.

Frank Coyle, retired professor from Berkeley, argued for combining probabilistic models with a symbolic layer that can enforce business rules. He presented RDFS as a way to infer facts and OWL as a way to express hard constraints. In his example, turning “an order can be refunded at most once” into a machine-readable rule gives the system something it can check before allowing another refund. So the agent proposes and the ontology permits.

Emil Eifrem, from Neo4j, gave that idea a much more concrete shape. His starting point was that most of what looks like an “agent problem” – finding the right data, trusting it, avoiding duplicated logic across agents, learning from past runs – is actually a data-layer problem wearing an agent costume. Every agent builder ends up solving the same four things from scratch: discovery (finding the data sources at all), trust (is it current, am I allowed to access it, is it versioned), DRY (update one thing, have to update everywhere), and learning (no shared learning across agents, no self-learning over time).

His fix was to make agents themselves thinner – less custom logic duplicated per agent – by pushing that shared complexity down into a smarter substrate underneath them, built from three layers: a business ontology (a plain-language map of real-world concepts anyone can read), a technical ontology (where the data actually lives, its schemas, mapped back to the business ontology), and execution traces (a running record of what agents actually did – decisions, paths, outcomes, errors – so the system can learn instead of every agent starting cold). 

Markdown skills alone can’t hold this, he argued; they’re part of the answer but not the substrate itself. The payoff is that a new agent doesn’t have to rediscover every data source and access rule from scratch – it inherits trust, structure, and shared learning from the layer underneath it.

Between Coyle and Eifrem, the same idea kept surfacing: agents propose, and something more durable than a prompt decides what’s allowed. Watch this space. In the meantime, here’s what’s already changing in how I work.

What I’m changing about how I work

I wish I had come home with a neat new software-engineering playbook. The less comfortable truth is that we are all relearning the job while trying to do it. The models change, their failure modes change, and practices that seemed sensible six months ago can become unnecessary or actively unhelpful. These are the parts of the new job I am working on now.

Finding a better home for shared context. I have a personal brain and vault that preserve useful context across my own work, but that does not solve the team problem, because decisions and discoveries made inside a thread on a developer’s laptop are easily lost when it closes. 

To solve that problem at the moment coding agents default to adding more and more to READMEs, and developers keep adding more to AGENTS.md to get agents to behave. And as a result these files grow with every change until the useful instructions disappear inside them. So our team made a clean sweep last month and stripped our AGENTS.md back so it no longer repeats the README. An instruction has to earn its place – it must address a real mistake, be difficult to discover elsewhere, be specific enough to change behaviour, and prevent meaningful harm.

Moving verification earlier.  We have just started working with SonarQube and giving coding agents access to automated analysis before a pull request reaches GitHub, which is making our pull requests cleaner. We don’t have to rely on AI vibes for static checks. This is important because studies have shown that agents used 7–8% fewer tokens and revisited files 34% less often on clean codebases – we can’t just vibe code a mess and assume that LLMs will be able to work it out.

Making human review easier. I’m also changing the human side of review. My pull requests now include a quick guide explaining what changed, where to begin, and what deserves particular attention. This is something I stole from Linear. Our team is also focusing on using smaller, stacked GitHub pull requests where one large review would otherwise become unmanageable. We aren’t planning on removing the human reviewer yet, but we’re trying to make their job as easy as possible and give them a realistic chance of making a judgement instead of rubber-stamping a wall of generated code.

Measuring the cost of the result. We are actively trying to understand where AI is delivering a return. My clever boss Ben built a Codex hook that adds costings to our pull-request descriptions, so we can start comparing what the work cost with what it produced.

Using Codex more effectively. I learnt a lot in the various Codex workshops and talks – I already use Codex goals, and I want to use them more consistently when there is a real outcome to work towards. Side Chats let me investigate something without derailing the main task. Separate threads are useful for exploration and subtasks, with context shared only where it genuinely helps. 

Moving from Markdown to HTML. Thariq Shihipar’s talk changed a smaller but surprisingly useful part of my workflow too. His take was “Markdown for itself, Markdown for you, HTML for decisions.” I still use Markdown as model input but ask for HTML when the output is something I need to understand or make a decision from. Models have become very good at generating readable HTML, which means they can build a temporary dashboard, report, or comparison around the question instead of squeezing everything into a Markdown table.

Closing the loop with real users. Faster implementation does not help if I am building the wrong thing. I want to spend more attention on what users actually do with what we ship, rather than only what they asked us to build. I am already using PostHog and my own reporting pipeline to surface patterns in product behaviour. 

Improving my evals. I am testing Arize’s product for observability and their Signal agent to monitor my Safari Assistant. This allows me to evaluate real production behaviour to find repeated failures and attach the traces I need to understand the problems.

None of these changes feels finished. That may be the most honest conclusion I can offer. We are not swapping an old set of tools for a settled new set. We are rebuilding the practice around systems that change underneath us, while still being responsible for what reaches users.

A few other talks worth a watch on the live stream that didn’t make it into this write-up: Synx, OpenClaw — Peter Steinberger, and Kyle — Human Layer, “Loops”.

Adventures with UTF-8 encoding in a MySQL database

I had some technical difficulties today and I wanted to thank 2019 Dagmar for helping out 2024 Dagmar. This is a repost (and update) of an answer I added to Quora back then.

Getting MySQL to play nicely with UTF-8 can be a challenge because there are so many places that you have to make sure the encoding is correct. You need to look at the following:

  • The encoding of the database
  • The encoding of the table
  • The encoding of the column
  • The encoding of the database connection

Then you also need to consider if the system can display the characters, so does the OS or browser have the correct fonts installed?

Whew. Let’s jump in.

Okay, encodings, but what is a collation?

Let’s use an analogy to explain the difference:

  • character set is the alphabet of available letters and symbols.
  • collation is the set of rules for how to sort and compare those letters.

A character set is the defined list of characters that the database knows – each character is assigned a unique number. Examples of character sets are latin1, utf8mb3 (also known as utf8) and utf8mb4. The difference between utf8mb3 and utf8mb4 is that the latter stores up to 4 bytes per character which allows it to store every character in the Unicode standard.

A collation works with a character set (it cannot exist in isolation) and it answers questions like “is A the same as a” and “is café the same as cafe”, and “does a come before Z”. Examples of collations are latin1_swedish_ci, utf8mb4_unicode_ci, or utf8mb4_0900_ai_ci.

Here’s a quick reference of the defaults used in MySQL:

MySQL VersionDefault Character SetDefault Collation
MySQL 5.7latin1latin1_swedish_ci
MySQL 8.0utf8mb4utf8mb4_0900_ai_ci

Note: when I first wrote this article, the best practice was utf8mb4_unicode_ci but now it is utf8mb4_0900_ai_ci.

Here’s a breakdown of the key differences:

Featureutf8mb4_0900_ai_ciutf8mb4_unicode_ci
MySQL VersionRecommended for MySQL 8.0+legacy, used in MySQL 5.7 and earlier
Unicode StandardBased on Unicode 9.0.0 (more modern)Based on Unicode 4.0.0 (older)
Accent SensitivityAccent-Insensitive (e.g., cafe = café)Accent-Sensitive (e.g., cafe ≠ café)
Case SensitivityCase-Insensitive (e.g., A = a)Case-Insensitive (e.g., A = a)
PerformanceGenerally faster and more optimised.Slower than the new collations.

Before we start, let’s check your current setup

Before you start changing things, it’s a good idea to see how your MySQL is currently configured. To do this, use this command:

mysql> SHOW VARIABLES WHERE variable_name LIKE 'character_set_%' OR variable_name LIKE 'collation%';

This will give you the character set and collation defaults for your database – what is used when they are not specified. The variables are grouped into four main categories: server defaults, database defaults, connection settings, and system internals.

1. Server defaults:

VariableWhat it controlsNormal setting (MySQL 8.0+)Ideal setting (MySQL 5.7)
character_set_serverThe default character set for any new database.utf8mb4utf8mb4
collation_serverThe default collation for any new database.utf8mb4_0900_ai_ciutf8mb4_unicode_ci

2. Database defaults:

VariableWhat it controlsNormal setting (MySQL 8.0+)Ideal setting (MySQL 5.7)
character_set_databaseThe default for any new table in the current database.utf8mb4utf8mb4
collation_databaseThe default collation for any new table.utf8mb4_0900_ai_ciutf8mb4_unicode_ci

3. Connection defaults:

VariableWhat it controlsNormal setting (MySQL 8.0+)Ideal setting (MySQL 5.7)
character_set_clientThe encoding of SQL queries you send to the server.utf8mb4utf8mb4
character_set_resultsThe encoding of results the server sends back to you.utf8mb4utf8mb4
character_set_connectionThe encoding the server uses for calculations/comparisons.utf8mb4utf8mb4

4. System internals, generally should not be modified:

VariableWhat it controlsNormal setting (MySQL 8.0+)Ideal setting (MySQL 5.7)
character_set_systemUsed for MySQL’s internal metadata.utf8mb3utf8
character_set_filesystemUsed for filenames (e.g., LOAD DATA INFILE).binarybinary

Generally, the out of the box settings for MySQL 8.0+ should be perfect and don’t require you to make any changes, but if you have a MySQL 5.7 server then you will need to modify these in order to get UTF-8 support.

Here is the ideal my.cnf configuration file for MySQL 5.7:

# This section applies to the standard MySQL command-line clients
[client]
default-character-set=utf8mb4
 
# This section applies specifically to the interactive MySQL command-line client
[mysql]
default-character-set=utf8mb4

# This section configures the MySQL Server (daemon) itself
[mysqld]
collation_server=utf8mb4_unicode_ci
character_set_server=utf8mb4

Note – if you change the above config then you must restart the MySQL server before they will be applied.

It is possible to modify the variables using your SQL client, which applies new variables from your next session until the server restarts. Use this syntax:

mysql> SET GLOBAL variable_name = 'value';

If you want to apply the variables just for your session (so when you logout the value you set will be reverted), remove the GLOBAL keyword, like so:

mysql> SET variable_name = 'value';

Now let’s get into more details

But important – use the right collation!

Please make sure that you use the right collation – the examples below use utf8mb4_unicode_ci but if you are using MySQL 8.0+ then you should probably be using utf8mb4_0900_ai_ci

1st check – encoding of the database

Firstly, you need a database or table that has been created with the correct encoding.

This is how to create a new database and specify the encoding:

mysql> CREATE DATABASE mydb CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 

If you already have a database, you will need to check the encoding of your database by running this command:

mysql> show create database mydb; 

Which should return output that looks like:

+--------------+-----------------------------------------------------------------------------------------------+ 
| Database     | Create Database                                                                               | 
+--------------+-----------------------------------------------------------------------------------------------+ 
| mydb         | CREATE DATABASE `mydb` /*!40100 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci */ | 
+--------------+-----------------------------------------------------------------------------------------------+ 
1 row in set (0,01 sec) 

If you see the correct encoding, you database has created correctly and you now need to check the encoding of the tables and columns that contain characters.

2nd check – encoding of the table and columns

By default the tables and columns take on the encoding from the database and table definition, but because it’s possible to override them, you must check all.

This is how you can check the encoding for a table and columns:

mysql> show create table users; 

Which should return output that looks like:

+--------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ 
| Table  | Create Table                                                                                                                                                                                                                                                                                                                                                                                                                                               | 
+--------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ 
| groups | CREATE TABLE `users` ( 
  `id` int(11) NOT NULL AUTO_INCREMENT, 
  `name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, 
  `email` text COLLATE utf8mb4_unicode_ci, 
  `created_at` datetime NOT NULL, 
  `updated_at` datetime NOT NULL, 
  PRIMARY KEY (`id`) 
) ENGINE=InnoDB AUTO_INCREMENT=55 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci | 
+--------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ 
1 row in set (0,00 sec) 

It is OK if you only see an encoding set on the table and not on the individual columns, but if the encoding for the column you are inserting UTF8 data into is anything but utf8mb4_unicode_ci then you need to update your database.

How to update an existing database

If you find that your database, tables or columns have the wrong encoding, you need to update them, one by one.

Fixing the database:

ALTER DATABASE mydb CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 

Fixing a table:

ALTER TABLE users CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 

Fixing a column:

ALTER TABLE users MODIFY username varchar(255) CHARACTER SET  utf8mb4 COLLATE utf8mb4_unicode_ci; 

There are handy scripts that you can use to upgrade your database & tables, which I found on this stackoverflow article.

http://stackoverflow.com/questions/6115612/how-to-convert-an-entire-mysql-database-characterset-and-collation-to-utf-8

Here is my version:

#!/bin/bash 
set -e 
printf "### Convert MySQL db encoding to utf8mb4/utf8mb4_unicode_ci ###\n\n" 
# Get the MySQL username 
printf "Enter mysql username: " 
read -r USERNAME 
# Get the MySQL password 
printf "Enter mysql password for user %s: " "$USERNAME" 
read -rs PASSWORD 
printf "\n" 
# Get the MySQL database 
printf "Enter mysql database name: " "$DB" 
read -r DB 
( 
  echo 'ALTER DATABASE `'"$DB"'` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;' 
  mysql "$DB" -u"$USERNAME" -p"$PASSWORD" -e "SHOW TABLES" --batch --skip-column-names \ 
  | xargs -I{} echo 'SET foreign_key_checks = 0; ALTER TABLE `'{}'` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;' 
) \ 
| mysql "$DB" -u"$USERNAME" -p"$PASSWORD" \ 
&& echo "$DB database successfully updated ..." 
exit 

3rd check – encoding of the database connection

The next thing that can go wrong is that your connection to the database is using the incorrect encoding.

If you are connecting with a Java application, you may need to update your JDBC connection string to something like this:

jdbc:mysql://localhost:3306/mydb?useUnicode=true&characterEncoding=utf8mb4 

If you are connecting via another client (e.g. MySQL Workbench) or application (e.g. your Python script), then you need to check that the encoding is not being set incorrectly by that client or tool.

4th check – the fonts

Whew! If you’re still with me, there’s one last gotcha – and that’s to do with fonts.

Fonts are used to display characters (letters, numbers, symbols and emojis). Each font supports a specific range. Operating Systems come installed with a set of fonts which may not include newer UTF-8 characters. Characters are being added to UTF-8. When a character is encoded in UTF-8 but the font does not support it you may see a question mark or a box. You may also see an encoding like `\U+1F90C`.

You may also find that some UTF-8 characters are displayed in one application but not another, which could be down to different fonts being used. For example 🤌 [pinching hand] displays in my browser but not in my terminal window on my Mac.

You need to ensure that the correct font is installed in your browser, application and on your computer or server.

Testing

OK!! Once you have done the above steps and verified that your database, table and column has the correct encoding and your connection supports UTF-8, you can now try to insert data.

If you would like to try an easy unicode character, we could use this:

mysql> update users set name = 'Unicode is cool д' where id=1; 

To verify the insert happened correctly:

mysql> select name from users where id=1; 
+-----------------------+ 
| name                  | 
+-----------------------+ 
| Unicode is cool д   | 
+-----------------------+ 
1 row in set (0,07 sec) 

Bonus! UTF-8 in MySQL Docker containers

This was a new world of pain for me today.

I was running MySQL 8 in a Docker container and was struggling to get UTF-8 characters to work in the MySQL command line.

The documentation for the standard mysql:8 docker image says you can set the character set and collation when you create the Docker container, like so:

$ docker run --name some-mysql -e MYSQL_ROOT_PASSWORD=my-secret-pw -d mysql:tag --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci

This didn’t solve the problem, and the character_set_client was still latin1 when I checked the MySQL variables for character set and encoding.

So I tried creating a new Docker container and setting a my.cnf configuration file when(as mentioned above)

$ docker run --name some-mysql -v /my/custom:/etc/mysql/conf.d -e MYSQL_ROOT_PASSWORD=my-secret-pw -d mysql:tag

However, I still could not copy and paste UTF-8 characters into the command line:

mysql> INSERT INTO your_table (name) VALUES ('🤌');

Becomes

mysql> INSERT INTO your_table (name) VALUES ('');

I asked a question on Stackoverflow, but in the end I answered it myself.

The problem was two fold:

  1. The OS image used for the MySQL Docker containers does not have UTF-8 fonts installed
  2. The Docker container does not have the UTF-8 locale set

I created a custom MySQL 8 image using this Dockerfile, which used a Debian build to copy the systems fonts:

# Stage 1: Use Debian to install locales and fonts
FROM debian:latest as build

# Install locales and fonts
RUN apt-get update && \
    apt-get install -y locales fonts-dejavu-core && \
    locale-gen C.UTF-8 || true && \
    dpkg-reconfigure --frontend=noninteractive locales && \
    update-locale LANG=C.UTF-8

# Stage 2: Use official MySQL image as the base image
FROM mysql:8

# Set environment variables for UTF-8 locale
ENV LANG=C.UTF-8
ENV LC_ALL=C.UTF-8

# Copy locales and fonts from the build stage
COPY --from=build /usr/share/fonts /usr/share/fonts
COPY --from=build /usr/lib/locale /usr/lib/locale
COPY --from=build /usr/bin/locale /usr/bin/locale
COPY --from=build /usr/sbin/locale-gen /usr/sbin/locale-gen
COPY --from=build /etc/locale.gen /etc/locale.gen
COPY --from=build /usr/bin/localedef /usr/bin/localedef

# Expose MySQL port
EXPOSE 3306

# Set default command
CMD ["mysqld"]

I built the custom Docker image with this command:

docker build -t custom-mysql:8 .

I created a new Docker container using this image:

docker run --name some-mysql -e MYSQL_ROOT_PASSWORD=root -p 9306:3306 -d custom-mysql:8

When I logged into the MySQL command line tool, I was finally able to copy and paste UTF-8 characters:

% docker exec -it some-mysql mysql -u root -proot
mysql: [Warning] Using a password on the command line interface can be insecure.
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 8
Server version: 8.0.33 MySQL Community Server - GPL

Copyright (c) 2000, 2023, Oracle and/or its affiliates.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql> INSERT INTO your_table (name) VALUES ('🤌');

I hope my waffling can help someone else, but at least I can be pretty sure that future me will be grateful.

Prototype using the web APIs for speech

This is a test that will demonstrate how the standard web APIs can be used to recognise voice from audio input and to read out text.

Speech Recognition API test

Speech Synthesis API test

Read more here:

  1. SpeechSynthesis API documentation
  2. SpeechRecognition API documentation
  3. Can I use: SpeechRecognition API

Code

This code was generated for me by ChatGPT. Ironically I believe the SpeechSynthesis code is actually my own StackOverflow code from when I was creating my Google Docs Add-on Ready Steady Spell.

<!DOCTYPE html>
<html>
<head>
  <title>Speech Recognition and Synthesis</title>
  <style>
    button {
      font-size: 18px;
    }
    button:disabled {
      opacity: 0.5;
      cursor: not-allowed;
    }
  </style>
</head>
<body>
  <h1>Prototype using the web APIs for speech</h1>
  <p>This is a test that will demonstrate how the standard webkit APIs can be used to recognise voice from audio input,
 and to read out text.</p>
  <ul>
    <li><a href="https://developer.mozilla.org/en-US/docs/Web/API/SpeechSynthesis">SpeechSynthesis API documentation</a></li>
    <li><a href="https://developer.mozilla.org/en-US/docs/Web/API/SpeechRecognition">SpeechRecognition API documentation </a></li>
    <li><a href="https://caniuse.com/speech-recognition">Can I use: SpeechRecognition API</a></li>
  </ul>

  <h2>Speech Recognition API test</h2>
  <button id="startButton">Start Recording</button>
  <button id="stopButton" disabled>Stop Recording</button>
  <div id="result" style="margin-top: 20px;font-size: 18px;"></div>

  <h2>Speech Synthesis API test</h2>
  <form id="textToSpeechForm">
    <label for="textToSpeechInput">Text:</label>
    <textarea id="textToSpeechInput"></textarea>
    <label for="voiceSelect">Voice:</label>
    <select id="voiceSelect"></select>
    <button type="submit">Speak</button>
  </form>

  <script>
    // Check browser support for SpeechRecognition and SpeechSynthesis APIs
    if ('SpeechRecognition' in window || 'webkitSpeechRecognition' in window) {
      const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
      const recognition = new SpeechRecognition();
      const synthesis = window.speechSynthesis;
      const resultDiv = document.getElementById('result');
      const textToSpeechForm = document.getElementById('textToSpeechForm');
      const textToSpeechInput = document.getElementById('textToSpeechInput');
      const voiceSelect = document.getElementById('voiceSelect');
      let finalTranscript = '';

      recognition.continuous = true;

      // Event handler for when speech is recognized
      recognition.onresult = function(event) {
        let interimTranscript = '';
        for (let i = event.resultIndex; i < event.results.length; i++) {
          const transcript = event.results[i][0].transcript;
          if (event.results[i].isFinal) {
            finalTranscript += transcript;
          } else {
            interimTranscript += transcript;
          }
        }
        resultDiv.innerHTML = `<strong>Recognised text:</strong> ${finalTranscript + interimTranscript}`;
      };

      // Event handler for button clicks
      document.getElementById('startButton').onclick = function() {
        recognition.start();
        document.getElementById('startButton').disabled = true;
        document.getElementById('stopButton').disabled = false;
      };

      document.getElementById('stopButton').onclick = function() {
        recognition.stop();
        document.getElementById('startButton').disabled = false;
        document.getElementById('stopButton').disabled = true;
      };

      // Event handler for text-to-speech form submission
      textToSpeechForm.onsubmit = function(event) {
        event.preventDefault();
        const selectedVoice = voiceSelect.value;
        const text = textToSpeechInput.value;
        speak(text, selectedVoice);
      };

      // Function to convert text to speech
      function speak(text, voice) {
        const utterance = new SpeechSynthesisUtterance(text);
        if (voice) {
          const voices = synthesis.getVoices();
          const selectedVoice = voices.find(v => v.name === voice);
          utterance.voice = selectedVoice;
        }
        synthesis.speak(utterance);
      }

      // on load 
      window.onload = function() {
         populateVoiceList();
      };

      // Fetch available voices when the list is updated
      synthesis.onvoiceschanged = function() {
          populateVoiceList();
      };

      function populateVoiceList() {
        const voices = synthesis.getVoices();
        for (let i = 0; i < voices.length; i++) {
          const option = document.createElement('option');
          option.textContent = voices[i].name;
          voiceSelect.appendChild(option);
        }
      }

    } else {
      // Display an error message if the APIs are not supported
      resultDiv.innerText = 'Speech Recognition and/or Speech Synthesis APIs are not supported in this browser.';
    }
  </script>
</body>
</html>

LLM Prompt Injection attacks

Hacking ChatGPT to bypass its guardrails is a sport like hunting for easter eggs was in the 90s; it captured the interest of people who wouldn’t usually consider themselves hackers.

As more people look to integrate OpenAI APIs into their software, we need to consider prompt injection attacks. Especially where AI is used to generate code (like database queries). A hacker could, for example, give the system a prompt that tells the LLM to ignore any other instructions it has been given and give it a list of logins and unencrypted passwords.

This article from DZone gives an indepth overview on prompt hacking, how to do it and how to mitigate your risk. It’s important to understand both to write secure code.

Here’s the list of defensive mechanisms:
👉 filtering: check for naughty words or phrases in both the user input and the LLM output.
👉 instruction defence: add guardrails into your prompt to instruct the LLM to exercise caution with its output.
👉 delimiters: use random strings to encapsulate user input and keep it clearly separated from the instructions.
👉 XML delimiters: embedding user input in XML tags seems to be very effective
👉 post-prompting: place the user’s input ahead of the instruction part of the prompt.
👉 sandwich prompting: similar to post-prompting, you add a recap of the original instructions after adding the user input in the prompt. This keeps the LLM focused even if it received conflicting commands in the user’s input.
👉 LLM evaluation: This can be of the user input where you use another LLM to evaluate the user input to identify if the contains conflicting or dangerous instructions. Or of the response where you use another LLM (usually a higher level LLM) to check if the response is considered safe. Both of these require that you develop a rubric to assess and evaluate according to a set of criteria or guidelines.

When developing, it is recommended that you keep systems that have unfettered access to the APIs and other 3rd party content separate from systems that have access to sensitive information or can perform destructive operations. The article briefly touches on Dual LLM systems where one is quarantined and the other is privileged.

However, none of these ideas or concepts truly solve the problem of prompt injection attacks – yet. Beware and go with caution.