From 9f2f45c39e4471f524b729f31aae6037d03a4516 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Cayuelas=20Ruiz?= Date: Tue, 14 Nov 2023 16:13:29 +0100 Subject: [PATCH] [DOC] - Fix some documentations (#539) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fixed some documentations * Update docs/intro.md Co-authored-by: José Carlos Montañez * Fix some mistakes, thanks @Montagon --------- Co-authored-by: José Carlos Montañez --- README.md | 14 +- docs/intro.md | 195 ++++++++++++------ .../functional/xef/conversation/sql/README.md | 11 +- 3 files changed, 141 insertions(+), 79 deletions(-) diff --git a/README.md b/README.md index ca77c7c77..b2f621525 100644 --- a/README.md +++ b/README.md @@ -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:") + implementation("com.xebia:xef-core:") + implementation("com.xebia:xef-openai:") } ``` diff --git a/docs/intro.md b/docs/intro.md index ea5988838..ec56ccda7 100644 --- a/docs/intro.md +++ b/docs/intro.md @@ -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 = ai { - promptMessage("Give me a selection of books about $topic") -}.getOrThrow() +dependencies { + implementation("com.xebia:xef-core:") + implementation("com.xebia:xef-openai:") +} ``` -> **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= +``` -This can be done by either writing an extension function on `AIScope`, or by using the form `AI`. -Let's compare the two: +> **Caution / Important**:
+>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 = - 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` within an `ai` block or an `AIScope`. - ## Structure The output from the `books` function above may be hard to parse back from the @@ -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) @Serializable data class Book(val title: String, val author: String) -suspend fun AIScope.books(topic: String): AI> = - 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 +) + +@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> { - val prompt = buildPrompt { - + "Give me a selection of books about the following topic:" - + topic +@Serializable +data class Books(val books: List) + +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, @@ -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 +) + +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= ``` -> **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 = -> contextScope(InMemoryLuceneBuilder(LUCENE_PATH)) { /* do stuff */ } -> ``` +>**Better vector stores**
+>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. diff --git a/examples/src/main/kotlin/com/xebia/functional/xef/conversation/sql/README.md b/examples/src/main/kotlin/com/xebia/functional/xef/conversation/sql/README.md index bae933276..3aecd6c5e 100644 --- a/examples/src/main/kotlin/com/xebia/functional/xef/conversation/sql/README.md +++ b/examples/src/main/kotlin/com/xebia/functional/xef/conversation/sql/README.md @@ -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= ./gradlew xef-kotlin-examples:run-sql-example` +`env OPENAI_TOKEN= ./gradlew xef-examples:run-sql-example` + +### Clean up the mysql container +`./gradlew xef-examples:docker-sql-example-down`