Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Scala examples refactor #514

Merged
merged 3 commits into from
Nov 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ in your build, if you haven't done it before.

## 📖 Quick Introduction

In this small introduction we look at the main features of xef, including the `ai` function.
In this small introduction we look at the main features of xef, including the `conversation` function.

- [<img src="https://upload.wikimedia.org/wikipedia/commons/3/37/Kotlin_Icon_2021.svg" height="15px" alt="Kotlin logo"> Kotlin version](https://github.com/xebia-functional/xef/blob/main/docs/intro/kotlin.md)
- [<img src="https://www.scala-lang.org/resources/img/frontpage/scala-spiral.png" height="15px" alt="Scala logo"> Scala version](https://github.com/xebia-functional/xef/blob/main/docs/intro/scala.md)
Expand All @@ -121,5 +121,5 @@ In this small introduction we look at the main features of xef, including the `a
You can also have a look at the examples to have a feeling of how using the library looks like.

- [<img src="https://upload.wikimedia.org/wikipedia/commons/3/37/Kotlin_Icon_2021.svg" height="15px" alt="Kotlin logo"> Examples in Kotlin](https://github.com/xebia-functional/xef/tree/main/examples/kotlin/src/main/kotlin/com/xebia/functional/xef/conversation)
- [<img src="https://www.scala-lang.org/resources/img/frontpage/scala-spiral.png" height="15px" alt="Scala logo"> Examples in Scala](https://github.com/xebia-functional/xef/tree/main/examples/scala/src/main/scala/com/xebia/functional/xef/scala/conversation)
- [<img src="https://www.scala-lang.org/resources/img/frontpage/scala-spiral.png" height="15px" alt="Scala logo"> Examples in Scala](https://github.com/xebia-functional/xef/tree/main/examples/scala/src/main/scala/com/xebia/functional/xef/examples/scala)
- [<img src="https://upload.wikimedia.org/wikipedia/en/thumb/3/30/Java_programming_language_logo.svg/234px-Java_programming_language_logo.svg.png" height="15px" alt="Java logo"> Examples in Java](https://github.com/xebia-functional/xef/tree/main/examples/java/src/main/java/com/xebia/functional/xef/java/auto)
123 changes: 50 additions & 73 deletions docs/intro/scala.md
Original file line number Diff line number Diff line change
@@ -1,51 +1,19 @@
# Quick introduction to xef.ai (Scala version)

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 gate to the modern AI world.
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 `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.
(Large Language Model). The easiest way is to just get the information back as a string or list of strings.

```scala
import com.xebia.functional.xef.scala.auto.*
```scala 3
import com.xebia.functional.xef.scala.conversation.*

@main def runBook: Unit = ai {
val topic: String = "functional programming"
promptMessage(s"Give me a selection of books about $topic")
}.getOrElse(ex => println(ex.getMessage))
```

In the example above we _execute_ the `ai` block with `getOrElse`, so in case an exception
is thrown (for example, if your API key is not correct), we are handing the error by printing
the reason of the error.

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.

This can be done by either using a context parameters or function _using_ `AIScope`.
Let's compare the two:

```scala
def book(topic: String)(using scope: AIScope): List[String] =
promptMessage(s"Give me a selection of books about $topic")

def book(topic: String): AIScope ?=> List[String] =
promptMessage(s"Give me a selection of books about $topic")
```

Using the type alias `AI`, defined in `com.xebia.functional.xef.scala.auto` as:

```scala
type AI[A] = AIScope ?=> A
```

book function can be written in this way:

```scala
def book(topic: String): AI[List[String]] =
promptMessage(s"Give me a selection of books about $topic")
def books(topic: String): Unit = conversation:
val topBook: String = promptMessage(s"Give me the top-selling book about $topic")
println(topBook)
val selectedBooks: List[String] = promptMessages(s"Give me a selection of books about $topic")
println(selectedBooks.mkString("\n"))
```

## Additional setup
Expand All @@ -54,7 +22,7 @@ If the code above fails, you may need to perform some additional setup.

### OpenAI

By default, the `ai` block connects to [OpenAI](https://platform.openai.com/).
By default, the `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.

Expand Down Expand Up @@ -102,28 +70,29 @@ strings we obtain. Fortunately, you can also ask xef.ai to give you back the inf
using a _custom type_. The library takes care of instructing the LLM on building such
a structure, and deserialize the result back for you.

```scala
import com.xebia.functional.xef.scala.auto.*
This can be done by declaring a case class that `derives SerialDescriptor, Decoder`:

```scala 3
import com.xebia.functional.xef.scala.conversation.*
import com.xebia.functional.xef.scala.serialization.*
import io.circe.Decoder
import io.circe.parser.decode

private final case class Book(name: String, author: String, summary: String) derives SerialDescriptor, Decoder
case class Book(name: String, author: String, pages: Int) derives SerialDescriptor, Decoder
```

def summarizeBook(title: String, author: String)(using scope: AIScope): Book =
prompt(s"$title by $author summary.")
The `conversation` block can then be written in this way:

@main def runBook: Unit =
ai {
val toKillAMockingBird = summarizeBook("To Kill a Mockingbird", "Harper Lee")
println(s"${toKillAMockingBird.name} by ${toKillAMockingBird.author} summary:\n ${toKillAMockingBird.summary}")
}.getOrElse(ex => println(ex.getMessage))
```scala 3
def bookExample(topic: String): Unit = conversation:
val Book(title, author, pages) = prompt[Book](s"Give me the best-selling book about $topic")
println(s"The book $title is by $author and has $pages pages.")
```

xef.ai for Scala uses xef.ai core, which it's based on Kotlin. Hence, the core
xef.ai for Scala uses xef.ai core, which is based on the Kotlin implementation. Hence, the core
reuses [Kotlin's common serialization](https://kotlinlang.org/docs/serialization.html), and
Scala uses [circe](https://github.com/circe/circe) to derive the required serializable instance.
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).
The LLM is usually able to detect which kind of information should go in each field based on its name
(like `title` and `author` above).

## Context

Expand All @@ -133,37 +102,45 @@ often want to supplement the LLM with more data:
- Transient information referring to the current moment, like the current weather, or
the trends in the stock market in the past 10 days.
- Non-public information, for example for summarizing a piece of text you're creating
within you organization.
within your organization.

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.
and make its response part of the context. One such agent is `search`, which uses the
[Google Search API (SerpApi)](https://serpapi.com/) to enrich that context.

(Note that a SerpApi token may be required to run this example.)

```scala 3
import com.xebia.functional.xef.conversation.llm.openai.*
import com.xebia.functional.xef.reasoning.serpapi.*
import com.xebia.functional.xef.scala.conversation.*
import com.xebia.functional.xef.scala.serialization.*
import io.circe.Decoder

```scala
import com.xebia.functional.xef.scala.agents.DefaultSearch
import com.xebia.functional.xef.scala.auto.*
val openAI: OpenAI = OpenAI.FromEnvironment

private def getQuestionAnswer(question: String)(using scope: AIScope): List[String] =
contextScope(DefaultSearch.search("Weather in Cádiz, Spain")) {
promptMessage(question)
}
def setContext(query: String)(using conversation: ScalaConversation): Unit =
addContext(Search(openAI.DEFAULT_CHAT, conversation, 3).search(query).get)

@main def runWeather: Unit = ai {
@main def runWeather(): Unit = conversation:
setContext("Weather in Cádiz, Spain")
val question = "Knowing this forecast, what clothes do you recommend I should wear if I live in Cádiz?"
println(getQuestionAnswer(question).mkString("\n"))
}.getOrElse(ex => println(ex.getMessage))
val answer = promptMessage(question)
println(answer)
```

> **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
> By default xef.ai uses an _in-memory_ vector store, since this 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.

## Examples

Check out the [examples folder](https://github.com/xebia-functional/xef/blob/main/examples/scala/auto) for a complete list of different use cases.
Check out the
[examples folder](https://github.com/xebia-functional/xef/blob/main/examples/scala/src/main/scala/com/xebia/functional/xef/examples)
for a complete list of different use cases.
2 changes: 1 addition & 1 deletion examples/scala/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ java {
dependencies {
implementation(projects.xefCore)
implementation(projects.xefScala)
implementation(projects.xefReasoning)
implementation(projects.xefReasoning)
implementation(projects.xefOpenai)
implementation(libs.circe.parser)
implementation(libs.scala.lang)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.xebia.functional.xef.examples.scala

import com.xebia.functional.xef.scala.conversation.*

@main def runBooks(): Unit = conversation:
val topic = "functional programming"
val topBook: String = promptMessage(s"Give me the top-selling book about $topic")
println(topBook)
val selectedBooks: List[String] = promptMessages(s"Give me a selection of books about $topic")
println(selectedBooks.mkString("\n"))
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package com.xebia.functional.xef.examples.scala.context.serpapi

import com.xebia.functional.xef.conversation.llm.openai.*
import com.xebia.functional.xef.reasoning.serpapi.*
import com.xebia.functional.xef.scala.conversation.*
import com.xebia.functional.xef.scala.serialization.*
import io.circe.Decoder

import java.text.SimpleDateFormat
import java.util.Date

val openAI: OpenAI = OpenAI.FromEnvironment

val sdf = SimpleDateFormat("dd/M/yyyy")
def currentDate: String = sdf.format(new Date)

def setContext(query: String)(using conversation: ScalaConversation): Unit =
addContext(Search(openAI.DEFAULT_CHAT, conversation, 3).search(query).get)

case class BreakingNews(summary: String) derives SerialDescriptor, Decoder

case class MarketNews(news: String, risingStockSymbols: List[String], fallingStockSymbols: List[String]) derives SerialDescriptor, Decoder

case class Estimate(number: Long) derives SerialDescriptor, Decoder

@main def runWeather(): Unit = conversation:
setContext("Weather in Cádiz, Spain")
val question = "Knowing this forecast, what clothes do you recommend I should wear if I live in Cádiz?"
val answer = promptMessage(question)
println(answer)

@main def runBreakingNews(): Unit = conversation:
setContext(s"$currentDate COVID News")
val BreakingNews(summary) = prompt[BreakingNews](s"Write a summary of about 300 words given the provided context.")
println(summary)

@main def runMarketNews(): Unit = conversation:
setContext(s"$currentDate Stock market results, rising stocks, falling stocks")
val news = prompt[MarketNews]("Write a short summary of the stock market results given the provided context.")
println(news)

@main def runFermiEstimate(): Unit = conversation:
setContext("Estimate the number of medical needles in the world")
val Estimate(needlesInWorld) = prompt[Estimate]("Answer the question with an integer number given the provided context.")
println(s"Needles in world: $needlesInWorld")
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.xebia.functional.xef.examples.scala.context.serpapi

import com.xebia.functional.xef.reasoning.pdf.PDF
import com.xebia.functional.xef.scala.conversation.*
import com.xebia.functional.xef.scala.serialization.*
import io.circe.Decoder

import scala.io.StdIn.readLine

case class AIResponse(answer: String) derives SerialDescriptor, Decoder

val PdfUrl = "https://people.cs.ksu.edu/~schmidt/705a/Scala/Programming-in-Scala.pdf"

@main def runUserQueries(): Unit = conversation:
val pdf = PDF(openAI.DEFAULT_CHAT, openAI.DEFAULT_SERIALIZATION, summon[ScalaConversation])
addContext(Array(pdf.readPDFFromUrl.readPDFFromUrl(PdfUrl).get))
while (true)
println("Enter your question: ")
val AIResponse(answer) = prompt[AIResponse](readLine())
println(s"$answer\n---\n")
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.xebia.functional.xef.examples.scala.images

import com.xebia.functional.xef.prompt.Prompt
import com.xebia.functional.xef.scala.conversation.*
import com.xebia.functional.xef.scala.serialization.*
import io.circe.Decoder

@main def runHybridCity(): Unit = conversation:
val imageUrls = images(Prompt("A hybrid city of Cádiz, Spain and Seattle, US."))
println(imageUrls)
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.xebia.functional.xef.examples.scala.iteration

import com.xebia.functional.xef.prompt.JvmPromptBuilder
import com.xebia.functional.xef.scala.conversation.*
import com.xebia.functional.xef.scala.serialization.*
import io.circe.Decoder

case class Animal(name: String, habitat: String, diet: String) derives SerialDescriptor, Decoder
case class Invention(name: String, inventor: String, year: Int, purpose: String) derives SerialDescriptor, Decoder

@main def runAnimalStory(): Unit = conversation:
val animal = prompt[Animal]("A unique animal species")
val invention = prompt[Invention]("A groundbreaking invention from the 20th century.")
println(s"Animal: $animal")
println(s"Invention: $invention")
val builder = new JvmPromptBuilder()
.addSystemMessage("You are a writer for a science fiction magazine.")
.addUserMessage("Write a short story of 200 words that involves the animal and the invention.")
val story = promptMessage(builder.build)
println(story)
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.xebia.functional.xef.examples.scala.iteration

import com.xebia.functional.xef.prompt.Prompt
import com.xebia.functional.xef.scala.conversation.*
import com.xebia.functional.xef.scala.serialization.*
import io.circe.Decoder

import scala.annotation.tailrec

case class ChessMove(player: String, move: String) derives SerialDescriptor, Decoder
case class ChessBoard(board: String) derives SerialDescriptor, Decoder
case class GameState(ended: Boolean = false, winner: Option[String] = None) derives SerialDescriptor, Decoder

@tailrec
private def chessGame(moves: List[ChessMove] = Nil, gameState: GameState = new GameState)(using ScalaConversation): (String, ChessMove) =
if !gameState.ended then
val currentPlayer = if moves.size % 2 == 0 then "Player 1 (White)" else "Player 2 (Black)"
val previousMoves = moves.map(m => m.player + ":" + m.move).mkString(", ")
val movePrompt = moves match {
case Nil => s"""
|$currentPlayer, you are playing chess and it's your turn.
|Make your first move:
""".stripMargin
case l => s"""
|$currentPlayer, you are playing chess and it's your turn.
|Here are the previous moves: $previousMoves
|Make your next move:
""".stripMargin
}
println(movePrompt)
val move = prompt[ChessMove](movePrompt)
println(s"Move is: $move")
val boardPrompt =
s"""
|Given the following chess moves: $previousMoves,
|generate a chess board on a table with appropriate emoji representations for each move and piece.
|Add a brief description of the move and its implications.
""".stripMargin
val chessBoard = prompt[ChessBoard](boardPrompt)
println(s"Current board:\n${chessBoard.board}")
val gameStatePrompt =
s"""
|Given the following chess moves: ${moves.mkString(", ")},
|has the game ended (win, draw, or stalemate)?
""".stripMargin
val gameState = prompt[GameState](gameStatePrompt)
chessGame(moves :+ move, gameState)
else (gameState.winner.getOrElse("Something went wrong"), moves.last)

@main def runChessGame(): Unit = conversation:
val (winner, fMove) = chessGame()
println(s"Game over. Final move: $fMove, Winner: $winner")
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.xebia.functional.xef.examples.scala.serialization

import com.xebia.functional.xef.prompt.Prompt
import com.xebia.functional.xef.scala.conversation.*
import com.xebia.functional.xef.scala.serialization.*
import io.circe.Decoder

@Description("A book")
case class AnnotatedBook(
@Description("The name of the book") name: String,
@Description("The author of the book") author: String,
@Description("A 50 word paragraph with a summary of this book") summary: String
) derives SerialDescriptor,
Decoder

@main def runAnnotatedBook(): Unit = conversation:
val book = prompt[AnnotatedBook]("To Kill a Mockingbird")
println(book)
Loading