Jun 24, 2024 - Easy-wins for generative AI

I will start this post with a direct question:

What are the most useful generative AI-powered product features you regularly use?

Think about it for a while. The answer usually includes [1]:

  1. Chat apps (e.g., ChatGPT, Google Gemini)
  2. Media generation tools (e.g., audio, image, video)
  3. Code generation extensions (e.g., GitHub Copilot, Code Whisperer)

These are indeed very useful! But weren’t we promised that generative AI would completely change every aspect of our lives? So why can’t most of us, specially non technical people, think of more examples to answer this question? The logical answer is that they aren’t that prevalent, yet.

To add more context, it’s not that no one is trying. We’ve all seen prominent companies investing significant resources to ride the generative AI bandwagon. However, many of them decided to chase home runs instead of easy-wins, and for that reason a lot of them failed [2].

So in this post, I’ll argue that to successfully adopt generative AI in your product, you should focus on easy wins rather than home runs.

But what is an Easy-win?

An easy win, also known as a “low-hanging fruit,” can be defined as any product feature that delivers significant value to the end user while having relatively low implementation complexity:

Prioritization matrix

This concept originates from the prioritization matrix, a tool that helps teams manage tasks more efficiently, reduce viability risks, and optimize delivered value.

It may seem obvious to target easy wins, but more often that not we get carried away chasing shiny objects and commit to poorly assessed challenges.

How to discover gen AI easy-wins?

Considering that you may already have a working product that customers use (and love!), you should look for opportunities to improve your product’s existing user flows to make them easier, faster, and more enjoyable.

If we go back to the start of this post we can see that the gen AI apps that we use more often are those that save us time, make us more productive. Focus on that, make your customers more productive. That’s valuable.

One way I like to think of generative AI, contrary to the belief that it’s a superintelligent being, is to consider it as a very competent intern — one that can receive generic instructions and carry out low cognitive complexity tasks with reasonable precision. So, in the context of easy wins, avoid assigning your competent intern tasks that are too broad in scope, overly complex, or demand extreme precision. Those aren’t easy wins.

Additionally, anticipate and prepare for imprecise results from generative AI. If you start with a zero-tolerance policy for errors, you’re likely to fail. Instead, design your solutions to support inexpensive review and regeneration cycles. This allows end users to easily assess the quality of the output and either accept it or quickly regenerate it if necessary.

Wrapping It Up:

  1. Target existing user flows
  2. Search for opportunities to make your users more productive
  3. Adopt gen AI for low-cognitive, narrowly scoped tasks
  4. Design for inexpensive review and regeneration cycles

A concrete example

I believe it’s important to provide a real world example to illustrate how these principles can be applied in practice. At MindMiners, we offer innovative web and mobile market research solutions. Our customers create surveys on our platform and collect responses from our proprietary respondents panel via a mobile app.

One of the most time-consuming user flows on our platform is the questionnaire creation process, so we decided to focus on improving it. A common low cognitive complexity task for survey creators is listing options for multiple-choice questions based on the question text. To streamline this, we added a button for generating options.

Here’s how it looks - And of course, we took the opportunity to add some marketing flair to the icon and label there 😉:

Question without options

Upon clicking this button, the platform constructs a simple prompt using the question text and requests a suggestion of options from the generative AI REST API. Once a result is generated, it is inserted into the frontend:

Question with AI generated options

If the end user is not satisfied with the results, they can easily and quickly regenerate the question options and even provide additional instructions, such as specifying the number of options.

Closing thoughts

In this post, I outline a strategy for identifying opportunities to leverage your existing product with generative AI. Instead of pursuing overly ambitious and sophisticated features, I advocate for starting with simpler yet highly valuable improvements. I present a concrete example of a technically straightforward feature that we have developed and that was very well received by our users, demonstrating the effectiveness of this approach.


Reference

[1] Ask HN: What are some of the best user experiences with AI?

[2] Every app that adds AI looks like this

Oct 16, 2023 - Guidelines for addressing requests from colleagues

