Skip to main content

4 posts tagged with "ai"

View All Tags

· 2 min read
Sam Sussman

Eventual Cloud is the best way to build many different types of services. In this blog, we'll show how to build an OpenAI Chat Plugin.

Eventual Cloud provides a fully serverless framework for building plugins

Anatomy of a OpenAI Chat Plugin

An OpenAI Chat Plugin needs 3 things to operate (find all of the details here):

  1. /.well-known/ai-plugin.json - The Plugin Manifest defines details about what the plugin is and how it operates. This must be at a public /.well-known url and will be used when registering and installing the plugin.
  2. OpenAPI 3 Specification - OpenAPI 3 Specification is used to describe the REST APIs that are available and provide descriptions of the APIs, request, response, and fields involved in the API. This file should be public and hosted at the location described in api.url in the ai-plugin.json file.
  3. Rest API - Finally the REST API described in the OpenAPI Specification must be available at the url in the OpenAPI servers array.

Getting Started

This blog will start an OpenAI Plugin from scratch. There is a complete version in eventual-examples.

  1. Create an EventualCloud project.
  2. Create the ai-plugin endpoint
  3. Create the OpenAPI Spec
  4. Create our plugin operations
  5. Describe the operations for Open AI
  6. Test the Plugin Locally with Lang Chain
  7. Test the Plugin locally with ChatGPT
  8. Deploy the Plugin

Create An Eventual Cloud Project

· 5 min read
Sam Goodwin

What a year it has been already! It’s only been 3 months and we’ve already seen glimpses of a future that looks nothing like today. A world where real AI (actually intelligent AI) is available to everyone at low-cost. I think we’re all starting to wonder about our place in the world and how we need to change the way we work and what we do.

I’ve always been obsessed with efficiency and abstraction, which is what has drawn me to programming as a profession. For the past 5-6 years I’ve been thinking deeply about how cloud development, specifically on AWS, can be simplified down to a very simple programming model instead of what we have today. I’ve always wondered - what is that next level of abstraction for developers that will unlock more value? The next “infrastructure as code” (IaC), so to speak. I used to believe strongly that it was new programming languages, but it now seems more likely that the next level of abstraction is natural language!

What is powerful about IaC is the concept of “declarative infrastructure”. Instead of worrying about HOW to update your infrastructure configuration, you simply “declare” WHAT infrastructure you need and the engine takes care of “making it happen”. I love this way of working because it eliminates a whole class of problems and enables new forms of abstractions like templates and component libraries of infrastructure.

Today, I’m excited to announce eventualAi (waitlist) and eventualCloud (public beta), which we believe are the next steps in declarative software development - where business requirements can be declared in natural, conversational language and then fed into an AI system that takes care of making it happen.

eventualAi is a companion autonomous software development team of smart agents who know how to use the eventualCloud framework and apply Domain Driven Design (DDD) principles to translate business requirements into functioning services. Feed it your business problem or domain in natural language and have it “print” on-demand, scalable and fully serverless solutions.



eventualCloud is an open source, high-level framework for building distributed systems on AWS with TypeScript, serverless and IaC. It provides "core abstractions" — including APIs, Transactions, Messaging and long-running, durable Workflows — that shield you from the complexities of distributed systems and ensure a consistent, best-practice serverless architecture. Our goal with eventualCloud is to streamline and standardize the process of building functional services on AWS by providing code-first primitives and patterns that align with business concepts. We won't go deep into the details, for that you can read this dedicated blog post.

In short, we’ve long wanted a better way to build cloud services - one that gives the right level of abstractions for distributed systems in a simple programming model. For example, implementing long-running workflows shouldn’t involve learning weird, archaic domain specific languages when we already have tools like if-else, for-loops, async/await in all the programming languages we know and love. We want to program the cloud in the same way we program servers and local machines - and eventualCloud enables this.

