Skip to content

Getting Started

Brian MacKay edited this page Aug 11, 2023 · 15 revisions

What is Ouroboros.NET?

Ouro works fine for making OpenAI completion API calls, but it shines in two areas:

  • Chaining: breaking a problem up into multiple steps by piping the results of one prompt into another. This often leads to better reasoning and better results. It also leads to chain-of-thought, the research-backed basis for most generative agents. Ouro provides a fluent API for this, as well as tools for manipulating the resulting conversation.
  • Output Processing: Ouro has great tools for turning text generated by LLMs into C# objects that are easy to use in code.

Ouro is built on top of Betalgo, an open source SDK for OpenAI.

How do I get started?

First, you need to sign up for OpenAI and get an API key. This is separate from ChatGPT Pro.

You should never save keys directly in source code. If you commit code to a public repo that contains an OpenAI key in plaintext, they will disable it within seconds. Look into secrets for best practices.

Dependency Injection

Drop this into Program.cs under service configuration:

services.AddOuroboros("[SECRET]");

From there, you can inject OuroClient into MVC controllers, Blazor pages, etc.

Without Dependency Injection

You can create an OuroClient directly like this:

var client = new OuroClient("[SECRET]");

Simple Chaining

Consider this simple example, which you can find in the repo under Ouroboros.Console > Program.cs:

var client = new OuroClient("[SECRET]");

var dialog = client.CreateDialog();
await dialog
    .SystemMessage("# Writer\r\n" +
                   "You are a brilliant writer who creates and refines scenes for sci-fi stories.")
    .UserMessage("Generate 10 great story ideas.")
    .SendAndAppend()
    .UserMessage("From this list, identify the story idea that will bring the most joy to the world. Create an outline for it using the 3-act structure.")
    .SendAndAppend()
    .UserMessage("Generate 10 ideas for a main character, numbered. Only include the numbered list. Use no additional commentary.\r\n" +
                 "Format: [Number]. [Name]: [Summary]")
    .SendAndAppend()
    .Execute();

if (dialog.HasErrors)
{
    Console.WriteLine("Last Error: " + dialog.LastError);
}

Console.WriteLine(dialog.ToString());

This trivial chain about stories shows you how the output of one prompt can flow into another, and another. Even though this is a silly example intended to demonstrate syntax, it's worth pointing out that the results of this chain are pretty good. If you continued down the road of creating a story, very quickly this multi-step chained approach would outstrip the capabilities of a single prompt.

Detailed Breakdown

So what are we doing here? Let's go step by step. :) First, we create a dialog:

var dialog = client.CreateDialog();

Dialog keeps track of the chain and including all messages and any errors. It gives you several methods of manipulating messages. Behind the scenes, it also holds a reference to the client and uses it to call OpenAI for you.

Next, we put a prompt together.

await dialog
    .SystemMessage("# Writer\r\n" +
                   "You are a brilliant writer who creates and refines scenes for sci-fi stories.")
    .UserMessage("Generate 10 great story ideas.")
    .SendAndAppend()

The system message tells the prompt what it's purpose is. The user message line is the equivalent of typing a message into ChatGPT.

SendAndAppend() executes the prompt and then adds the result as a new message. This is pretty basic; at this point we've just built up a prompt and run it.

    .UserMessage("From this list, identify the story idea that will bring the most joy to the world. Create an outline for it using the 3-act structure.")
    .SendAndAppend()

Now we're chaining. This is like an automated ChatGPT session. The 10 great story ideas are in there, even though a human never necessarily sees them, and we tell it to choose one and write an outline. It does, and we tell it to add this onto our conversation.

    .UserMessage("Generate 10 ideas for a main character, numbered. Only include the numbered list. Use no additional commentary.\r\n" +
                 "Format: [Number]. [Name]: [Summary]")
    .SendAndAppend()
    .Execute();

More chaining. This time we're generating character ideas. The Execute() command is important. It tells the dialog to run all the commands you entered.

if (dialog.HasErrors)
{
    Console.WriteLine("Last Error: " + dialog.LastError);
}

Console.WriteLine(dialog.ToString());

This shows that the dialog can report back if any errors happened during execution, even if multiple API calls are involved. Note that by default, Polly is set up to do multiple retries. Currently, the OpenAI API can be a little flaky, so retries are important for real-world applications.

The last line writes out the entire conversation and is mostly for debugging. In this case, it will return all messages ending in the ten numbered character ideas we requested.

What's the point?

In the real world, the need for chains comes up fairly often and they generate a lot of ugly code. This example would have taken pages, but here the meaning is tight and maintainable.

It's also possible to use Ouro in cases where you need to run a chain, do some server or human processing, and then continue with the chain. Or take the output from one prompt and use it as part of a new prompt somewhere else.

In the example above, ayou could of course continue the chain much longer, attempting to write an entire story. The results of chaining in this way will be much better than if you had tried to do it in one operation. More importantly, chaining creates points where a human could intervene, or where you could take the output and put it together with a new prompt optimized for that specific step in the process.

What else?

Ouro is designed for text processing. Our example above ends with creating a list of 10 characters. You could do something like this:

var characters = await dialog
    .SystemMessage("# Writer\r\n" +
                   "You are a brilliant writer who creates and refines scenes for sci-fi stories.")
    .UserMessage("Generate 10 great story ideas.")
    .SendAndAppend()
    .UserMessage("From this list, identify the story idea that will bring the most joy to the world. Create an outline for it using the 3-act structure.")
    .SendAndAppend()
    .UserMessage("Generate 10 ideas for a main character, numbered. Only include the numbered list. Use no additional commentary.\r\n" +
                 "Format: [Number]. [Name]: [Summary]")
    .SendAndExtractNumberedList();

This is the same example, but we've replaced the .Execute() line with SendAndExtractNumberedList(). This runs some powerful, heavily-tested pattern matching and returns List<NumberedListItem>, which has an Index field for the number and a Text field with the details about the character.

This, like Execute(), is called a Terminator because it's always the last item in the fluent interface and it tells Ouro to run all the commands you've chained. There are other Terminators, like ExtractString() which just gets a string, and there's potential for working with a few other kinds of responses. It turns out you can tell GPT to only respond with yes or no, likert scale items such as Strongly Agree, etc.

When you use a Terminator, it is important to check the dialog for errors. If your communication with OpenAI fails, the return value of your terminator will likely be empty.

What's Next?

Although there's more to cover, this is all we have for documentation at the moment. We will write more if there's interest. We are always looking for collaborators so please reach out if you want to get involved and grow the .NET AI scene!