In the workplace we are constantly receiving requests from colleagues, and the more senior we get, the higher the demand for us. That’s natural and expected as we progress in our carrers and take on leadership roles.

The way we address these requests has a direct impact on how we are perceived within the company and in the collaborative work environment we want to foster. I see them as opportunities to exercise communication and leadership skills, share knowledge, give feedback and positively impact my team and the business.

Let’s take a look at how we can approach requests effectively, how seniority plays a role and analyze a few illustrative examples.

Guidelines

In a nutshell, our communication in these situations should be driven by the following guidelines:

  • Employ empathy and nonviolent communication
  • Promote a cooperative, trusting, and supportive environment
  • Empower colleagues’ autonomy
  • Advance colleagues’ technical skills
  • Focus on results

Before responding to a request, reflect on whether your communication meets most of the above criteria, and whether it doesn’t directly go against any one of them. If your communication is aligned with this, proceed with confidence.

Notice I deliberately highlighted ‘autonomy’, since I believe it plays a central role. In this context, it means that we should encourage our colleagues to seek solutions to their requests as autonomously as possible. It’s like the old saying: ‘Give a man a fish, and you feed him for a day; teach a man to fish, and you feed him for a lifetime’ (even though sometimes we’ll have no option other than give the fish - due to short deadlines, lack of experience of the courterpat, etc).

Seniority

Before heading to examples, a quick note on how the seniority level from our counterpart affects the way we communicate.

Junior-level colleagues require more care and attention in communication, as they are in an early stage of professional development and need the consideration and support of more senior colleagues to help them grow. Positive and constructive feedback should be given frequently, and we should push them out of their comfort zone, but in a healthy and considerate way.

In senior-to-senior relationships I encourage some friction. We need to have the courage to be honest with each other without beating around the bush, and trust in the maturity of our colleagues to receive feedback that is less polished, but constructive and insightful. This can speed things up and foster a more dynamic environment.

Examples

To illustrate below are examples of typical reponses we often give. The first sentence in red shows an inadequate response according to the guidelines. The following sentence in green shows a more appropriate response. It is important to note that there isn’t only one way to act in each situation, and depending on your background and leadership style, your communication will vary. Additionally, keep in mind that we’re examining scenarios in which junior professionals seek assistance from their senior counterparts.

A)

“Hi Alice, I don't have time to help you, I'm too busy.”

This is a cold response, if you reply like this a lot then you’re not making yourself available to your colleagues, creating the perception that the door to collaboration is closed.

“Hi Alice, unfortunately I'm very short on time today due to a priority activity I need to finish by the day's end. Is this urgent, or can we come back to this topic tomorrow? You can also check if Bill is available. Please keep me informed either way.”

This response shows adequate empathy, as it not only provides insight into your own situation but also expresses genuine interest in understanding the urgency behind the request. It further demonstrates cooperation by suggesting a schedule for assistance, possibly the next day. It also indicates alternative ways, such as exploring the availability of another colleague, possibly familiar with the subject, who could provide assistance.

B)

“Bob, you should be able to do this by yourself. Try harder”

This can be characterized as a harsh response, especially for people at a more junior level, as, it potentially brings an unfavorable judgment of the person’s ability, and closes the door to collaboration.

“Bob, kindly share what you've attempted so far, along with any unsuccessful attempts. This will help me grasp the context and offer more tailored guidance.”

This response aims to comprehend prior efforts through clear and well-crafted communication, naturally fostering the culture of autonomy valued within the company. It conveys the expectation that individuals have made an effort to address the issue independently before seeking assistance.

C)

“Hi Alice, look, I've explained this to you several times, do some research.”

Again, an example of a harsh response. Even though a subject may have been already discussed in the past, it’s essential to maintain an environment of trust and companionship by employing a more thoughtful communication.

“Hi Alice, I recall encountering this issue on multiple occasions in the past, and I believe that by revisiting our previous solutions, we can resolve it effectively. Please consider retrieving the approach we took when dealing with a similar situation, such as 'XYZ'. If you have any questions, feel free to reach out. I'm here to assist.”

