From 1ddcfb1e1993004e1a1417c7b1d923f192e94a67 Mon Sep 17 00:00:00 2001 From: Lawrence Lavigne Date: Wed, 1 Nov 2023 06:47:57 -0700 Subject: [PATCH] Scala examples refactor (#514) --- README.md | 4 +- docs/intro/scala.md | 123 +++++++----------- examples/scala/build.gradle.kts | 2 +- .../xef/examples/scala/Simple.scala | 10 ++ .../scala/context/serpapi/Simple.scala | 45 +++++++ .../scala/context/serpapi/UserQueries.scala | 20 +++ .../examples/scala/images/HybridCity.scala | 10 ++ .../scala/iteration/AnimalStory.scala | 20 +++ .../examples/scala/iteration/ChessGame.scala | 52 ++++++++ .../scala/serialization/Annotated.scala | 18 +++ .../examples/scala/serialization/Simple.scala | 37 ++++++ .../conversation/contexts/BreakingNews.scala | 22 ---- .../contexts/DivergentTasks.scala | 17 --- .../scala/conversation/contexts/Markets.scala | 24 ---- .../conversation/contexts/PDFDocument.scala | 25 ---- .../scala/conversation/contexts/Weather.scala | 15 --- .../conversation/conversations/Animal.scala | 26 ---- .../xef/scala/conversation/fields/Book.scala | 22 ---- .../scala/conversation/image/Population.scala | 19 --- .../conversation/serialization/ASCIIArt.scala | 13 -- .../conversation/serialization/ChessAI.scala | 63 --------- .../conversation/serialization/Movie.scala | 13 -- .../conversation/serialization/Recipe.scala | 13 -- gradle.properties | 2 +- scala/README.md | 9 +- .../conversation/KotlinXSerializers.scala | 38 ------ .../xef/scala/conversation/package.scala | 83 +++++------- .../serialization/KotlinXSerializers.scala | 18 +++ .../SerialDescriptor.scala | 30 ++--- .../SerialDescriptorInstances.scala | 2 +- .../SerialDescriptorSpec.scala | 2 +- 31 files changed, 334 insertions(+), 463 deletions(-) create mode 100644 examples/scala/src/main/scala/com/xebia/functional/xef/examples/scala/Simple.scala create mode 100644 examples/scala/src/main/scala/com/xebia/functional/xef/examples/scala/context/serpapi/Simple.scala create mode 100644 examples/scala/src/main/scala/com/xebia/functional/xef/examples/scala/context/serpapi/UserQueries.scala create mode 100644 examples/scala/src/main/scala/com/xebia/functional/xef/examples/scala/images/HybridCity.scala create mode 100644 examples/scala/src/main/scala/com/xebia/functional/xef/examples/scala/iteration/AnimalStory.scala create mode 100644 examples/scala/src/main/scala/com/xebia/functional/xef/examples/scala/iteration/ChessGame.scala create mode 100644 examples/scala/src/main/scala/com/xebia/functional/xef/examples/scala/serialization/Annotated.scala create mode 100644 examples/scala/src/main/scala/com/xebia/functional/xef/examples/scala/serialization/Simple.scala delete mode 100644 examples/scala/src/main/scala/com/xebia/functional/xef/scala/conversation/contexts/BreakingNews.scala delete mode 100644 examples/scala/src/main/scala/com/xebia/functional/xef/scala/conversation/contexts/DivergentTasks.scala delete mode 100644 examples/scala/src/main/scala/com/xebia/functional/xef/scala/conversation/contexts/Markets.scala delete mode 100644 examples/scala/src/main/scala/com/xebia/functional/xef/scala/conversation/contexts/PDFDocument.scala delete mode 100644 examples/scala/src/main/scala/com/xebia/functional/xef/scala/conversation/contexts/Weather.scala delete mode 100644 examples/scala/src/main/scala/com/xebia/functional/xef/scala/conversation/conversations/Animal.scala delete mode 100644 examples/scala/src/main/scala/com/xebia/functional/xef/scala/conversation/fields/Book.scala delete mode 100644 examples/scala/src/main/scala/com/xebia/functional/xef/scala/conversation/image/Population.scala delete mode 100644 examples/scala/src/main/scala/com/xebia/functional/xef/scala/conversation/serialization/ASCIIArt.scala delete mode 100644 examples/scala/src/main/scala/com/xebia/functional/xef/scala/conversation/serialization/ChessAI.scala delete mode 100644 examples/scala/src/main/scala/com/xebia/functional/xef/scala/conversation/serialization/Movie.scala delete mode 100644 examples/scala/src/main/scala/com/xebia/functional/xef/scala/conversation/serialization/Recipe.scala delete mode 100644 scala/src/main/scala/com/xebia/functional/xef/scala/conversation/KotlinXSerializers.scala create mode 100644 scala/src/main/scala/com/xebia/functional/xef/scala/serialization/KotlinXSerializers.scala rename scala/src/main/scala/com/xebia/functional/xef/scala/{conversation => serialization}/SerialDescriptor.scala (94%) rename scala/src/main/scala/com/xebia/functional/xef/scala/{conversation => serialization}/SerialDescriptorInstances.scala (98%) rename scala/src/test/scala/com/xebia/functional/xef/scala/{conversation => serialization}/SerialDescriptorSpec.scala (98%) diff --git a/README.md b/README.md index fc9f1c63b..bbbb5af8a 100644 --- a/README.md +++ b/README.md @@ -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. - [Kotlin logo Kotlin version](https://github.com/xebia-functional/xef/blob/main/docs/intro/kotlin.md) - [Scala logo Scala version](https://github.com/xebia-functional/xef/blob/main/docs/intro/scala.md) @@ -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. - [Kotlin logo Examples in Kotlin](https://github.com/xebia-functional/xef/tree/main/examples/kotlin/src/main/kotlin/com/xebia/functional/xef/conversation) -- [Scala logo Examples in Scala](https://github.com/xebia-functional/xef/tree/main/examples/scala/src/main/scala/com/xebia/functional/xef/scala/conversation) +- [Scala logo Examples in Scala](https://github.com/xebia-functional/xef/tree/main/examples/scala/src/main/scala/com/xebia/functional/xef/examples/scala) - [Java logo Examples in Java](https://github.com/xebia-functional/xef/tree/main/examples/java/src/main/java/com/xebia/functional/xef/java/auto) diff --git a/docs/intro/scala.md b/docs/intro/scala.md index ca47d3c19..dd5be1a56 100644 --- a/docs/intro/scala.md +++ b/docs/intro/scala.md @@ -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 @@ -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. @@ -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 @@ -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. diff --git a/examples/scala/build.gradle.kts b/examples/scala/build.gradle.kts index 3ce0f1895..a88198b95 100644 --- a/examples/scala/build.gradle.kts +++ b/examples/scala/build.gradle.kts @@ -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) diff --git a/examples/scala/src/main/scala/com/xebia/functional/xef/examples/scala/Simple.scala b/examples/scala/src/main/scala/com/xebia/functional/xef/examples/scala/Simple.scala new file mode 100644 index 000000000..c9f4b17d1 --- /dev/null +++ b/examples/scala/src/main/scala/com/xebia/functional/xef/examples/scala/Simple.scala @@ -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")) diff --git a/examples/scala/src/main/scala/com/xebia/functional/xef/examples/scala/context/serpapi/Simple.scala b/examples/scala/src/main/scala/com/xebia/functional/xef/examples/scala/context/serpapi/Simple.scala new file mode 100644 index 000000000..fbe40da7e --- /dev/null +++ b/examples/scala/src/main/scala/com/xebia/functional/xef/examples/scala/context/serpapi/Simple.scala @@ -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") diff --git a/examples/scala/src/main/scala/com/xebia/functional/xef/examples/scala/context/serpapi/UserQueries.scala b/examples/scala/src/main/scala/com/xebia/functional/xef/examples/scala/context/serpapi/UserQueries.scala new file mode 100644 index 000000000..711db19da --- /dev/null +++ b/examples/scala/src/main/scala/com/xebia/functional/xef/examples/scala/context/serpapi/UserQueries.scala @@ -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") diff --git a/examples/scala/src/main/scala/com/xebia/functional/xef/examples/scala/images/HybridCity.scala b/examples/scala/src/main/scala/com/xebia/functional/xef/examples/scala/images/HybridCity.scala new file mode 100644 index 000000000..1c9a3b4a9 --- /dev/null +++ b/examples/scala/src/main/scala/com/xebia/functional/xef/examples/scala/images/HybridCity.scala @@ -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) diff --git a/examples/scala/src/main/scala/com/xebia/functional/xef/examples/scala/iteration/AnimalStory.scala b/examples/scala/src/main/scala/com/xebia/functional/xef/examples/scala/iteration/AnimalStory.scala new file mode 100644 index 000000000..19e6d433e --- /dev/null +++ b/examples/scala/src/main/scala/com/xebia/functional/xef/examples/scala/iteration/AnimalStory.scala @@ -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) diff --git a/examples/scala/src/main/scala/com/xebia/functional/xef/examples/scala/iteration/ChessGame.scala b/examples/scala/src/main/scala/com/xebia/functional/xef/examples/scala/iteration/ChessGame.scala new file mode 100644 index 000000000..42472e1a0 --- /dev/null +++ b/examples/scala/src/main/scala/com/xebia/functional/xef/examples/scala/iteration/ChessGame.scala @@ -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") diff --git a/examples/scala/src/main/scala/com/xebia/functional/xef/examples/scala/serialization/Annotated.scala b/examples/scala/src/main/scala/com/xebia/functional/xef/examples/scala/serialization/Annotated.scala new file mode 100644 index 000000000..168599987 --- /dev/null +++ b/examples/scala/src/main/scala/com/xebia/functional/xef/examples/scala/serialization/Annotated.scala @@ -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) diff --git a/examples/scala/src/main/scala/com/xebia/functional/xef/examples/scala/serialization/Simple.scala b/examples/scala/src/main/scala/com/xebia/functional/xef/examples/scala/serialization/Simple.scala new file mode 100644 index 000000000..2652fd8a3 --- /dev/null +++ b/examples/scala/src/main/scala/com/xebia/functional/xef/examples/scala/serialization/Simple.scala @@ -0,0 +1,37 @@ +package com.xebia.functional.xef.examples.scala.serialization + +import com.xebia.functional.xef.scala.conversation.* +import com.xebia.functional.xef.scala.serialization.* +import io.circe.Decoder + +case class AsciiArt(art: String) derives SerialDescriptor, Decoder + +case class Book(title: String, author: String, pages: Int) derives SerialDescriptor, Decoder + +case class City(population: Int, description: String) derives SerialDescriptor, Decoder + +case class Movie(title: String, genre: String, director: String) derives SerialDescriptor, Decoder + +case class Recipe(name: String, ingredients: List[String]) derives SerialDescriptor, Decoder + +@main def runAsciiArt(): Unit = conversation: + val AsciiArt(art) = prompt[AsciiArt]("ASCII art of a cat dancing") + println(art) + +@main def runBook(): Unit = conversation: + val topic = "functional programming" + 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.") + +@main def runMovie(): Unit = conversation: + val Movie(title, genre, director) = prompt[Movie]("Inception movie genre and director.") + println(s"The movie $title is a $genre film directed by $director.") + +@main def runCities(): Unit = conversation: + val cadiz = prompt[City]("Cádiz, Spain") + val seattle = prompt[City]("Seattle, WA") + println(s"The population of Cádiz is ${cadiz.population} and the population of Seattle is ${seattle.population}.") + +@main def runRecipe(): Unit = conversation: + val Recipe(name, ingredients) = prompt[Recipe]("Recipe for chocolate chip cookies.") + println(s"The recipe for $name is $ingredients.") diff --git a/examples/scala/src/main/scala/com/xebia/functional/xef/scala/conversation/contexts/BreakingNews.scala b/examples/scala/src/main/scala/com/xebia/functional/xef/scala/conversation/contexts/BreakingNews.scala deleted file mode 100644 index 10316ae6d..000000000 --- a/examples/scala/src/main/scala/com/xebia/functional/xef/scala/conversation/contexts/BreakingNews.scala +++ /dev/null @@ -1,22 +0,0 @@ -package com.xebia.functional.xef.scala.conversation.contexts - -import com.xebia.functional.xef.conversation.llm.openai.OpenAI -import com.xebia.functional.xef.prompt.Prompt -import com.xebia.functional.xef.reasoning.serpapi.Search -import com.xebia.functional.xef.scala.conversation.* -import io.circe.Decoder - -import java.text.SimpleDateFormat -import java.util.Date - -private final case class BreakingNewsAboutCovid(summary: String) derives SerialDescriptor, Decoder - -@main def runBreakingNews: Unit = - conversation { - val sdf = SimpleDateFormat("dd/M/yyyy") - val currentDate = sdf.format(Date()) - val search = Search(OpenAI.FromEnvironment.DEFAULT_CHAT, summon[ScalaConversation], 3) - addContext(search.search(s"$currentDate Covid News").get()) - val news = prompt[BreakingNewsAboutCovid](Prompt(s"Write a paragraph of about 300 words about: $currentDate Covid News")) - println(news.summary) - } diff --git a/examples/scala/src/main/scala/com/xebia/functional/xef/scala/conversation/contexts/DivergentTasks.scala b/examples/scala/src/main/scala/com/xebia/functional/xef/scala/conversation/contexts/DivergentTasks.scala deleted file mode 100644 index bd842f932..000000000 --- a/examples/scala/src/main/scala/com/xebia/functional/xef/scala/conversation/contexts/DivergentTasks.scala +++ /dev/null @@ -1,17 +0,0 @@ -package com.xebia.functional.xef.scala.conversation.contexts - -import com.xebia.functional.xef.conversation.llm.openai.OpenAI -import com.xebia.functional.xef.prompt.Prompt -import com.xebia.functional.xef.reasoning.serpapi.Search -import com.xebia.functional.xef.scala.conversation.* -import io.circe.Decoder - -private final case class NumberOfMedicalNeedlesInWorld(numberOfNeedles: Long) derives SerialDescriptor, Decoder - -@main def runDivergentTasks: Unit = - conversation { - val search = Search(OpenAI.FromEnvironment.DEFAULT_CHAT, summon[ScalaConversation], 3) - addContext(search.search("Estimate amount of medical needles in the world").get()) - val needlesInWorld = prompt[NumberOfMedicalNeedlesInWorld](Prompt("Provide the number of medical needles in the world as an integer number")) - println(s"Needles in world: ${needlesInWorld.numberOfNeedles}") - } diff --git a/examples/scala/src/main/scala/com/xebia/functional/xef/scala/conversation/contexts/Markets.scala b/examples/scala/src/main/scala/com/xebia/functional/xef/scala/conversation/contexts/Markets.scala deleted file mode 100644 index 3eb8a0814..000000000 --- a/examples/scala/src/main/scala/com/xebia/functional/xef/scala/conversation/contexts/Markets.scala +++ /dev/null @@ -1,24 +0,0 @@ -package com.xebia.functional.xef.scala.conversation.contexts - -import com.xebia.functional.xef.conversation.llm.openai.OpenAI -import com.xebia.functional.xef.prompt.Prompt -import com.xebia.functional.xef.reasoning.serpapi.Search -import com.xebia.functional.xef.scala.conversation.* -import io.circe.Decoder - -import java.text.SimpleDateFormat -import java.util.Date - -private final case class MarketNews(news: String, raisingStockSymbols: List[String], decreasingStockSymbols: List[String]) - derives SerialDescriptor, - Decoder - -@main def runMarkets: Unit = - conversation { - val sdf = SimpleDateFormat("dd/M/yyyy") - val currentDate = sdf.format(Date()) - val search = Search(OpenAI.FromEnvironment.DEFAULT_CHAT, summon[ScalaConversation], 3) - addContext(search.search(s"$currentDate Stock market results, raising stocks, decreasing stocks").get()) - val news = prompt[MarketNews](Prompt("Write a short summary of the stock market results given the provided context.")) - println(news) - } diff --git a/examples/scala/src/main/scala/com/xebia/functional/xef/scala/conversation/contexts/PDFDocument.scala b/examples/scala/src/main/scala/com/xebia/functional/xef/scala/conversation/contexts/PDFDocument.scala deleted file mode 100644 index 2aa1d5bf8..000000000 --- a/examples/scala/src/main/scala/com/xebia/functional/xef/scala/conversation/contexts/PDFDocument.scala +++ /dev/null @@ -1,25 +0,0 @@ -package com.xebia.functional.xef.scala.conversation.contexts - -import com.xebia.functional.xef.conversation.llm.openai.OpenAI -import com.xebia.functional.xef.prompt.Prompt -import com.xebia.functional.xef.reasoning.pdf.PDF -import com.xebia.functional.xef.scala.conversation.* -import io.circe.Decoder - -import scala.io.StdIn.readLine - -private final case class AIResponse(answer: String) derives SerialDescriptor, Decoder - -val pdfUrl = "https://people.cs.ksu.edu/~schmidt/705a/Scala/Programming-in-Scala.pdf" - -@main def runPDFDocument: Unit = - conversation { - val pdf = PDF(OpenAI.FromEnvironment.DEFAULT_CHAT, OpenAI.FromEnvironment.DEFAULT_SERIALIZATION, summon[ScalaConversation]) - addContext(Array(pdf.readPDFFromUrl.readPDFFromUrl(pdfUrl).get())) - while (true) { - println("Enter your question: ") - val line = scala.io.StdIn.readLine() - val response = prompt[AIResponse](Prompt(line)) - println(s"${response.answer}\n---\n") - } - } diff --git a/examples/scala/src/main/scala/com/xebia/functional/xef/scala/conversation/contexts/Weather.scala b/examples/scala/src/main/scala/com/xebia/functional/xef/scala/conversation/contexts/Weather.scala deleted file mode 100644 index 453700db7..000000000 --- a/examples/scala/src/main/scala/com/xebia/functional/xef/scala/conversation/contexts/Weather.scala +++ /dev/null @@ -1,15 +0,0 @@ -package com.xebia.functional.xef.scala.conversation - -import com.xebia.functional.xef.scala.conversation.* -import com.xebia.functional.xef.conversation.llm.openai.OpenAI -import com.xebia.functional.xef.reasoning.serpapi.Search -import com.xebia.functional.xef.prompt.Prompt - -private def getQuestionAnswer(question: Prompt)(using conversation: ScalaConversation): String = - val search: Search = Search(OpenAI.FromEnvironment.DEFAULT_CHAT, conversation, 3) - addContext(search.search("Weather in Cádiz, Spain").get()) - promptMessage(question) - -@main def runWeather: Unit = - val question = Prompt("Knowing this forecast, what clothes do you recommend I should wear if I live in Cádiz?") - println(conversation(getQuestionAnswer(question)).mkString("\n")) diff --git a/examples/scala/src/main/scala/com/xebia/functional/xef/scala/conversation/conversations/Animal.scala b/examples/scala/src/main/scala/com/xebia/functional/xef/scala/conversation/conversations/Animal.scala deleted file mode 100644 index 4f3405bb2..000000000 --- a/examples/scala/src/main/scala/com/xebia/functional/xef/scala/conversation/conversations/Animal.scala +++ /dev/null @@ -1,26 +0,0 @@ -package com.xebia.functional.xef.scala.conversation.conversations - -import com.xebia.functional.xef.prompt.{JvmPromptBuilder, Prompt} -import com.xebia.functional.xef.scala.conversation.* -import io.circe.Decoder - -private final case class Animal(name: String, habitat: String, diet: String) derives SerialDescriptor, Decoder - -private final case class Invention(name: String, inventor: String, year: Int, purpose: String) derives SerialDescriptor, Decoder - -@main def runAnimal: Unit = - conversation { - val animal: Animal = prompt(Prompt("A unique animal species")) - val invention: Invention = prompt(Prompt("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) - } diff --git a/examples/scala/src/main/scala/com/xebia/functional/xef/scala/conversation/fields/Book.scala b/examples/scala/src/main/scala/com/xebia/functional/xef/scala/conversation/fields/Book.scala deleted file mode 100644 index 15f278c5f..000000000 --- a/examples/scala/src/main/scala/com/xebia/functional/xef/scala/conversation/fields/Book.scala +++ /dev/null @@ -1,22 +0,0 @@ -package com.xebia.functional.xef.scala.conversation.fields - -import com.xebia.functional.xef.prompt.Prompt -import com.xebia.functional.xef.scala.conversation.* -import io.circe.Decoder - -@Description("A book") -case class Book( - @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 - -def summarizeBook(title: String, author: String)(using conversation: ScalaConversation): Book = - prompt(Prompt(s"$title by $author summary.")) - -@main def runBook: Unit = - conversation { - val toKillAMockingBird = summarizeBook("To Kill a Mockingbird", "Harper Lee") - println(s"${toKillAMockingBird.name} by ${toKillAMockingBird.author} summary:\n ${toKillAMockingBird.summary}") - } diff --git a/examples/scala/src/main/scala/com/xebia/functional/xef/scala/conversation/image/Population.scala b/examples/scala/src/main/scala/com/xebia/functional/xef/scala/conversation/image/Population.scala deleted file mode 100644 index 62dc2b1c4..000000000 --- a/examples/scala/src/main/scala/com/xebia/functional/xef/scala/conversation/image/Population.scala +++ /dev/null @@ -1,19 +0,0 @@ -package com.xebia.functional.xef.scala.conversation.image - -import com.xebia.functional.xef.llm.models.images.ImagesGenerationResponse -import com.xebia.functional.xef.prompt.Prompt -import com.xebia.functional.xef.scala.conversation.* -import io.circe.Decoder - -private final case class Population(size: Int, description: String) derives SerialDescriptor, Decoder - -private final case class Image(description: String, url: String) derives SerialDescriptor, Decoder - -@main def runPopulation: Unit = - conversation { - val cadiz: Population = prompt(Prompt("Population of Cádiz, Spain.")) - val seattle: Population = prompt(Prompt("Population of Seattle, WA.")) - val imgs: ImagesGenerationResponse = images(Prompt("A hybrid city of Cádiz, Spain and Seattle, US.")) - println(imgs) - println(s"The population of Cádiz is ${cadiz.size} and the population of Seattle is ${seattle.size}") - } diff --git a/examples/scala/src/main/scala/com/xebia/functional/xef/scala/conversation/serialization/ASCIIArt.scala b/examples/scala/src/main/scala/com/xebia/functional/xef/scala/conversation/serialization/ASCIIArt.scala deleted file mode 100644 index ea963b93c..000000000 --- a/examples/scala/src/main/scala/com/xebia/functional/xef/scala/conversation/serialization/ASCIIArt.scala +++ /dev/null @@ -1,13 +0,0 @@ -package com.xebia.functional.xef.scala.conversation.serialization - -import com.xebia.functional.xef.prompt.Prompt -import com.xebia.functional.xef.scala.conversation.* -import io.circe.Decoder - -private final case class ASCIIArt(art: String) derives SerialDescriptor, Decoder - -@main def runASCIIArt: Unit = - lazy val asciiArt = conversation { - prompt[ASCIIArt](Prompt("ASCII art of a cat dancing")) - } - println(asciiArt.art) diff --git a/examples/scala/src/main/scala/com/xebia/functional/xef/scala/conversation/serialization/ChessAI.scala b/examples/scala/src/main/scala/com/xebia/functional/xef/scala/conversation/serialization/ChessAI.scala deleted file mode 100644 index d4dde7858..000000000 --- a/examples/scala/src/main/scala/com/xebia/functional/xef/scala/conversation/serialization/ChessAI.scala +++ /dev/null @@ -1,63 +0,0 @@ -package com.xebia.functional.xef.scala.conversation.serialization - -import com.xebia.functional.xef.prompt.Prompt -import com.xebia.functional.xef.scala.conversation.* -import io.circe.Decoder - -import scala.annotation.tailrec - -private final case class ChessMove(player: String, move: String) derives SerialDescriptor, Decoder - -private final case class ChessBoard(board: String) derives SerialDescriptor, Decoder - -private final case class GameState(ended: Boolean, winner: Option[String]) derives SerialDescriptor, Decoder - -@tailrec -private def chessGame(moves: List[ChessMove], gameState: GameState)(using conversation: 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. - |These are no previous chess board moves. - |Make your first move: - """.stripMargin - case l => s""" - |$currentPlayer, you are playing chess and it's your turn. - |These are the previous chess board moves: $previousMoves - |Make your next move: - """.stripMargin - } - println(movePrompt) - val move: ChessMove = prompt(Prompt(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 it's implications - """.stripMargin - - val chessBoard: ChessBoard = prompt(Prompt(boardPrompt)) - println(s"Current board:\n${chessBoard.board}") - - val gameStatePrompt = - Prompt(s""" - |Given the following chess moves: ${moves.mkString(", ")}, - |has the game ended (win, draw, or stalemate)? - """.stripMargin) - - val gameState: GameState = prompt(gameStatePrompt) - - chessGame(moves :+ move, gameState) - else (gameState.winner.getOrElse("Something went wrong"), moves.last) - -@main def runChess: Unit = - conversation { - val (winner, fMove) = chessGame(Nil, GameState(false, None)) - println(s"Game over. Final move: $fMove, Winner: $winner") - } diff --git a/examples/scala/src/main/scala/com/xebia/functional/xef/scala/conversation/serialization/Movie.scala b/examples/scala/src/main/scala/com/xebia/functional/xef/scala/conversation/serialization/Movie.scala deleted file mode 100644 index 6eb070589..000000000 --- a/examples/scala/src/main/scala/com/xebia/functional/xef/scala/conversation/serialization/Movie.scala +++ /dev/null @@ -1,13 +0,0 @@ -package com.xebia.functional.xef.scala.conversation.serialization - -import com.xebia.functional.xef.prompt.Prompt -import com.xebia.functional.xef.scala.conversation.* -import io.circe.Decoder - -private final case class Movie(title: String, genre: String, director: String) derives SerialDescriptor, Decoder - -@main def runMovie: Unit = - conversation { - val movie = prompt[Movie](Prompt("Inception movie genre and director.")) - println(s"The movie ${movie.title} is a ${movie.genre} film directed by ${movie.director}.") - } diff --git a/examples/scala/src/main/scala/com/xebia/functional/xef/scala/conversation/serialization/Recipe.scala b/examples/scala/src/main/scala/com/xebia/functional/xef/scala/conversation/serialization/Recipe.scala deleted file mode 100644 index f548c49f2..000000000 --- a/examples/scala/src/main/scala/com/xebia/functional/xef/scala/conversation/serialization/Recipe.scala +++ /dev/null @@ -1,13 +0,0 @@ -package com.xebia.functional.xef.scala.conversation.serialization - -import com.xebia.functional.xef.prompt.Prompt -import com.xebia.functional.xef.scala.conversation.* -import io.circe.Decoder - -private final case class Recipe(name: String, ingredients: List[String]) derives SerialDescriptor, Decoder - -@main def runRecipe: Unit = - conversation { - val recipe: Recipe = prompt(Prompt("Recipe for chocolate chip cookies.")) - println(s"The recipe for ${recipe.name} is ${recipe.ingredients}") - } diff --git a/gradle.properties b/gradle.properties index 9400b3a33..a0979c287 100644 --- a/gradle.properties +++ b/gradle.properties @@ -22,4 +22,4 @@ systemProp.org.gradle.unsafe.kotlin.assignment=true # Workaround to disable Dokka setup from Arrow Gradle Config dokkaEnabled=false -scalaVersion=3.3.0 +scalaVersion=3.3.1 diff --git a/scala/README.md b/scala/README.md index 611295003..c04cd2a66 100644 --- a/scala/README.md +++ b/scala/README.md @@ -9,7 +9,8 @@ Build the project locally, from the project root: ## Scalafmt The Scala module uses the [spotless](https://github.com/diffplug/spotless/tree/main/plugin-gradle#scala) plugin. -Therefore, the previous command (`./gradlew build`) will fail in case there is any formatting issue. To apply format, you can run the following command: +Therefore, the previous command (`./gradlew build`) will fail if there are any formatting issues. +To apply formatting, you can run the following command: ```bash ./gradlew spotlessApply @@ -23,6 +24,6 @@ Check out some use case at the [Scala examples](../examples/scala) folder. How to run the examples (from IntelliJ IDEA): -* Set Java version 19 -* Set VM options: "--enable-preview" -* Set Env variable: "OPENAI_TOKEN=xxx" +* Set Java version 20 +* Set VM options: `--enable-preview` +* Set Env variable: `OPENAI_TOKEN=xxx` diff --git a/scala/src/main/scala/com/xebia/functional/xef/scala/conversation/KotlinXSerializers.scala b/scala/src/main/scala/com/xebia/functional/xef/scala/conversation/KotlinXSerializers.scala deleted file mode 100644 index cd57424c2..000000000 --- a/scala/src/main/scala/com/xebia/functional/xef/scala/conversation/KotlinXSerializers.scala +++ /dev/null @@ -1,38 +0,0 @@ -package com.xebia.functional.xef.scala.conversation - -import kotlinx.serialization.KSerializer -import kotlinx.serialization.builtins.BuiltinSerializersKt -import kotlinx.serialization.builtins.BuiltinSerializersKt.serializer - -import java.lang - -object KotlinXSerializers: - val int: KSerializer[Integer] = - serializer(kotlin.jvm.internal.IntCompanionObject.INSTANCE) - - val string: KSerializer[String] = - serializer(kotlin.jvm.internal.StringCompanionObject.INSTANCE) - - val boolean: KSerializer[lang.Boolean] = - serializer(kotlin.jvm.internal.BooleanCompanionObject.INSTANCE) - - val double: KSerializer[lang.Double] = - serializer(kotlin.jvm.internal.DoubleCompanionObject.INSTANCE) - - val float: KSerializer[lang.Float] = - serializer(kotlin.jvm.internal.FloatCompanionObject.INSTANCE) - - val long: KSerializer[lang.Long] = - serializer(kotlin.jvm.internal.LongCompanionObject.INSTANCE) - - val short: KSerializer[lang.Short] = - serializer(kotlin.jvm.internal.ShortCompanionObject.INSTANCE) - - val byte: KSerializer[lang.Byte] = - serializer(kotlin.jvm.internal.ByteCompanionObject.INSTANCE) - - val char: KSerializer[Character] = - serializer(kotlin.jvm.internal.CharCompanionObject.INSTANCE) - - val unit: KSerializer[kotlin.Unit] = - serializer(kotlin.Unit.INSTANCE) diff --git a/scala/src/main/scala/com/xebia/functional/xef/scala/conversation/package.scala b/scala/src/main/scala/com/xebia/functional/xef/scala/conversation/package.scala index 7f4d107c6..d65b83efa 100644 --- a/scala/src/main/scala/com/xebia/functional/xef/scala/conversation/package.scala +++ b/scala/src/main/scala/com/xebia/functional/xef/scala/conversation/package.scala @@ -1,84 +1,65 @@ package com.xebia.functional.xef.scala.conversation +import com.xebia.functional.xef.conversation.* import com.xebia.functional.xef.conversation.llm.openai.* -import com.xebia.functional.xef.prompt.Prompt -import com.xebia.functional.xef.conversation.{FromJson, JVMConversation} +import com.xebia.functional.xef.conversation.llm.openai.OpenAI.FromEnvironment.* import com.xebia.functional.xef.llm.* import com.xebia.functional.xef.llm.models.images.* -import com.xebia.functional.xef.store.{ConversationId, LocalVectorStore, VectorStore} -import com.xebia.functional.xef.metrics.{LogsMetric, Metric} +import com.xebia.functional.xef.metrics.* +import com.xebia.functional.xef.prompt.Prompt +import com.xebia.functional.xef.scala.serialization.* +import com.xebia.functional.xef.store.* import io.circe.Decoder import io.circe.parser.parse -import org.reactivestreams.{Subscriber, Subscription} +import org.reactivestreams.* -import java.util -import java.util.UUID +import java.util.UUID.* import java.util.concurrent.LinkedBlockingQueue import scala.jdk.CollectionConverters.* -class ScalaConversation(store: VectorStore, metric: Metric, conversationId: Option[ConversationId]) - extends JVMConversation(store, metric, conversationId.orNull) +class ScalaConversation(store: VectorStore, metric: Metric, conversationId: ConversationId) extends JVMConversation(store, metric, conversationId) def addContext(context: Array[String])(using conversation: ScalaConversation): Unit = conversation.addContextFromArray(context).join() -def prompt[A: Decoder: SerialDescriptor]( - prompt: Prompt, - chat: ChatWithFunctions = OpenAI.FromEnvironment.DEFAULT_SERIALIZATION -)(using - conversation: ScalaConversation -): A = - val fromJson = new FromJson[A] { - def fromJson(json: String): A = - parse(json).flatMap(Decoder[A].decodeJson(_)).fold(throw _, identity) - } +def prompt[A: Decoder: SerialDescriptor](prompt: Prompt, chat: ChatWithFunctions = DEFAULT_SERIALIZATION)(using conversation: ScalaConversation): A = conversation.prompt(chat, prompt, chat.chatFunction(SerialDescriptor[A].serialDescriptor), fromJson).join() -def promptMessage( - prompt: Prompt, - chat: Chat = OpenAI.FromEnvironment.DEFAULT_CHAT -)(using conversation: ScalaConversation): String = +def promptMessage(prompt: Prompt, chat: Chat = DEFAULT_CHAT)(using conversation: ScalaConversation): String = conversation.promptMessage(chat, prompt).join() -def promptMessages( - prompt: Prompt, - chat: Chat = OpenAI.FromEnvironment.DEFAULT_CHAT -)(using - conversation: ScalaConversation -): List[String] = +def promptMessages(prompt: Prompt, chat: Chat = DEFAULT_CHAT)(using conversation: ScalaConversation): List[String] = conversation.promptMessages(chat, prompt).join().asScala.toList -def promptStreaming( - prompt: Prompt, - chat: Chat = OpenAI.FromEnvironment.DEFAULT_CHAT -)(using - conversation: ScalaConversation -): LazyList[String] = +def promptStreaming(prompt: Prompt, chat: Chat = DEFAULT_CHAT)(using conversation: ScalaConversation): LazyList[String] = val publisher = conversation.promptStreamingToPublisher(chat, prompt) val queue = new LinkedBlockingQueue[String]() - publisher.subscribe(new Subscriber[String] { - // TODO change to fs2 or similar + publisher.subscribe(new Subscriber[String]: // TODO change to fs2 or similar def onSubscribe(s: Subscription): Unit = s.request(Long.MaxValue) - def onNext(t: String): Unit = queue.add(t); () - def onError(t: Throwable): Unit = throw t - def onComplete(): Unit = () - }) + ) LazyList.continually(queue.take) -def images( - prompt: Prompt, - images: Images = OpenAI.FromEnvironment.DEFAULT_IMAGES, - numberImages: Int = 1, - size: String = "1024x1024" -)(using +def prompt[A: Decoder: SerialDescriptor](message: String)(using ScalaConversation): A = + prompt(Prompt(message)) + +def promptMessage(message: String)(using ScalaConversation): String = + promptMessage(Prompt(message)) + +def promptMessages(message: String)(using ScalaConversation): List[String] = + promptMessages(Prompt(message)) + +def promptStreaming(message: String)(using ScalaConversation): LazyList[String] = + promptStreaming(Prompt(message)) + +def images(prompt: Prompt, images: Images = DEFAULT_IMAGES, numberImages: Int = 1, size: String = "1024x1024")(using conversation: ScalaConversation ): ImagesGenerationResponse = conversation.images(images, prompt, numberImages, size).join() -def conversation[A]( - block: ScalaConversation ?=> A, - conversationId: Option[ConversationId] = Some(ConversationId(UUID.randomUUID().toString)) -): A = block(using ScalaConversation(LocalVectorStore(OpenAI.FromEnvironment.DEFAULT_EMBEDDING), LogsMetric(), conversationId)) +def conversation[A](block: ScalaConversation ?=> A, id: ConversationId = ConversationId(randomUUID.toString)): A = + block(using ScalaConversation(LocalVectorStore(DEFAULT_EMBEDDING), LogsMetric(), id)) + +private def fromJson[A: Decoder]: FromJson[A] = json => parse(json).flatMap(Decoder[A].decodeJson(_)).fold(throw _, identity) diff --git a/scala/src/main/scala/com/xebia/functional/xef/scala/serialization/KotlinXSerializers.scala b/scala/src/main/scala/com/xebia/functional/xef/scala/serialization/KotlinXSerializers.scala new file mode 100644 index 000000000..30aa05e4a --- /dev/null +++ b/scala/src/main/scala/com/xebia/functional/xef/scala/serialization/KotlinXSerializers.scala @@ -0,0 +1,18 @@ +package com.xebia.functional.xef.scala.serialization + +import kotlin.jvm.internal.* +import kotlinx.serialization.KSerializer +import kotlinx.serialization.builtins.BuiltinSerializersKt +import kotlinx.serialization.builtins.BuiltinSerializersKt.serializer + +object KotlinXSerializers: + val int: KSerializer[java.lang.Integer] = serializer(IntCompanionObject.INSTANCE) + val string: KSerializer[String] = serializer(StringCompanionObject.INSTANCE) + val boolean: KSerializer[java.lang.Boolean] = serializer(BooleanCompanionObject.INSTANCE) + val double: KSerializer[java.lang.Double] = serializer(DoubleCompanionObject.INSTANCE) + val float: KSerializer[java.lang.Float] = serializer(FloatCompanionObject.INSTANCE) + val long: KSerializer[java.lang.Long] = serializer(LongCompanionObject.INSTANCE) + val short: KSerializer[java.lang.Short] = serializer(ShortCompanionObject.INSTANCE) + val byte: KSerializer[java.lang.Byte] = serializer(ByteCompanionObject.INSTANCE) + val char: KSerializer[Character] = serializer(CharCompanionObject.INSTANCE) + val unit: KSerializer[kotlin.Unit] = serializer(kotlin.Unit.INSTANCE) diff --git a/scala/src/main/scala/com/xebia/functional/xef/scala/conversation/SerialDescriptor.scala b/scala/src/main/scala/com/xebia/functional/xef/scala/serialization/SerialDescriptor.scala similarity index 94% rename from scala/src/main/scala/com/xebia/functional/xef/scala/conversation/SerialDescriptor.scala rename to scala/src/main/scala/com/xebia/functional/xef/scala/serialization/SerialDescriptor.scala index a7dc121df..9b574065a 100644 --- a/scala/src/main/scala/com/xebia/functional/xef/scala/conversation/SerialDescriptor.scala +++ b/scala/src/main/scala/com/xebia/functional/xef/scala/serialization/SerialDescriptor.scala @@ -1,20 +1,19 @@ -package com.xebia.functional.xef.scala.conversation +package com.xebia.functional.xef.scala.serialization import com.xebia.functional.xef.conversation.jvm.Description as JvmDescription -import kotlinx.serialization.descriptors.{SerialDescriptor as KtSerialDescriptor, SerialKind, StructureKind} import kotlinx.serialization.KSerializer +import kotlinx.serialization.descriptors.{SerialDescriptor as KtSerialDescriptor, SerialKind, StructureKind} import kotlinx.serialization.encoding.{Decoder as KtDecoder, Encoder as KtEncoder} -import scala.quoted.* import java.lang.annotation.Annotation import java.util -import scala.compiletime.{constValue, erasedValue, summonInline} +import scala.annotation.meta.* +import scala.compiletime.* import scala.deriving.* import scala.jdk.CollectionConverters.* +import scala.quoted.* import scala.reflect.ClassTag -import scala.annotation.meta._ - trait SerialDescriptor[A]: def serialDescriptor: KtSerialDescriptor def kserializer: KSerializer[A] = new KSerializer[A]: @@ -81,40 +80,33 @@ object SerialDescriptor extends SerialDescriptorInstances: // Does the element at the given index have a default value, or is it wrapped in `Option`, or is a union with `Null`? override def isElementOptional(index: Int): Boolean = false - def serialDescriptor = serialDescriptorImpl + def serialDescriptor: KtSerialDescriptor = serialDescriptorImpl inline def getStaticAnnotations[A]: List[Any] = ${ getAnnotationsImpl[A] } -def getAnnotationsImpl[A: Type](using Quotes): Expr[List[Any]] = { +def getAnnotationsImpl[A: Type](using Quotes): Expr[List[Any]] = import quotes.reflect.* val annotations = TypeRepr.of[A].typeSymbol.annotations.map(_.asExpr) Expr.ofList(annotations) -} inline def getFieldAnnotationsMap[A]: Map[String, List[Any]] = ${ getFieldAnnotationsMapImpl[A] } -def getFieldAnnotationsMapImpl[A: Type](using Quotes): Expr[Map[String, List[Any]]] = { - import quotes.reflect._ - +def getFieldAnnotationsMapImpl[A: Type](using Quotes): Expr[Map[String, List[Any]]] = + import quotes.reflect.* // Extract the fields from the type val fields = TypeRepr.of[A].typeSymbol.primaryConstructor.paramSymss.flatten - // For each field, extract its name and annotations - val fieldAnnotationsMap: List[Expr[(String, List[Any])]] = fields.map { field => + val fieldAnnotationsMap: List[Expr[(String, List[Any])]] = fields.map: field => val fieldName = Expr(field.name) val annotations = Expr.ofList(field.annotations.map(_.asExpr)) '{ ($fieldName, $annotations) } - } - // Convert list of (String, List[Any]) tuples to a map - fieldAnnotationsMap match { + fieldAnnotationsMap match case Nil => '{ Map.empty[String, List[Any]] } case _ => val mapExpr = Expr.ofList(fieldAnnotationsMap) '{ $mapExpr.toMap } - } -} /** * A scala annotation that maps to the jvm description annotation diff --git a/scala/src/main/scala/com/xebia/functional/xef/scala/conversation/SerialDescriptorInstances.scala b/scala/src/main/scala/com/xebia/functional/xef/scala/serialization/SerialDescriptorInstances.scala similarity index 98% rename from scala/src/main/scala/com/xebia/functional/xef/scala/conversation/SerialDescriptorInstances.scala rename to scala/src/main/scala/com/xebia/functional/xef/scala/serialization/SerialDescriptorInstances.scala index c213c28e9..ee761f27c 100644 --- a/scala/src/main/scala/com/xebia/functional/xef/scala/conversation/SerialDescriptorInstances.scala +++ b/scala/src/main/scala/com/xebia/functional/xef/scala/serialization/SerialDescriptorInstances.scala @@ -1,4 +1,4 @@ -package com.xebia.functional.xef.scala.conversation +package com.xebia.functional.xef.scala.serialization import kotlin.jvm.internal.Reflection import kotlin.reflect.KClass diff --git a/scala/src/test/scala/com/xebia/functional/xef/scala/conversation/SerialDescriptorSpec.scala b/scala/src/test/scala/com/xebia/functional/xef/scala/serialization/SerialDescriptorSpec.scala similarity index 98% rename from scala/src/test/scala/com/xebia/functional/xef/scala/conversation/SerialDescriptorSpec.scala rename to scala/src/test/scala/com/xebia/functional/xef/scala/serialization/SerialDescriptorSpec.scala index 6141cc617..4c103b6e1 100644 --- a/scala/src/test/scala/com/xebia/functional/xef/scala/conversation/SerialDescriptorSpec.scala +++ b/scala/src/test/scala/com/xebia/functional/xef/scala/serialization/SerialDescriptorSpec.scala @@ -1,4 +1,4 @@ -package com.xebia.functional.xef.scala.conversation +package com.xebia.functional.xef.scala.serialization import cats.syntax.either.* import kotlinx.serialization.builtins.BuiltinSerializersKt