Skip to content

Commit

Permalink
[DOC] - Fix some documentations (#539)
Browse files Browse the repository at this point in the history
* Fixed some documentations

* Update docs/intro.md

Co-authored-by: José Carlos Montañez <[email protected]>

* Fix some mistakes, thanks @Montagon

---------

Co-authored-by: José Carlos Montañez <[email protected]>
  • Loading branch information
tomascayuelas and Montagon authored Nov 14, 2023
1 parent a4b27ab commit 9f2f45c
Show file tree
Hide file tree
Showing 3 changed files with 141 additions and 79 deletions.
14 changes: 7 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,23 +47,23 @@ strategies.

Libraries are published in Maven Central, under the `com.xebia` group.

1. `xef-kotlin` for Kotlin support.
2. The name of a library we provide integration for, like `xef-lucene`.
1. `xef-core` is the core library.
2. `xef-openai` is the integration with OpenAI's API.
3. The name of a library we provide integration for, like `xef-lucene`.


Libraries are published in Maven Central. You may need to add that repository explicitly
in your build, if you haven't done it before.
You may need to add that repository explicitly in your build, if you haven't done it before.

```groovy
repositories { mavenCentral() }
```

Then add the library in the usual way.
Then add the libraries in the usual way.

```groovy
// In Gradle Kotlin
dependencies {
implementation("com.xebia:xef-kotlin:<version>")
implementation("com.xebia:xef-core:<version>")
implementation("com.xebia:xef-openai:<version>")
}
```

Expand Down
195 changes: 127 additions & 68 deletions docs/intro.md
Original file line number Diff line number Diff line change
@@ -1,53 +1,52 @@
# Quick introduction to xef.ai (Kotlin version)
## Getting the libraries

After adding the library to your project
(see the [main README](https://github.com/xebia-functional/xef/blob/main/README.md) for instructions),
you get access to the `ai` function, which is your port of entry to the modern AI world.
Inside of it, you can _prompt_ for information, which means posing the question to an LLM
(Large Language Model). The easiest way is to just get the information back as a string.
Libraries are published in Maven Central. You may need to add that repository explicitly
in your build, if you haven't done it before. Then add the library in the usual way.

```kotlin
import com.xebia.functional.xef.auto.*
repositories {
mavenCentral()
}

fun books(topic: String): List<String> = ai {
promptMessage("Give me a selection of books about $topic")
}.getOrThrow()
dependencies {
implementation("com.xebia:xef-core:<version>")
implementation("com.xebia:xef-openai:<version>")
}
```

> **Note**
> By default the `ai` block connects to [OpenAI](https://platform.openai.com/).
> To use their services you should provide the corresponding API key in the `OPENAI_TOKEN`
> environment variable, and have enough credits.
We publish all libraries at once under the same version, so
[version catalogs](https://docs.gradle.org/current/userguide/platforms.html#sec:sharing-catalogs)
could be useful.

In the example above we _execute_ the `ai` block with `getOrThrow`, that throws an exception
whenever a problem is found (for example, if your API key is not correct). If you want more
control, you can use `getOrElse` (to which you provide a custom handler for errors), or
`toEither` (which returns the result using
[`Either` from Arrow](https://arrow-kt.io/learn/typed-errors/either-and-ior/)).
By default, the `OpenAI.conversation` block connects to [OpenAI](https://platform.openai.com/).
To use their services you should provide the corresponding API key in the `OPENAI_TOKEN`
environment variable, and have enough credits.

In the next examples we'll write functions that rely on `ai`'s DSL functionality,
but without actually extracting the values yet using `getOrThrow` or `getOrElse`.
We'll eventually call this functions from an `ai` block as we've shown above, and
this allows us to build larger pipelines, and only extract the final result at the end.
```shell
env OPENAI_TOKEN=<your-token> <gradle-command>
```

This can be done by either writing an extension function on `AIScope`, or by using the form `AI<Something>`.
Let's compare the two:
> **Caution / Important**: <br />
>This library may transmit source code and potentially user input data to third-party services as part of its functionality.
>Developers integrating this library into their applications should be aware of this behavior and take necessary precautions to ensure that sensitive data is not inadvertently transmitted.
>Read our [_Data Transmission Disclosure_](https://github.com/xebia-functional/xef#%EF%B8%8F-data-transmission-disclosure) for further information.
```kotlin
import com.xebia.functional.xef.auto.*
## Your first prompt

After adding the library to your project
you get access to the `conversation` function, which is your port of entry to the modern AI world.
Inside of it, you can _prompt_ for information, which means posing the question to an LLM
(Large Language Model). The easiest way is to just get the information back as a string.

suspend fun AIScope.books(topic: String): String =
promptMessage("Give me a selection of books about $topic")
```kotlin
import com.xebia.functional.xef.conversation.llm.openai.OpenAI
import com.xebia.functional.xef.conversation.llm.openai.prompt

fun books2(topic: String): AI<String> =
promptMessage("Give me a selection of books about $topic")
suspend fun books(topic: String): String = OpenAI.conversation {
prompt("Give me a selection of books about $topic")
}
```

Both functions are equivalent, but the first is considered most idiomatic, and can be compared to
`CoroutineScope` from KotlinX Coroutines which gives access to concurrency primitives like `launch` and `async`.
The second form is useful when you want to create an extension function on something else than `AIScope`,
and you can use `bind` to extract the `String` value from `AI<String>` within an `ai` block or an `AIScope`.

## Structure

The output from the `books` function above may be hard to parse back from the
Expand All @@ -56,43 +55,96 @@ using a _custom type_. The library takes care of instructing the LLM on building
a structure, and deserialize the result back for you.

```kotlin
import com.xebia.functional.xef.auto.*
import com.xebia.functional.xef.conversation.llm.openai.OpenAI
import com.xebia.functional.xef.conversation.llm.openai.prompt
import kotlinx.serialization.Serializable

@Serializable
data class Books(val books: List<Book>)

@Serializable
data class Book(val title: String, val author: String)

suspend fun AIScope.books(topic: String): AI<List<Book>> =
prompt("Give me a selection of books about $topic")
suspend fun books(topic: String): Books = OpenAI.conversation {
prompt("Give me a selection of books about $topic")
}
```

xef.ai reuses [Kotlin's common serialization](https://kotlinlang.org/docs/serialization.html),
which requires adding the `kotlinx.serialization` plug-in to your build, and mark each
class as `@Serializable`. The LLM is usually able to detect which kind of information should
go on each field based on its name (like `title` and `author` above).
For those cases where the LLM is not able to infer the type, you can use the `@Description` annotation:

## Prompt templates
## @Description annotations

```kotlin
import com.xebia.functional.xef.conversation.Description
import com.xebia.functional.xef.conversation.llm.openai.OpenAI
import com.xebia.functional.xef.conversation.llm.openai.prompt
import kotlinx.serialization.Serializable

@Serializable
@Description("A list of books")
data class Books(
@Description("The list of books")
val books: List<Book>
)

@Serializable
@Description("A book")
data class Book(
@Description("The title of the book")
val title: String,
@Description("The author of the book")
val author: String,
@Description("A 50 word summary of the book")
val summary: String
)

suspend fun books(topic: String): Books = OpenAI.conversation {
prompt("Give me a selection of books about $topic")
}
```

All the types and properties annotated with `@Description` will be used to build the
json schema `description` fields used for the LLM to reply with the right format and data
in order to deserialize the result back.

## Prompts

The function `books` uses naive string interpolation to make the topic part of the question
to the LLM. As the prompt gets bigger, though, you may want to break it into smaller parts.
The `buildPrompt` function is the tool here: inside of it you can include any string or
smaller prompt by prefixing it with `+`
The `buildPrompt` function is the tool here: inside of it you can include messages and other prompts
which get built before the chat completions endpoint.
(this is known as the [builder pattern](https://kotlinlang.org/docs/type-safe-builders.html)).

```kotlin
import com.xebia.functional.xef.auto.*
import com.xebia.functional.xef.conversation.Conversation
import com.xebia.functional.xef.conversation.llm.openai.prompt
import com.xebia.functional.xef.prompt.Prompt
import com.xebia.functional.xef.prompt.templates.*
import kotlinx.serialization.Serializable

@Serializable
data class Book(val title: String, val author: String)

suspend fun AIScope.books(topic: String): AI<List<Book>> {
val prompt = buildPrompt {
+ "Give me a selection of books about the following topic:"
+ topic
@Serializable
data class Books(val books: List<Book>)

suspend fun Conversation.books(topic: String): Books {
val myPrompt = Prompt {
+system("You are an assistant in charge of providing a selection of books about topics provided")
+assistant("I will provide relevant suggestions of books and follow the instructions closely.")
+user("Give me a selection of books about $topic")
}
return prompt(prompt)
return prompt(myPrompt)
}
```

This style of prompting is more effective than simple strings messages as it describes a scene of how the LLM
should behave and reply. We use different roles for each message constructed with the `Prompt` builder.

In a larger AI application it's common to end up with quite some template for prompts.
Online material like [this course](https://www.deeplearning.ai/short-courses/chatgpt-prompt-engineering-for-developers/)
and [this tutorial](https://learnprompting.org/docs/intro) explain some of the most important patterns,
Expand All @@ -108,33 +160,40 @@ often want to supplement the LLM with more data:
- Non-public information, for example for summarizing a piece of text you're creating
within you organization.

These additional pieces of information are called the _contextScope_ in xef.ai, and are attached
These additional pieces of information are called the _context_ in xef.ai, and are attached
to every question to the LLM. Although you can add arbitrary strings to the context at any
point, the most common mode of usage is using an _agent_ to consult an external service,
and make its response part of the context. One such agent is `search`, which uses a web
search service to enrich that context.

```kotlin
import com.xebia.functional.xef.auto.*
import com.xebia.functional.xef.reasoning.serpapi.Search

suspend fun AIScope.whatToWear(place: String): String =
contextScope(search("Weather in $place")) {
promptMessage("Knowing this forecast, what clothes do you recommend I should wear?")
@Serializable
data class MealPlan(
val name: String,
val recipes: List<Recipe>
)

suspend fun mealPlan() {
OpenAI.conversation {
val search = Search(OpenAI.FromEnvironment.DEFAULT_CHAT, this)
addContext(search("gall bladder stones meals"))
prompt("Meal plan for the week for a person with gall bladder stones that includes 5 recipes.")
}
}

```

To use the `search` agent you need to define your SERPAPI key in the `SERP_API_KEY` environment variable.
```shell
env SERP_API_KEY=<your-api-key>
```

> **Note**
> The underlying mechanism of the context is a _vector store_, a data structure which
> saves a set of strings, and is able to find those similar to another given one.
> By default xef.ai uses an _in-memory_ vector store, since it provides maximum
> compatibility across platforms. However, if you foresee your context growing above
> the hundreds of elements, you may consider switching to another alternative, like
> Lucene or PostgreSQL.
>
> ```kotlin
> import com.xebia.functional.xef.auto.*
> import com.xebia.functional.xef.vectorstores
>
> suspend fun AIScope.books(topic: String): List<Book> =
> contextScope(InMemoryLuceneBuilder(LUCENE_PATH)) { /* do stuff */ }
> ```
>**Better vector stores**<br />
>The underlying mechanism of the context is a _vector store_, a data structure which
>saves a set of strings, and is able to find those similar to another given one.
>By default xef.ai uses an _in-memory_ vector store, since it provides maximum
>compatibility across platforms. However, if you foresee your context growing above
>the hundreds of elements, you may consider switching to another alternative, like
>Lucene or PostgreSQL also supported by xef.
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,14 @@ For this example we are using a docker container with a mysql database with some

# Steps to run

### Build and up the mysql container
`./gradlew xef-kotlin-examples:docker-sql-example-up`
### Build and up the mysql container
`./gradlew xef-examples:docker-sql-example-up`

### Create the database and populate it with some data:
`./gradlew xef-kotlin-examples:docker-sql-example-create`
`./gradlew xef-examples:docker-sql-example-populate`

### Set OPENAI_TOKEN and run the example
`env OPENAI_TOKEN=<your-token> ./gradlew xef-kotlin-examples:run-sql-example`
`env OPENAI_TOKEN=<your-token> ./gradlew xef-examples:run-sql-example`

### Clean up the mysql container
`./gradlew xef-examples:docker-sql-example-down`

0 comments on commit 9f2f45c

Please sign in to comment.