An unforeseen benefit of how we built eventualCloud is that the same solutions that make it easier for human engineers to translate business requirements into solutions, also help AI agents do the same. Because our abstractions map closely with business processes and constrain the problem of “build me a service that does XYZ” down to a few repeatable and scalable patterns, intelligent agents built with LLMs (when given the right instructions and context) can apply these patterns just as effectively.

This new capability ushers in a transformative way for businesses to tackle problem-solving. It is now possible to truly “work backwards” from the customer in a declarative way, focusing on WHAT needs to be solved instead of HOW to solve it. Define your business goals, tenets, and policies in natural language, and then employ an intelligent agent to explore your business domain automatically. This intelligent agent can incorporate feedback, suggest improvements, break down the domain into smaller pieces, and even implement solutions. Additionally, it can maintain and operate production services, providing an all-encompassing solution for businesses.

We believe that we’re entering a new era of software development frameworks where the primary user is an intelligent AI with human supervision. Frameworks already do the job of simplifying and standardizing how problems should be solved, making them ideal “targets” for what we think of as “AI compilers” - i.e. systems of intelligent agents that “compile” business problems described in natural language to functioning solutions.

We’ll be sharing more soon. You can start building and playing with the eventualCloud framework today or sign up for the eventualAi waitlist. Come say hi on Discord, star us on GitHub, and follow us on Twitter. We’d love to hear about your business use-cases to help refine the technology before open sourcing it.

· 4 min read
Sam Goodwin

For years, I’ve dreamed of building serverless, massively distributed micro-services with the simplicity of a local program. I strongly believe there’s no conceptual difference between a laptop and the cloud. Both are complex systems made of many components. However, while we can simply program our laptop with a single language and runtime, unfortunately, the same is not true for the cloud. The developer experience is still at the “hardware level” so to speak - meaning that developers must understand how to configure each individual service and then glue them together into a functioning architecture. This is hard.

While there’s no conceptual difference between a laptop and the cloud, there is a glaringly obvious physical one - the cloud is massively distributed, and distributed systems are subject to an entire category of new problems around timing, failure and consistency. In a way, a large part of AWS’s business is providing solutions to these problems in the form of managed services, such as AWS Step Functions, or Event Bridge.

But, these services come at the cost of a consistent developer experience. They’re severely disjointed. You can’t just write code and run it - instead you have to: 1) hand-configure these services, all with their own domain specific configuration languages, and 2) reason through each of their failure cases and how they affect each other. You also can’t test all this easily because the entire system can’t run locally. This is why the current “best practice” is that you should mostly rely on integration tests. All of this requires specialized knowledge, has a slow developer iteration cycle, and limits composability and re-usability. Over time this accumulates as tech debt.

We believe that developing a scalable and fault tolerant service should be as lean as building a frontend app in a framework like NextJS. Simply make a change and then observe its impact in less than a second, not minutes or hours. The framework should take care of reasoning through complex failure cases so you can just focus on your application.

This is the vision of Eventual.

Eventual is an open source TypeScript framework that offers "core abstractions" — including APIs, Messaging and long-running, durable Workflows — that shield you from the complexities of distributed systems and ensure a consistent, best-practice serverless architecture. The top-level concept of Eventual is a "Service" that has its own API Gateway, Event Bus, and Workflow Engine that you customize and build on using the core abstractions.

Each Service can be thought of as exposing a Service Interface, consisting of APIs and Events, and then then performing Event-Driven Orchestration & Choreography with Workflows, Tasks and Subscriptions.

Service Contract

These can then be adopted iteratively, and seamlessly integrate and interact with any external or existing system, whether it be a database or another service. In our opinion, having confidence in (and relying on) these consistent patterns can 10x productivity from a reliability, productivity, maintainability and agility standpoint.

Everything can be written and tested in a single TypeScript code-base. You can run your massively scalable distributed cloud system LOCALLY before you even deploy 🤯. Run, test and iterate locally, then deploy only when it’s working. And here's the kicker - you can even debug production locally. That's right - debug your production system on your local machine.

Developer Iteration Cycle