Here, we assume an honest oversight without passing judgment, recognizing that we can sometimes struggle to recall past situations and apply them to new ones. With time and guidance, we can enhance this skill and progress. This presents a valuable opportunity to support a colleague’s technical development and foster their autonomy.

D)

“Bob, don't worry, I'll take a look and solve this problem by myself, I think it will be faster. I'll let you know when I'm done.”

At first glance, this response appears cooperative and results-oriented. However, it contradicts the autonomy guideline by depriving the individual who sought help of the opportunity to learn and grow from the challenge. This approach can inadvertently foster dependency, as the person may not acquire the skills to handle similar situations independently in the future.

Unless there’s a good reason to ‘hijack’ the problem, for instance due to a short deadline, the following approach is recommended:

“Bob, this is a very interesting problem. Let's discuss it, and I'll assist you in finding a solution. I anticipate it might arise in future scenarios, making it crucial to solidify this learning.”

Now we see the clear mentoring approach, emphasizing the development of colleagues and a forward-looking perspective.

Closing thoughts

The more senior a team member becomes, the higher the expectation for them to have a positive impact on the team and subsequently in the business, inspiring and pushing their colleagues beyond their comfort zones, and thus earning recognition as a leader among peers.

It’s far from easy; in fact, it’s quite challenging to assume the responsibilities of mentoring and take a prominent role within the team. Effective time management will be essential to strike a balance between safeguarding our personal time and remaining accessible to the team. This means prioritizing tasks, setting clear boundaries, and ensuring that our schedule allows for both focused work and availability to assist our colleagues. By efficiently managing our time, we can fulfill our leadership roles without becoming overwhelmed or unavailable when needed.

The leadership challenge, whether in a managerial or technical role, is substantial, but the personal growth it brings is truly worth the effort.

Apr 21, 2023 - Setting up a reverse proxy using nginx and docker

A reverse proxy is a web server that sits between client devices and backend servers, receiving requests from clients and directing them to the appropriate server. The backend servers are shielded from direct internet access and their IP addresses are hidden, providing an additional layer of security.

reverse-proxy

Overall, reverse proxies are useful for improving security, performance, and scalability of web applications and services. They’re commonly used for load balacing traffic between any number of backend servers, and for SSL offloading.

In this brief post I provide a template for setting up an nginx reverse proxy using docker.

Docker-compose

This is a docker-compose file with two services, the nginx web server that will act as a reverse proxy, and a certbot agent for enabling SSL connections to it:

version: '3.3'

services:

  nginx:
    image: nginx:1.19-alpine
    restart: always
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./volumes/nginx:/etc/nginx/conf.d
      - ./volumes/certbot/conf:/etc/letsencrypt
      - ./volumes/certbot/www:/var/www/certbot
    command: "/bin/sh -c 'while :; do sleep 6h & wait $${!}; nginx -s reload; done & nginx -g \"daemon off;\"'"

  certbot:
    image: certbot/certbot
    restart: always
    volumes:
      - ./volumes/certbot/conf:/etc/letsencrypt
      - ./volumes/certbot/www:/var/www/certbot
    entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;'"

The nginx service uses the official Nginx Docker image with version 1.19-alpine. It maps ports 80 and 443 to the host machine, which allows incoming HTTP and HTTPS requests to be forwarded to the Nginx container. The volumes section maps three directories on the host machine to directories in the container:

./volumes/nginx is mapped to /etc/nginx/conf.d, which allows custom Nginx configuration files to be added to the container.

./volumes/certbot/conf is mapped to /etc/letsencrypt, which stores the SSL/TLS certificates generated by Certbot.

./volumes/certbot/www is mapped to /var/www/certbot, which is where Certbot writes temporary files during the certificate renewal process.

The certbot service uses the official Certbot Docker image. It also maps the same volumes as the nginx service. The entrypoint section specifies a shell command that is executed when the container starts up. This command runs a loop that sleeps for 12 hours before evaluating the renewal of SSL/TLS certificates using Certbot.

Now let’s see how each service is configured.

Nginx

Below you’ll’ find an nginx configuration file that sets it up as a load balancer and reverse proxy for the thomasvilhena.com domain:

### Nginx Load Balancer

upstream webapi {
	server 10.0.0.10;
	server 10.0.0.11;
	server 10.0.0.12; down;
}

server {
	listen 80;
	server_name localhost thomasvilhena.com;
	server_tokens off;
		
	location ^~ /.well-known/acme-challenge/ {
		default_type "text/plain";
		alias /var/www/certbot/.well-known/acme-challenge/;
	}
		
	location = /.well-known/acme-challenge/ {
		return 404;
	}
		
	location / {
		return 301 https://thomasvilhena.com$request_uri;
	}
}


server {
	listen 443 ssl http2;
	server_name localhost thomasvilhena.com;
	server_tokens off;

	ssl_certificate /etc/letsencrypt/live/thomasvilhena.com/fullchain.pem;
	ssl_certificate_key /etc/letsencrypt/live/thomasvilhena.com/privkey.pem;
	include /etc/letsencrypt/options-ssl-nginx.conf;
	ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;

	location / {
		proxy_pass http://webapi;
		proxy_set_header Host $http_host;
		proxy_set_header X-Real-IP $remote_addr;
		proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
		proxy_set_header X-Forwarded-Proto $scheme;
		proxy_set_header X-NginX-Proxy true;
		proxy_redirect off;
	}
}

The “upstream” section defines a group of servers to be load balanced, with three sample servers listed (10.0.0.10, 10.0.0.11, and 10.0.0.12). One server is marked as “down” which means it won’t receive requests.

The first “server” block listens on port 80 and redirects all requests to the HTTPS version of the site. It also includes some configuration for serving temporary files over HTTP which are required for the SSL certificate renewal process through Let’s Encrypt.

The second “server” block listens on port 443 for HTTPS traffic and proxies requests to the defined “upstream” group of servers. The “location /” block specifies that all URLs will be proxied. The various “proxy_set_header” directives are used to set the headers needed for the upstream servers to function correctly.

Certbot

Certbot requires two configuration files:

/volumes/certbot/conf/options-ssl-nginx.conf contains recommended security settings for SSL/TLS configurations in Nginx. Here’s a sample content:

ssl_session_cache shared:le_nginx_SSL:10m;
ssl_session_timeout 1440m;
ssl_session_tickets off;

ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off;

ssl_ciphers "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384";

/volumes/certbot/conf/ssl-dhparams.pem contains Diffie-Hellman parameters used for SSL/TLS connections. It is generated by running the following command:

openssl dhparam -out /etc/letsencrypt/ssl-dhparams.pem 2048

Here’s a sample content:

-----BEGIN DH PARAMETERS-----
MIIBCAKCAQEA3r1mOXp1FZPW+8kRJGBOBGGg/R87EBfBrrQ2BdyLj3r3OvXX1e+E
8ZdKahgB/z/dw0a+PmuIjqAZpXeEQK/OJdKP5x5G5I5bE11t0fbj2hLWTiJyKjYl
/2n2QvNslPjZ8TpKyEBl1gMDzN6jux1yVm8U9oMcT34T38uVfjKZoBCmV7g4OD4M
QlN2I7dxHqLShrYXfxlNfyMDZpwBpNzNwCTcetNtW+ZHtPMyoCkPLi15UBXeL1I8
v5x5m5DilKzJmOy8MPvKOkB2QIFdYlOFL6/d8fuVZKj+iFBNemO7Blp6WjKsl7Hg
T89Sg7Rln2j8uVfMNc3eM4d0SEzJ6uRGswIBAg==
-----END DH PARAMETERS-----

That’s it, now you just need to docker-compose up and your reverse proxy should be up and running ✅