Eventual is built with (and consumed from) modern Infrastructure-as-Code tooling such as the AWS CDK and Pulumi to give you maximum flexibility and control, as well as total ownership of your data by deploying everything into your own AWS account and within your own security boundaries. We implement best practices and solve edge cases for you so you can spend more time on your application. Best practice security, best practice scalability, best practice operations, best practice cost efficiency etc. - we lay the groundwork for you.

Finally, because it’s all TypeScript, you have full end-to-end type safety for a massively scalable distributed cloud system. We believe that relying on the TypeScript compiler to scream at you when you make mistakes is one of the biggest time savers you can have as a developer. As you make changes, simply follow the red squiggly lines all the way from your frontendbackend serviceinfrastructure configuration.

In the next part, eventualCloud Part 2 - Features, we'll walk through each of the features offered by Eventual. Be sure to check that out if you want to see some code!

To jump right in, see the Quick Start.

-Sam

· 11 min read
Sam Goodwin

In the previous part, eventualCloud Part 1 - Philosophy, we introduced the philosophy behind Eventual. How we envision a world where programming massively scalable, distributed systems in the cloud is as simple as writing local programs. In this second part, we'll give an overview of Eventual's features and developer experience.

Service

The Service is the top-level Concept of Eventual. It's a totally encapsulated micro-service deployable with a simple Construct that can be instantiated in an AWS CDK or Pulumi application.

It takes only 4 lines of code to deploy an entire micro-service to AWS:

const myService = new Service(this, "Service", {
name: "my-service",
// point it at where your backend code NPM package is
entry: require.resolve("@my/service"),
});

Each Service has its own API Gateway, Event Bus and Workflow engine. And, because it’s all just Infrastructure-as-Code, it can be customized to your heart’s content.

The business logic of the Service is automatically discovered by analyzing the entry point of your code. In there are Commands, Events, Subscriptions, Workflows, Tasks and Signals.

APIs

What would a service without APIs? Answer: not much. As mentioned, each Service comes with its own API Gateway that you can register routes on using Commands (RPC) or a HTTP router.

Command - i.e. RPC

A Command is simply a function that can be called over HTTP - aka. Remote Procedure Call (RPC). It has a simple input/output contract - it takes one argument as input and returns a value as output.

export const hello = command("hello", async (name: string) => {
return `hello ${name}`;
});

Each command is automatically added as a route on your Service’s API Gateway and invokes a dedicated, individually tree-shaken AWS Lambda Function. This enables you to tweak and tune the memory, timeout (and any other properties) for individual API routes.

APIs are exposed to the outside world, so it's important to provide a schema to validate requests. Eventual integrates with Zod for defining schemas.

export const hello = command(
"hello",
{
input: z.string(),
},
async (name) => {
return `hello ${name}`;
}
);

These schemas are then used for runtime validation in your Function, but also to generate an OpenAPI spec and attach it to your API Gateway. This ensures your Lambda Function is only invoked if the data is valid according to the schema - a good practice.

Calling commands from another application, for example your frontend react application, can be achieved without any code generation using the ServiceClient. And it’s all type-safe.

import type * as MyService from "@my/service";

const client = new ServiceClient<typeof MyService>({
serviceUrl: process.env.SERVICE_URL!,
});

await client.hello("sam");

Simply, import the types of your backend into the consuming application and instantiate a client. In this case @my/service points to a separate NPM package containing the service code. You can then directly call commands as if they were in the same code-base, while also promoting sensible separation of concerns.

REST (i.e. raw HTTP)

If you need to register raw HTTP routes, such as GET, PUT, POST, PATCH, etc., you can always use the api router.

api.get("/hello", async (request) => {
return new Response("OK");
});

Similar to Commands, reach route translates to an individual Lambda Function invoked by your API Gateway.

Middleware

Commands and HTTP routes can integrate with middleware chains that perform functions such as validating requests, setting headers, authorizing and fetching user information.

To create a Command with middleware, use the api.use utility to first create a middleware chain, and then finally created the command.

export const hello = api
.use(cors)
.use(authorized)
.command("hello", async (name: string, { user }) => {
// etc.
});

Messaging

The next aspect of an event-driven micro-service is Messaging. In Eventual, we provide Events and Subscriptions for passing messages around within and outside a Service.

When something happens in a service, it’s often a good idea to record it as an “event” and emit it to an Event Bus so other parts of your system can react to it. They’re also useful for logging and analytical use-cases, among many others. This is known as “Choreography”

Subscriptions have the benefit of decoupling the emitter of an event from the subscriber. This simplifies how you evolve your system over time as you can always add more subscribers without disrupting other parts of your service.

Event

In Eventual, you declare Event types:

export const HelloEvent = event("HelloEvent");

You can then emit an event from anywhere using the emit function:

await HelloEvent.emit({ key: "value"});

Sticking with our theme of TypeScript and type-safety, Eventual supports declaring a type for each event - and we highly encourage you to do so. There’s nothing worse than un-typed code.

export const HelloEvent = event<{
key: string;
}>("HelloEvent");

And for that extra level of safety, you can also use Zod to define a schema for runtime validation.

export const HelloEvent = event("HelloEvent", z.object({
key: z.string().min(1)
});

Subscription

To process events, you create a Subscription to one or more event types.

export const onHelloEvent = subscription(
"onHelloEvent",
{
events: [HelloEvent],
},
async (event) => {
console.log(event.key);
}
);

Each Subscription will automatically create a new Lambda Function, Event Bridge Rule and a SQS Dead Letter Queue.

Your function will be invoked by AWS Event Bridge for each event that matches the selection and any messages that fail to be processed will be safely stored in the dead letter queue for you to deal with as a part of your operational procedure.

Orchestration

When we talk about programming the cloud like a local machine, there’s just no getting around the distributed nature of it. Everything fails, all the time. So, orchestrating business logic that interacts with people, time and services is a challenging task.

Workflow

The most powerful piece of Eventual is most definitely the Workflow. In Eventual, you can orchestrate long running, durable workflows using plain TypeScript - such as if-else, loops, functions, async/await, and all that goodness. This gives you an expressive, Turing complete way to implement business logic, however complex, distributed or time-dependent it may be.

Workflows are where you put control-flow logic. Eventual ensures your code runs exactly as written, in a fault tolerant way such that you do not need to worry about things like transient failures, race conditions, temporary outages, or runtime duration etc.

For example, the below code implements a workflow that will send an email to a user every day. It will loop forever, sleep for a day and then send an email.

export const emailDaily = workflow("emailDaily", async (email: string) => {
while (true) {
await duration(1, "day");

// send an email to the user every day
await sendEmail(email);
}
});

With Eventual, your code can run forever, even sleep forever. We achieve this feat using serverless primitives behind the scene to allow you to program distributed systems with the mental model of a local machine.

Task

Workflows are not where you do actual work, such as interacting with a database. They are purely for deciding what to do and when. Instead, you separate out side-effects into what are called Tasks.

A task is a function that runs in its own AWS Lambda Function and can be invoked by a Workflow with exactly-once guarantees.

export const getUser = task("getUser", async (userId: string) => {
return client.getItem({
TableName: process.env.TABLE_NAME,
Key: { userId },
});
});

If you call a task, you can be sure it will run exactly once, which enables you to safely control when you interact and change resources such as database records.

You can also configure things like a retry policy that the platform will enforce, as well as protections such as heartbeats.

task(
"getUser",
{
// require a heartbeat every 30s
heartbeatTimeout: duration(30, "seconds"),
},
async (userId: string, ctx) => {
await ctx.sendHeartbeat();
}
);

Signal

Signals are messages that can be sent into a running workflow. They’re useful for integrating other parts of your application into a workflow, for example having a person approve something before continuing.

Creating a Signal is very similar to creating an Event type. All you need is a name and an optional type.

export const userEmailChanged = event<string>("userEmailChanged");

You can then use expectSignal within a workflow to pause execution until such information is received:

await userEmailChanged.expectSignal();

Or register a callback to be invoked whenever a signal is received:

userEmailChanged.onSignal(async (newAddress) => {
emailDaily(newAddress);
});

Signals are a powerful tool for building capabilities around workflows, for example human-in-the-loop systems where a UI or CLI can send data into a workflow to influence it.

This is barely scratching the surface of workflow orchestration - to learn more visit eventual.ai.

Testing

Testing distributed systems is difficult because of how fragmented the system is physically. It can be impossible or impractical to reproduce timing and race conditions in a real-world system with integration tests.

In Eventual, you can test any function locally. We also provide a TestEnvironment utility that gives fine-grained control over time and the underlying system, so that you can target tests towards those tricky edge cases.

You can write tests for your workflows with per-second granularity, up to extremes such as days, months or even years.

test("workflow should wait 1 second before completing", async () => {
const execution = await env.startExecution(myWorkflow, "input");

expect(await execution.getStatus()).toBe("PENDING");

// advance time by 1 second
env.tick(1);

expect(await execution.getStatus()).toBe("SUCCESS");
});

This test starts workflow, asserts that it is running, then explicitly advances time by 1 second, and then asserting that the workflow completed successfully. This form of control allows you to craft deterministic tests for timing and race conditions.

Local Simulation

An entire Eventual service can be simulated locally. Simply run the eventual local command to stand up a server on localhost:9000 which can be interacted with on your local machine.

eventual dev

Set breakpoints in your code and step-through any part of your application.

Even parts that span multiple cloud services, such as APIs emitting Events, that trigger Subscriptions, that then trigger Workflows, and so on.

The entire control flow can be walked through within the context of a single NodeJS runtime.

Debug Time Machine

Imagine the scenario where you’ve been paged at 2am in the morning because one of your workflows broke for some unknown reason.

Eventual provides what we call the “Debug Time Machine” that allows you to replay a workflow execution that already ran (or is still running) in production, locally, so you can debug from the comfort of your IDE.

Simply take the workflow execution ID and run the eventual replay CLI command.

eventual replay --execution-id <execution-id>

This will download the workflow’s history and run everything locally. You can then attach your debugger, for example with VS Code, and step through everything that happened as if it’s happening in real-time. Inspect variables, look at the returned values of tasks, identity and fix the bug.

A note on end-to-end Type Safety

This blog is getting a bit long, it’s hard to fit it all in! We’ll finish with a note on how Eventual really goes the extra mile when it comes to “end-to-end type safety”.

We use types to map everything back to the source, from your frontend → to your service implementation → and finally to its infrastructure configuration. This makes refactoring as easy as following those red squiggly lines. If your code compiles, you can be pretty confident it’s working - or at least that there’s no stupid mistakes 😉.

As previously mentioned, you can use the ServiceClient to call your Commands without generating any code. Simply import the types of your backend code and instantiate the client.

import type * as MyService from "@my/service";

const client = new ServiceClient<typeof MyService>({
serviceUrl: process.env.SERVICE_URL!,
});

await client.hello("sam");

The same goes for when you’re configuring infrastructure. Import the types of the backend and then safely customize and integrate with each of the pieces of generated infrastructure.

import type * as MyService from "@my/service";

const service = new Service<typeof MyService>(this, "Service", {
commands: {
// safely configure any of the commands
hello: {
environment: { .. }
}
}
});

// safely access any generated infrastructure

// such as the hello Command's Lambda Function
service.commands.hello;

// or a Subscription's dead letter queue
service.subscriptions.onHelloEvent.deadLetterQueue

Conclusion

That does it for now. To learn more, visit eventual.ai, star us on GitHub, follow us on twitter, and please, come chat to us on Discord. We’d love to hear from you

We want to help you build scalable cloud services. And we want it to be fast and we want it to be fun .