Skip to content

Commit

Permalink
DRAFT: CU-865c5pdxy AI as a language primitive via an auto-agent (#15)
Browse files Browse the repository at this point in the history
* AI as a language primitive via an auto-agent

* Small clean-up, and fix ktor-client-js issue

* Move around HttpClient dependencies

* Add Simon's PR suggestion to fix bug listing tasks + additional example

* Remove while and storages (#16)

* Flatten structures

* Improve Task and TaskResult model

* Improve logging, and setup example module (#17)

* Improve logging, and setup example module
* Fix package, split logback into TOML, and remove old reference to storage

* Simplify Auto agent by depending on just `ai` function + basic agents draft (#18)

* autonomous agent in terms of `ai` function with self reasoning and agent support.

Extended `ai` function to accept agents

* wikipedia agent, currently failing JS node on import child_process.ExecOptions, io multiplatform solution based on https://github.com/jmfayard/kotlin-cli-starter

* wikipedia agent, currently failing JS node on import child_process.ExecOptions, io multiplatform solution based on https://github.com/jmfayard/kotlin-cli-starter

* Fix build

---------

Co-authored-by: Simon Vergauwen <nomisRev@users.noreply.github.com>

---------

Co-authored-by: Simon Vergauwen <nomisRev@users.noreply.github.com>
raulraja and nomisRev authored May 2, 2023

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
1 parent c0c170c commit bb3d9e0
Showing 55 changed files with 1,855 additions and 45 deletions.
46 changes: 36 additions & 10 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -49,10 +49,10 @@ kotlin {
mingwX64()

sourceSets {
commonMain {
val commonMain by getting {
dependencies {
implementation(libs.bundles.arrow)
implementation(libs.bundles.ktor.client)
api(libs.bundles.ktor.client)
implementation(libs.kotlinx.serialization.json)
implementation(libs.okio)
implementation(libs.uuid)
@@ -69,12 +69,16 @@ kotlin {
implementation(libs.kotest.assertions.arrow)
}
}

val jvmMain by getting {
dependencies {
implementation(libs.hikari)
implementation(libs.postgresql)
api(libs.ktor.client.cio)
implementation(libs.logback)
}
}

val jvmTest by getting {
dependencies {
implementation(libs.kotest.junit5)
@@ -83,21 +87,43 @@ kotlin {
}
}

val commonMain by getting
val linuxX64Main by getting
val macosX64Main by getting
val mingwX64Main by getting
val jsMain by getting {
dependencies {
api(libs.ktor.client.js)
implementation(libs.okio.nodefilesystem)
}
}

val linuxX64Main by getting {
dependencies {
api(libs.ktor.client.cio)
}
}

val macosX64Main by getting {
dependencies {
api(libs.ktor.client.cio)
}
}

val mingwX64Main by getting {
dependencies {
api(libs.ktor.client.winhttp)
}
}

val commonTest by getting
val linuxX64Test by getting
val macosX64Test by getting
val mingwX64Test by getting

create("nativeMain") {
dependsOn(commonMain)
linuxX64Main.dependsOn(this)
macosX64Main.dependsOn(this)
mingwX64Main.dependsOn(this)
}

val commonTest by getting
val linuxX64Test by getting
val macosX64Test by getting
val mingwX64Test by getting
create("nativeTest") {
dependsOn(commonTest)
linuxX64Test.dependsOn(this)
22 changes: 22 additions & 0 deletions example/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
plugins {
id(libs.plugins.kotlin.jvm.get().pluginId)
id(libs.plugins.kotlinx.serialization.get().pluginId)
}

repositories {
mavenCentral()
}

java {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
toolchain {
languageVersion = JavaLanguageVersion.of(11)
}
}

dependencies {
implementation(rootProject)
implementation(libs.kotlinx.serialization.json)
implementation(libs.logback)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.xebia.functional.langchain4k.auto

import com.xebia.functional.auto.ai
import kotlinx.serialization.Serializable

@Serializable
data class ASCIIArt(val art: String)

suspend fun main() {
val art: ASCIIArt = ai("ASCII art of a cat dancing")
println(art.art)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.xebia.functional.langchain4k.auto

import com.xebia.functional.auto.ai
import kotlinx.serialization.Serializable

@Serializable
data class Animal(val name: String, val habitat: String, val diet: String)

@Serializable
data class Invention(val name: String, val inventor: String, val year: Int, val purpose: String)

@Serializable
data class Story(val animal: Animal, val invention: Invention, val story: String)

suspend fun main() {
val animal: Animal = ai("A unique animal species.")
val invention: Invention = ai("A groundbreaking invention from the 20th century.")

val storyPrompt = """
Write a short story that involves the following elements:
1. A unique animal species called ${animal.name} that lives in ${animal.habitat} and has a diet of ${animal.diet}.
2. A groundbreaking invention from the 20th century called ${invention.name}, invented by ${invention.inventor} in ${invention.year}, which serves the purpose of ${invention.purpose}.
""".trimIndent()

val story: Story = ai(storyPrompt)

println("Story about ${animal.name} and ${invention.name}: ${story.story}")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.xebia.functional.langchain4k.auto

import com.xebia.functional.auto.ai
import kotlinx.serialization.Serializable

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

suspend fun main() {
val toKillAMockingbird: Book = ai("To Kill a Mockingbird by Harper Lee summary.")
println("To Kill a Mockingbird summary:\n ${toKillAMockingbird.summary}")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package com.xebia.functional.langchain4k.auto

import com.xebia.functional.auto.ai
import kotlinx.serialization.Serializable

@Serializable
data class ChessMove(val player : String, val move: String)
@Serializable
data class ChessBoard(val board: String)
@Serializable
data class GameState(val ended: Boolean, val winner: String)

suspend fun main() {
val moves = mutableListOf<ChessMove>()
var gameEnded = false
var winner = ""

while (!gameEnded) {
val currentPlayer = if (moves.size % 2 == 0) "Player 1 (White)" else "Player 2 (Black)"
val prompt = """
|$currentPlayer, it's your turn.
|Previous moves: ${moves.joinToString(", ")}
|Make your next move:
""".trimIndent()

val move: ChessMove = ai(prompt)
moves.add(move)

// Update boardState according to move.move
// ...

val boardPrompt = """
Given the following chess moves: ${moves.joinToString(", ") { it.player + ":"+ it.move }}},
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
""".trimIndent()

val chessBoard: ChessBoard = ai(boardPrompt)
println("Current board:\n${chessBoard.board}")

val gameStatePrompt = """
Given the following chess moves: ${moves.joinToString(", ")},
has the game ended (win, draw, or stalemate)?
""".trimIndent()

val gameState: GameState = ai(gameStatePrompt)

gameEnded = gameState.ended
winner = gameState.winner
}

println("Game over. Final move: ${moves.last()}, Winner: $winner")
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.xebia.functional.langchain4k.auto

import com.xebia.functional.auto.ai
import kotlinx.serialization.Serializable

@Serializable
data class Colors(val colors: List<String>)

suspend fun main() {
val colors: Colors = ai("a selection of 10 beautiful colors that go well together")
println(colors)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.xebia.functional.langchain4k.auto

import com.xebia.functional.auto.ai
import com.xebia.functional.auto.agents.Agent
import com.xebia.functional.auto.agents.wikipedia
import kotlinx.serialization.Serializable

@Serializable
data class NumberOfMedicalNeedlesInWorld(val numberOfNeedles: Long)

suspend fun main() {
val needlesInWorld: NumberOfMedicalNeedlesInWorld = ai(
"""|Provide the number of medical needles in the world.
""".trimMargin(),
agents = listOf(Agent.wikipedia())
)
println("Needles in world: ${needlesInWorld.numberOfNeedles}")
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.xebia.functional.langchain4k.auto

import com.xebia.functional.auto.ai
import kotlinx.serialization.Serializable

@Serializable
data class Employee(val firstName: String, val lastName: String, val age: Int, val position: String, val company: Company)

@Serializable
data class Address(val street: String, val city: String, val country: String)

@Serializable
data class Company(val name: String, val address: Address)

suspend fun main() {
val complexPrompt = "Provide information for an Employee that includes their first name, last name, age, position, and their company's name and address (street, city, and country)."

val employeeData: Employee = ai(complexPrompt)

println("Employee Information:\n\n" +
"Name: ${employeeData.firstName} ${employeeData.lastName}\n" +
"Age: ${employeeData.age}\n" +
"Position: ${employeeData.position}\n" +
"Company: ${employeeData.company.name}\n" +
"Address: ${employeeData.company.address.street}, ${employeeData.company.address.city}, ${employeeData.company.address.country}")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.xebia.functional.langchain4k.auto

import com.xebia.functional.auto.ai
import kotlinx.serialization.Serializable

@Serializable
data class Fact(val topic: String, val content: String)

suspend fun main() {
val fact1: Fact = ai("A fascinating fact about you")
val fact2: Fact = ai("An interesting fact about me")

val riddlePrompt = """
Create a riddle that combines the following facts:
Fact 1: ${fact1.content}
Fact 2: ${fact2.content}
""".trimIndent()

val riddle: String = ai(riddlePrompt)

println("Riddle:\n\n${riddle}")
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.xebia.functional.langchain4k.auto

import com.xebia.functional.auto.ai

suspend fun main() {
val love: List<String> = ai("tell me you like me with just emojis")
println(love)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.xebia.functional.langchain4k.auto

import com.xebia.functional.auto.ai
import com.xebia.functional.auto.agents.Agent
import com.xebia.functional.auto.agents.wikipedia
import kotlinx.serialization.Serializable

@Serializable
data class MealPlan(val name: String, val recipes: List<Recipe>) {
fun prettyPrint(): String {
return recipes.joinToString("\n") { "${it.name}:\n${it.ingredients.joinToString("\n")}" }
}
}

suspend fun main() {
val mealPlan: MealPlan =
ai(
"Meal plan for the week for a person with gall bladder stones that includes 5 recipes.",
auto = true,
agents = listOf(Agent.wikipedia())
)
println(
"""The meal plan for the week is:
|${mealPlan.prettyPrint()}""".trimMargin()
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.xebia.functional.langchain4k.auto

import com.xebia.functional.auto.ai
import kotlinx.serialization.Serializable

@Serializable
data class MeaningOfLife(val mainTheories: List<String>)

suspend fun main() {
val meaningOfLife: MeaningOfLife = ai("What are the main theories about the meaning of life")
println("There are several theories about the meaning of life:\n ${meaningOfLife.mainTheories}")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.xebia.functional.langchain4k.auto

import com.xebia.functional.auto.ai
import kotlinx.serialization.Serializable

@Serializable
data class Movie(val title: String, val genre: String, val director: String)

suspend fun main() {
val movie: Movie = ai("Inception movie genre and director.")
println("The movie ${movie.title} is a ${movie.genre} film directed by ${movie.director}.")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.xebia.functional.langchain4k.auto

import com.xebia.functional.auto.ai
import kotlinx.serialization.Serializable

@Serializable
data class Person(val name: String, val age: Int)

suspend fun main() {
val bogus: List<Person> = ai("come up with some random data that matches the type")
println(bogus)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.xebia.functional.langchain4k.auto

import com.xebia.functional.auto.ai
import com.xebia.functional.auto.agents.Agent
import com.xebia.functional.auto.agents.wikipedia
import kotlinx.serialization.Serializable

@Serializable
data class Planet(val name: String, val distanceFromSun: Double, val moons: List<Moon>)

@Serializable
data class Moon(val name: String, val distanceFromPlanet: Double)

suspend fun main() {
val earth: Planet = ai("Information about Earth and its moon.", auto = true, agents = listOf(Agent.wikipedia()))
val mars: Planet = ai("Information about Mars and its moons.", auto = true, agents = listOf(Agent.wikipedia()))

fun planetInfo(planet: Planet): String {
return """${planet.name} is ${planet.distanceFromSun} million km away from the Sun.
|It has the following moons:
|${planet.moons.joinToString("\n") { " - ${it.name}: ${it.distanceFromPlanet} km away from ${planet.name}" }}
""".trimMargin()
}

println("Celestial bodies information:\n\n${planetInfo(earth)}\n\n${planetInfo(mars)}")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.xebia.functional.langchain4k.auto

import com.xebia.functional.auto.ai
import kotlinx.serialization.Serializable

@Serializable
data class Poem(val title: String, val content: String)

suspend fun main() {
val poem1: Poem = ai("A poem about the beauty of nature.")
val poem2: Poem = ai("A poem about the power of technology.")
val poem3: Poem = ai("A poem about the wisdom of artificial intelligence.")

val combinedPoemContent = "${poem1.content}\n\n${poem2.content}\n\n${poem3.content}"

val newPoemPrompt = """
Write a new poem that combines ideas from the following themes: the beauty of nature, the power of technology, and the wisdom of artificial intelligence. Here are some examples of poems on these themes:
$combinedPoemContent
""".trimIndent()

val newPoem: Poem = ai(newPoemPrompt)

println("New Poem:\n\n${newPoem.content}")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.xebia.functional.langchain4k.auto

import com.xebia.functional.auto.ai
import kotlinx.serialization.Serializable

@Serializable
data class Population(val size: Int, val description: String)

suspend fun main() {
val cadiz: Population = ai("Population of Cádiz, Spain.")
val seattle: Population = ai("Population of Seattle, WA.")
println("The population of Cádiz is ${cadiz.size} and the population of Seattle is ${seattle.size}")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.xebia.functional.langchain4k.auto

import com.xebia.functional.auto.ai
import kotlinx.serialization.Serializable

@Serializable
data class Recipe(val name: String, val ingredients: List<String>)

suspend fun main() {
val recipe: Recipe = ai("Recipe for chocolate chip cookies.")
println("The recipe for ${recipe.name} is ${recipe.ingredients}")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.xebia.functional.langchain4k.auto

import com.xebia.functional.auto.ai
import kotlinx.serialization.Serializable

@Serializable
data class TopAttraction(val city: City, val attractionName: String, val description: String, val weather: Weather)

@Serializable
data class City(val name: String, val country: String)

@Serializable
data class Weather(val city: City, val temperature: Double, val description: String)

suspend fun main() {
val nearbyTopAttraction: TopAttraction = ai("Top attraction in Cádiz, Spain.")
println(
"""
|The top attraction in ${nearbyTopAttraction.city.name} is ${nearbyTopAttraction.attractionName}.
|Here's a brief description: ${nearbyTopAttraction.description}.
|The weather in ${nearbyTopAttraction.city.name} is ${nearbyTopAttraction.weather.temperature} degrees Celsius and ${nearbyTopAttraction.weather.description}.
|""".trimMargin()
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.xebia.functional.langchain4k.auto

import com.xebia.functional.auto.ai
import kotlinx.serialization.Serializable

@Serializable
data class TouristAttraction(val name: String, val location: String, val history: String)

suspend fun main() {
val statueOfLiberty: TouristAttraction = ai("Statue of Liberty location and history.")
println(
"""${statueOfLiberty.name} is located in ${statueOfLiberty.location} and has the following history:
|${statueOfLiberty.history}""".trimMargin()
)
}
11 changes: 11 additions & 0 deletions example/src/main/resources/logback.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>

<root level="DEBUG">
<appender-ref ref="STDOUT" />
</root>
</configuration>
10 changes: 9 additions & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -16,6 +16,7 @@ postgresql = "42.5.1"
testcontainers = "1.17.6"
hikari = "5.0.1"
dokka = "1.8.10"
logback = "1.4.6"

[libraries]
arrow-core = { module = "io.arrow-kt:arrow-core", version.ref = "arrow" }
@@ -24,10 +25,14 @@ arrow-resilience = { module = "io.arrow-kt:arrow-resilience", version.ref = "arr
open-ai = { module = "com.theokanning.openai-gpt3-java:service", version.ref = "openai" }
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-json" }
ktor-client = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
ktor-client-js = { module = "io.ktor:ktor-client-js", version.ref = "ktor" }
ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor" }
ktor-client-winhttp = { module = "io.ktor:ktor-client-winhttp", version.ref = "ktor" }
ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" }
ktor-client-serialization = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" }
okio = { module = "com.squareup.okio:okio", version.ref = "okio" }
okio-fakefilesystem = { module = "com.squareup.okio:okio-fakefilesystem", version.ref = "okio" }
okio-nodefilesystem = { module = "com.squareup.okio:okio-nodefilesystem", version.ref = "okio" }
kotest-assertions = { module = "io.kotest:kotest-assertions-core", version.ref = "kotest" }
kotest-framework = { module = "io.kotest:kotest-framework-engine", version.ref = "kotest" }
kotest-property = { module = "io.kotest:kotest-property", version.ref = "kotest" }
@@ -39,6 +44,7 @@ klogging = { module = "io.github.oshai:kotlin-logging", version.ref = "klogging"
hikari = { module = "com.zaxxer:HikariCP", version.ref = "hikari" }
postgresql = { module = "org.postgresql:postgresql", version.ref = "postgresql" }
testcontainers-postgresql = { module = "org.testcontainers:postgresql", version.ref = "testcontainers" }
logback = { module = "ch.qos.logback:logback-classic", version.ref = "logback" }

[bundles]
arrow = [
@@ -54,8 +60,10 @@ ktor-client = [

[plugins]
kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
kotlin-js = { id = "org.jetbrains.kotlin.js", version.ref = "kotlin" }
kotlinx-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
spotless = { id = "com.diffplug.spotless", version.ref = "spotless" }
dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" }
arrow-gradle-nexus = { id = "io.arrow-kt.arrow-gradle-config-nexus", version.ref = "arrowGradle" }
arrow-gradle-publish = { id = "io.arrow-kt.arrow-gradle-config-publish", version.ref = "arrowGradle" }
arrow-gradle-publish = { id = "io.arrow-kt.arrow-gradle-config-publish", version.ref = "arrowGradle" }
24 changes: 24 additions & 0 deletions nodejs-commandexecutor/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
plugins {
id(libs.plugins.kotlin.js.get().pluginId)
}

group = "com.xebia.functional.langchain4k"
version = "0.0.1-SNAPSHOT"

repositories {
mavenCentral()
}

kotlin {
js(IR) {
nodejs()
}

sourceSets {
val main by getting {
dependencies {
implementation(rootProject)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.xebia.functional.agents

import com.xebia.functional.auto.agents.Agent
import com.xebia.functional.auto.agents.WikipediaResult
import com.xebia.functional.auto.agents.wikipedia
import com.xebia.functional.io.CommandExecutor
import com.xebia.functional.io.SYSTEM

fun Agent.Companion.wikipedia(): Agent<WikipediaResult> =
Agent.wikipedia(CommandExecutor.SYSTEM)
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package com.xebia.functional.io

import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine

private external val child_process: dynamic
private external val os: dynamic

private open external class ExecOptions {
var cwd: String
}

val CommandExecutor.Companion.SYSTEM
get() = NodeCommandExecutor

object NodeCommandExecutor : CommandExecutor {
override suspend fun executeCommandAndCaptureOutput(command: List<String>, options: ExecuteCommandOptions): String {
val commandToExecute = command.joinToString(separator = " ") { arg ->
if (arg.contains(" ")) "'$arg'" else arg
}
val redirect = if (options.redirectStderr) "2>&1 " else ""
val execOptions = object : ExecOptions() {
init {
cwd = options.directory
}
}
return suspendCoroutine { continuation ->
child_process.exec("$commandToExecute $redirect", execOptions) { error, stdout, stderr ->
if (error != null) {
println(stderr)
continuation.resumeWithException(error)
} else {
continuation.resume(if (options.trim) stdout.trim() else stdout)
}
}
Unit
}
}

override suspend fun pwd(options: ExecuteCommandOptions): String =
// https://nodejs.org/api/os.html
when (os.platform()) {
"win32" -> executeCommandAndCaptureOutput(listOf("echo", "%cd%"), options).trim()
else -> executeCommandAndCaptureOutput(listOf("pwd"), options).trim()
}

override suspend fun findExecutable(executable: String): String = executable
}
3 changes: 2 additions & 1 deletion settings.gradle.kts
Original file line number Diff line number Diff line change
@@ -6,4 +6,5 @@ pluginManagement {

}
rootProject.name = "langchain4k"

include("nodejs-commandexecutor")
include("example")
39 changes: 39 additions & 0 deletions src/commonMain/kotlin/com/xebia/functional/auto/AutoAI.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.xebia.functional.auto

import com.xebia.functional.auto.agents.Agent
import com.xebia.functional.auto.model.*

tailrec suspend fun solveObjective(
task: Task,
maxAttempts: Int = 5,
previousSolutions: List<Solution> = emptyList(),
agents: List<Agent<*>> = emptyList()
): Solution = if (maxAttempts <= 0) {
Solution(-1, task, result = "Exceeded maximum attempts", accomplishesObjective = false)
} else {
logger.debug { "Solving objective: ${task.objective} with agents $agents" }
val contexts = agents.map { it.context(task, previousSolutions) }
logger.debug { "Contexts: $contexts" }
val ctx = AIContext(task, previousSolutions, contexts)
val result: Solution = ctx.solution()
logger.debug { "Solved: ${result.accomplishesObjective}" }
if (result.accomplishesObjective) {
logger.debug { "Solution accomplishes objective, proceeding to verification" }
val verification: Verification = ctx.verify(result)
if (verification.solvesTheProblemForReal) {
logger.debug { "Solution verified: ${verification.solution.result} accepted: ${verification.solution.accomplishesObjective}" }
result
} else {
val refinedTask: Task = ctx.refineTask()
logger.debug { "Refined task: ${refinedTask.objective}" }
solveObjective(refinedTask, maxAttempts - 1, previousSolutions + result)
}
} else {
val refinedTask: Task = ctx.refineTask()
logger.debug { "Refined task: ${refinedTask.objective}" }
solveObjective(refinedTask, maxAttempts - 1, previousSolutions + result)
}
}



125 changes: 125 additions & 0 deletions src/commonMain/kotlin/com/xebia/functional/auto/DSL.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
package com.xebia.functional.auto

import arrow.core.getOrElse
import arrow.core.raise.catch
import arrow.core.raise.either
import arrow.fx.coroutines.resourceScope
import com.xebia.functional.auto.agents.Agent
import com.xebia.functional.auto.model.Task
import com.xebia.functional.auto.serialization.buildJsonSchema
import com.xebia.functional.embeddings.OpenAIEmbeddings
import com.xebia.functional.env.OpenAIConfig
import com.xebia.functional.llm.openai.*
import com.xebia.functional.vectorstores.LocalVectorStore
import com.xebia.functional.vectorstores.VectorStore
import io.github.oshai.KotlinLogging
import io.ktor.client.engine.HttpClientEngine
import kotlinx.serialization.DeserializationStrategy
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.descriptors.serialDescriptor
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.serializer
import kotlin.time.ExperimentalTime

@PublishedApi
internal val logger = KotlinLogging.logger("AutoAI")

val json = Json {
ignoreUnknownKeys = true
isLenient = true
}

@OptIn(ExperimentalTime::class)
suspend inline fun <reified A> ai(
prompt: String,
engine: HttpClientEngine? = null,
agents: List<Agent<*>> = emptyList(),
auto: Boolean = false,
): A {
val descriptor = serialDescriptor<A>()
val jsonSchema = buildJsonSchema(descriptor)
return resourceScope {
either {
val openAIConfig = OpenAIConfig()
val openAiClient = KtorOpenAIClient(openAIConfig, engine)
val embeddings = OpenAIEmbeddings(openAIConfig, openAiClient, logger)
val vectorStore = LocalVectorStore(embeddings)
ai(prompt, descriptor, serializer<A>(), jsonSchema, openAiClient, vectorStore, agents, auto)
}.getOrElse { throw IllegalStateException(it.joinToString()) }
}
}

suspend fun <A> ai(
prompt: String,
descriptor: SerialDescriptor,
deserializationStrategy: DeserializationStrategy<A>,
jsonSchema: JsonObject,
openAIClient: OpenAIClient,
vectorStore: VectorStore,
agents: List<Agent<*>> = emptyList(),
auto: Boolean = false,
): A {
val augmentedPrompt = """
|Objective: $prompt
|Instructions: Use the following JSON schema to produce the result on json format
|JSON Schema:$jsonSchema
|Return exactly the JSON response and nothing else
""".trimMargin()
val result = if (auto) {
logger.debug { "Solving objective in auto reasoning mode: ${agents}\n$prompt" }
val resolutionContext = solveObjective(Task(-1, prompt, emptyList()), agents = agents).result
val promptWithContext = """
|$resolutionContext
|
|Given this information solve the objective:
|
|$augmentedPrompt
""".trimMargin()
openAIChatCall(openAIClient, promptWithContext)
} else {
logger.debug { "Solving objective without agents\n$prompt" }
openAIChatCall(openAIClient, augmentedPrompt)
}
return catch({
json.decodeFromString(deserializationStrategy, result)
}) { e ->
val fixJsonPrompt = """
|Result: $result
|Exception: ${e.message}
|Objective: $prompt
|Instructions: Use the following JSON schema to produce the result on valid json format avoiding the exception.
|JSON Schema:$jsonSchema
""".trimMargin()
logger.debug { "Attempting to Fix JSON due to error: ${e.message}" }

//here we should retry and handle errors, when we are executing the `ai` function again it might fail and it eventually crashes
//we should handle this and retry
ai(fixJsonPrompt, descriptor, deserializationStrategy, jsonSchema, openAIClient, vectorStore)
.also { logger.debug { "Fixed JSON: $it" } }
}
}

private suspend fun openAIChatCall(
openAIClient: OpenAIClient,
promptWithContext: String
): String {
val res = chatCompletionResponse(openAIClient, promptWithContext, "gpt-3.5-turbo", "AI_Value_Generator")
val msg = res.choices.firstOrNull()?.message?.content
requireNotNull(msg) { "No message found in result: $res" }
return msg
}

private suspend fun chatCompletionResponse(
openAIClient: OpenAIClient,
prompt: String,
model: String,
user: String
): ChatCompletionResponse {
val completionRequest = ChatCompletionRequest(
model = model,
messages = listOf(Message(Role.system.name, prompt, user)),
user = user
)
return openAIClient.createChatCompletion(completionRequest)
}
10 changes: 10 additions & 0 deletions src/commonMain/kotlin/com/xebia/functional/auto/agents/Agent.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.xebia.functional.auto.agents

import com.xebia.functional.auto.model.Solution
import com.xebia.functional.auto.model.Task

interface Agent<out A> {
suspend fun context(task: Task, previousSolutions: List<Solution>): AgentContext<A>

companion object
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.xebia.functional.auto.agents

import com.xebia.functional.auto.model.AIContext
import com.xebia.functional.auto.model.Solution
import com.xebia.functional.auto.model.Task
import com.xebia.functional.auto.model.previousSolutionsContext

data class AgentContext<out A>(
val task: Task, val previousSolutions: List<Solution>, val value: A, val output: String
)

internal fun AIContext.additionalContext(): String =
if (agentContexts.isEmpty()) previousSolutionsContext()
else """|${previousSolutionsContext()}
|
|Additional context provided by other agents for this context is shown below and delimited
|by a line of dashes
|-----------------------------------------------------------------------
|${agentContexts.joinToString("\n") { "- ${it.task.objective}\n\tresult:${it.output}\n\t" }}.
|-----------------------------------------------------------------------
|""".trimMargin()
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package com.xebia.functional.auto.agents

import com.xebia.functional.auto.*
import com.xebia.functional.auto.model.Solution
import com.xebia.functional.auto.model.Task
import com.xebia.functional.io.CommandExecutor
import com.xebia.functional.io.ExecuteCommandOptions
import io.ktor.http.*
import kotlinx.serialization.Serializable
import okio.FileSystem

@Serializable
data class WikipediaResult(val command: String, val description: String)

fun Agent.Companion.wikipedia(executor: CommandExecutor): Agent<WikipediaResult> = object : Agent<WikipediaResult> {
override suspend fun context(task: Task, previousSolutions: List<Solution>): AgentContext<WikipediaResult> {
val curlCommand = listOf(
"curl",
"-s",
"https://en.wikipedia.org/w/api.php?action=query&format=json&list=search&srsearch=${task.objective.encodeURLQueryComponent()}"
)
val result = executor.executeCommandAndCaptureOutput(
curlCommand,
ExecuteCommandOptions(
directory = FileSystem.SYSTEM_TEMPORARY_DIRECTORY.toString(),
abortOnError = true,
redirectStderr = true,
trim = true
)
)
val summarized: WikipediaResult = ai(
"""
You are an AI Agent in charge of extracting relevant information for objective:
${task.objective}
Instructions:
- Extract the relevant information from the following output extracted by command:
$curlCommand
- The output is:
----------------
$result
----------------
- If no relevant information is found, return the following:
<No relevant information found>
""".trimIndent()
)
logger.debug { "WikipediaAgent: $summarized" }
return AgentContext(task, previousSolutions, summarized, summarized.description)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.xebia.functional.auto.model

import com.xebia.functional.auto.agents.AgentContext

data class AIContext(
val currentTask: Task, val previousSolutions: List<Solution>, val agentContexts: List<AgentContext<*>>
)
26 changes: 26 additions & 0 deletions src/commonMain/kotlin/com/xebia/functional/auto/model/Solution.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.xebia.functional.auto.model

import com.xebia.functional.auto.agents.additionalContext
import com.xebia.functional.auto.ai
import kotlinx.serialization.Serializable

@Serializable
data class Solution(
val id: Int, val task: Task, val result: String, val accomplishesObjective: Boolean
)

suspend fun AIContext.solution(): Solution =
ai(
"""|You are an AI who performs one task based on the following objective:
|${currentTask.objective}
|
|${additionalContext()}
|If you can't solve the objective, use `false` for `accomplishesObjective` in the returned response.
""".trimMargin()
)

internal fun AIContext.previousSolutionsContext(): String =
if (previousSolutions.isEmpty()) ""
else """|Take into account these previously attempted tasks:
|${previousSolutions.joinToString("\n") { "- ${it.task.objective}\n\tresult:${it.result}\n\tAccomplishes Objective: ${it.accomplishesObjective}" }}.
""".trimMargin()
22 changes: 22 additions & 0 deletions src/commonMain/kotlin/com/xebia/functional/auto/model/Task.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.xebia.functional.auto.model

import com.xebia.functional.auto.agents.additionalContext
import com.xebia.functional.auto.ai
import kotlinx.serialization.Serializable

@Serializable
data class Task(
val id: Int, val objective: String, val instructions: List<String>
)

suspend fun AIContext.refineTask(): Task = ai(
"""|You are an AI who refines a task based on the following objective:
|${currentTask.objective}
|We have attempted to solve the with the current solution
|but we are not sure if it's successful.
|
|${additionalContext()}
|
|Please refine the following task: ${currentTask.objective} to make it more specific, so that it can be solved.
""".trimMargin()
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.xebia.functional.auto.model

import com.xebia.functional.auto.agents.additionalContext
import com.xebia.functional.auto.ai
import kotlinx.serialization.Serializable

@Serializable
data class Verification(val id: Int, val solution: Solution, val solvesTheProblemForReal: Boolean)

suspend fun AIContext.verify(result: Solution): Verification = ai(
"""|You are an AI who verifies an objective has been accomplished for one task based on the following objective:
|We have attempted to solve the following task objective:
|${currentTask.objective}
|We came up with the current solution, but we are not sure if it's successful.
|Please verify that the following solution: ${result.result} accomplishes the objective.
|Be very critical until you are sure that the solution solves the objective.
|
|${additionalContext()}
|
""".trimMargin()
)

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -39,13 +39,16 @@ class OpenAIEmbeddings(
private suspend fun createEmbeddingWithRetry(texts: List<String>, requestConfig: RequestConfig): List<Embedding> =
kotlin.runCatching {
config.retryConfig.schedule()
.log { retriesSoFar, _ -> logger.warn { "Open AI call failed. So far we have retried $retriesSoFar times." } }
.log { error, retriesSoFar ->
error.printStackTrace()
logger.warn { "Open AI call failed. So far we have retried $retriesSoFar times." }
}
.retry {
oaiClient.createEmbeddings(EmbeddingRequest(requestConfig.model.name, texts, requestConfig.user.id))
oaiClient.createEmbeddings(EmbeddingRequest(requestConfig.model.modelName, texts, requestConfig.user.id))
.data.map { Embedding(it.embedding) }
}
}.getOrElse {
logger.warn { "Open AI call failed. Giving up after ${config.retryConfig.maxRetries} retries" }
throw it
}
}
}
26 changes: 26 additions & 0 deletions src/commonMain/kotlin/com/xebia/functional/io/CommandExecutor.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.xebia.functional.io


data class ExecuteCommandOptions(
val directory: String,
val abortOnError: Boolean,
val redirectStderr: Boolean,
val trim: Boolean
)

enum class Platform {
LINUX, MACOS, WINDOWS,
}

interface CommandExecutor {
suspend fun executeCommandAndCaptureOutput(
command: List<String>,
options: ExecuteCommandOptions
): String

suspend fun pwd(options: ExecuteCommandOptions): String

suspend fun findExecutable(executable: String): String

companion object
}
21 changes: 13 additions & 8 deletions src/commonMain/kotlin/com/xebia/functional/ktor.kt
Original file line number Diff line number Diff line change
@@ -2,6 +2,7 @@ package com.xebia.functional

import arrow.fx.coroutines.ResourceScope
import io.ktor.client.HttpClient
import io.ktor.client.HttpClientConfig
import io.ktor.client.engine.HttpClientEngine
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.client.plugins.defaultRequest
@@ -19,12 +20,16 @@ inline fun <reified A> HttpRequestBuilder.configure(token: String, request: A):
setBody(request)
}

suspend fun ResourceScope.httpClient(engine: HttpClientEngine, baseUrl: Url): HttpClient =
suspend fun ResourceScope.httpClient(engine: HttpClientEngine?, baseUrl: Url): HttpClient =
install({
HttpClient(engine) {
install(ContentNegotiation) { json() }
defaultRequest {
url(baseUrl.toString())
}
}
}) { client, _ -> client.close() }
engine?.let {
HttpClient(engine) { configure(baseUrl) }
} ?: HttpClient { configure(baseUrl) }
}) { client, _ -> client.close() }

private fun HttpClientConfig<*>.configure(baseUrl: Url): Unit {
install(ContentNegotiation) { json() }
defaultRequest {
url(baseUrl.toString())
}
}
Original file line number Diff line number Diff line change
@@ -15,8 +15,8 @@ interface HuggingFaceClient {
}

suspend fun ResourceScope.KtorHuggingFaceClient(
engine: HttpClientEngine,
config: HuggingFaceConfig
config: HuggingFaceConfig,
engine: HttpClientEngine? = null
): HuggingFaceClient = KtorHuggingFaceClient(httpClient(engine, config.baseUrl), config)

private class KtorHuggingFaceClient(
Original file line number Diff line number Diff line change
@@ -2,23 +2,26 @@ package com.xebia.functional.llm.openai

import arrow.fx.coroutines.ResourceScope
import arrow.resilience.retry
import com.xebia.functional.auto.logger
import com.xebia.functional.configure
import com.xebia.functional.env.OpenAIConfig
import com.xebia.functional.httpClient
import io.ktor.client.HttpClient
import io.ktor.client.call.body
import io.ktor.client.engine.HttpClientEngine
import io.ktor.client.request.post
import io.ktor.client.statement.HttpResponse
import io.ktor.http.path

interface OpenAIClient {
suspend fun createCompletion(request: CompletionRequest): List<CompletionChoice>
suspend fun createChatCompletion(request: ChatCompletionRequest): ChatCompletionResponse
suspend fun createEmbeddings(request: EmbeddingRequest): EmbeddingResult
}

suspend fun ResourceScope.KtorOpenAIClient(
engine: HttpClientEngine,
config: OpenAIConfig
config: OpenAIConfig,
engine: HttpClientEngine? = null
): OpenAIClient = KtorOpenAIClient(httpClient(engine, config.baseUrl), config)

private class KtorOpenAIClient(
@@ -36,8 +39,19 @@ private class KtorOpenAIClient(
return response.body()
}

override suspend fun createEmbeddings(request: EmbeddingRequest): EmbeddingResult {
override suspend fun createChatCompletion(request: ChatCompletionRequest): ChatCompletionResponse {
val response = config.retryConfig.schedule().retry {
httpClient.post {
url { path("chat/completions") }
configure(config.token, request)
}
}
// TODO error body fails to parse into ChatCompletionResponse
return response.body()
}

override suspend fun createEmbeddings(request: EmbeddingRequest): EmbeddingResult {
val response: HttpResponse = config.retryConfig.schedule().retry {
httpClient.post {
url { path("embeddings") }
configure(config.token, request)
58 changes: 52 additions & 6 deletions src/commonMain/kotlin/com/xebia/functional/llm/openai/models.kt
Original file line number Diff line number Diff line change
@@ -4,7 +4,7 @@ import kotlin.jvm.JvmInline
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

enum class EmbeddingModel(name: String) {
enum class EmbeddingModel(val modelName: String) {
TextEmbeddingAda002("text-embedding-ada-002")
}

@@ -33,12 +33,58 @@ data class CompletionRequest(
val logprobs: Int? = null,
val echo: Boolean? = null,
val stop: List<String>? = null,
@SerialName("presence_penalty") val presencePenalty: Double? = null,
@SerialName("frequency_penalty") val frequencyPenalty: Double? = null,
@SerialName("best_of") val bestOf: Int? = null,
@SerialName("logit_bias") val logitBias: Map<String, Int>? = null,
@SerialName("presence_penalty") val presencePenalty: Double = 0.0,
@SerialName("frequency_penalty") val frequencyPenalty: Double = 0.0,
@SerialName("best_of") val bestOf: Int = 1,
@SerialName("logit_bias") val logitBias: Map<String, Int> = emptyMap(),
)

@Serializable
data class ChatCompletionRequest(
val model: String,
val messages: List<Message>,
val temperature: Double = 1.0,
@SerialName("top_p") val topP: Double = 1.0,
val n: Int = 1,
val stream: Boolean = false,
val stop: List<String>? = null,
@SerialName("max_tokens") val maxTokens: Int? = null,
@SerialName("presence_penalty") val presencePenalty: Double = 0.0,
@SerialName("frequency_penalty") val frequencyPenalty: Double = 0.0,
@SerialName("logit_bias") val logitBias: Map<String, Double>? = emptyMap(),
val user: String?
)

@Serializable
data class ChatCompletionResponse(
val id: String,
val `object`: String,
val created: Long,
val model: String,
val usage: Usage,
val choices: List<Choice>
)

@Serializable
data class Choice(
val message: Message,
@SerialName("finish_reason") val finishReason: String,
val index: Int
)


enum class Role {
system, user, assistant
}

@Serializable
data class Message(
val role: String,
val content: String,
val name: String? = Role.assistant.name
)


@Serializable
data class EmbeddingRequest(val model: String, val input: List<String>, val user: String)

@@ -56,6 +102,6 @@ class Embedding(val `object`: String, val embedding: List<Float>, val index: Int
@Serializable
data class Usage(
@SerialName("prompt_tokens") val promptTokens: Long,
@SerialName("completion_tokens") val completionTokens: Long,
@SerialName("completion_tokens") val completionTokens: Long? = null,
@SerialName("total_tokens") val totalTokens: Long
)
6 changes: 2 additions & 4 deletions src/commonMain/kotlin/com/xebia/functional/main.kt
Original file line number Diff line number Diff line change
@@ -16,17 +16,15 @@ import io.ktor.client.engine.HttpClientEngine
suspend fun main(): Unit = resourceScope {
either {
val env = Env()
val openAPI = KtorOpenAIClient(engine(), env.openAI)
val huggingFace = KtorHuggingFaceClient(engine(), env.huggingFace)
val openAPI = KtorOpenAIClient(env.openAI)
val huggingFace = KtorHuggingFaceClient(env.huggingFace)

println(openAIExample(openAPI))
println(hfExample(huggingFace))
println(openAIEmbeddingsExample(openAPI))
}.onLeft { println(it) }
}

fun engine(): HttpClientEngine = TODO()

suspend fun openAIEmbeddingsExample(client: OpenAIClient) =
client.createEmbeddings(
EmbeddingRequest(
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package com.xebia.functional.vectorstores

import com.xebia.functional.Document
import com.xebia.functional.embeddings.Embedding
import com.xebia.functional.embeddings.Embeddings
import com.xebia.functional.llm.openai.EmbeddingModel
import com.xebia.functional.llm.openai.RequestConfig
import kotlinx.uuid.UUID
import kotlinx.uuid.generateUUID
import kotlin.math.sqrt
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock

class LocalVectorStore(private val embeddings: Embeddings) : VectorStore {

private val requestConfig = RequestConfig(
EmbeddingModel.TextEmbeddingAda002,
RequestConfig.Companion.User("user")
)

private val mutex: Mutex = Mutex()
private val documents: MutableList<Document> = mutableListOf()
private val documentEmbeddings: MutableMap<Document, Embedding> = mutableMapOf()

override suspend fun addTexts(texts: List<String>): List<DocumentVectorId> {
val embeddingsList = embeddings.embedDocuments(texts, chunkSize = null, requestConfig = requestConfig)
return texts.zip(embeddingsList) { text, embedding ->
val document = Document(text)
mutex.withLock {
documents.add(document)
documentEmbeddings[document] = embedding
}
DocumentVectorId(UUID.generateUUID())
}
}

override suspend fun addDocuments(documents: List<Document>): List<DocumentVectorId> =
addTexts(documents.map(Document::content))

override suspend fun similaritySearch(query: String, limit: Int): List<Document> {
val queryEmbedding = embeddings.embedQuery(query, requestConfig = requestConfig).firstOrNull()
return queryEmbedding?.let {
similaritySearchByVector(it, limit)
}.orEmpty()
}

override suspend fun similaritySearchByVector(embedding: Embedding, limit: Int): List<Document> =
documents.mapNotNull { document ->
mutex.withLock { documentEmbeddings[document]?.cosineSimilarity(embedding) }?.let {
document to it
}
}.sortedByDescending { (_, similarity) -> similarity }
.take(limit)
.map { (document, _) -> document }

private fun Embedding.cosineSimilarity(other: Embedding): Double {
val dotProduct = this.data.zip(other.data).sumOf { (a, b) -> (a * b).toDouble() }
val magnitudeA = sqrt(this.data.sumOf { (it * it).toDouble() })
val magnitudeB = sqrt(other.data.sumOf { (it * it).toDouble() })
return dotProduct / (magnitudeA * magnitudeB)
}
}
Original file line number Diff line number Diff line change
@@ -2,8 +2,8 @@ package com.xebia.functional.vectorstores

import com.xebia.functional.Document
import com.xebia.functional.embeddings.Embedding
import kotlin.jvm.JvmInline
import kotlinx.uuid.UUID
import kotlin.jvm.JvmInline

@JvmInline
value class DocumentVectorId(val id: UUID)
@@ -17,6 +17,9 @@ interface VectorStore {
*/
suspend fun addTexts(texts: List<String>): List<DocumentVectorId>

suspend fun addText(texts: String): List<DocumentVectorId> =
addTexts(listOf(texts))

/**
* Add documents to the vector store after running them through the embeddings
*
@@ -42,4 +45,4 @@ interface VectorStore {
* @return list of Documents most similar to the embedding
*/
suspend fun similaritySearchByVector(embedding: Embedding, limit: Int): List<Document>
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
package com.xebia.functional.chains

import arrow.core.raise.either
import com.xebia.functional.llm.openai.CompletionChoice
import com.xebia.functional.llm.openai.CompletionRequest
import com.xebia.functional.llm.openai.EmbeddingRequest
import com.xebia.functional.llm.openai.EmbeddingResult
import com.xebia.functional.llm.openai.OpenAIClient
import com.xebia.functional.llm.openai.*
import com.xebia.functional.prompt.PromptTemplate
import io.kotest.assertions.arrow.core.shouldBeLeft
import io.kotest.assertions.arrow.core.shouldBeRight
@@ -68,5 +64,9 @@ val llm = object : OpenAIClient {
else -> listOf(CompletionChoice("foo", 1, "bar"))
}

override suspend fun createChatCompletion(request: ChatCompletionRequest): ChatCompletionResponse {
TODO("Not yet implemented")
}

override suspend fun createEmbeddings(request: EmbeddingRequest): EmbeddingResult = TODO()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.xebia.functional.auto.agents

import com.xebia.functional.io.CommandExecutor
import com.xebia.functional.io.SYSTEM

fun Agent.Companion.wikipedia(): Agent<WikipediaResult> =
Agent.wikipedia(CommandExecutor.SYSTEM)
90 changes: 90 additions & 0 deletions src/jvmMain/kotlin/com/xebia/functional/io/JvmCommandExecutor.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package com.xebia.functional.io

import kotlinx.coroutines.runBlocking
import java.io.File
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runInterruptible
import kotlinx.coroutines.withContext

val CommandExecutor.Companion.SYSTEM: CommandExecutor
get() = JvmCommandExecutor

object JvmCommandExecutor : CommandExecutor {

// TODO Remove RunBlocking
private val platform: Platform by lazy {
val osName = System.getProperty("os.name").lowercase()

when {
osName.startsWith("windows") -> {
val uname = runBlocking {
try {
executeCommandAndCaptureOutput(
listOf("where", "uname"),
ExecuteCommandOptions(
directory = ".",
abortOnError = true,
redirectStderr = false,
trim = true,
),
)
executeCommandAndCaptureOutput(
listOf("uname", "-a"),
ExecuteCommandOptions(
directory = ".",
abortOnError = true,
redirectStderr = true,
trim = true,
),
)
} catch (e: Exception) {
""
}
}
// if (uname.isNotBlank()) println("uname: $uname")
when {
uname.startsWith("MSYS") -> Platform.LINUX
uname.startsWith("MINGW") -> Platform.LINUX
uname.startsWith("CYGWIN") -> Platform.LINUX
else -> Platform.WINDOWS
} // .also { println("platform is $it") }
}

osName.startsWith("linux") -> Platform.LINUX
osName.startsWith("mac") -> Platform.MACOS
osName.startsWith("darwin") -> Platform.MACOS
else -> error("unknown osName: $osName")
}
}


override suspend fun executeCommandAndCaptureOutput(command: List<String>, options: ExecuteCommandOptions): String =
withContext(Dispatchers.IO) {
val process = ProcessBuilder().apply {
command(command.filter { it.isNotBlank() })
directory(File(options.directory))
}.start()
val stdout = process.inputStream.bufferedReader().use { it.readText() }
val stderr = process.errorStream.bufferedReader().use { it.readText() }
val exitCode = runInterruptible { process.waitFor() }
if (options.abortOnError) assert(exitCode == 0)
val output = if (stderr.isBlank()) stdout else "$stdout $stderr"
if (options.trim) output.trim() else output
}

override suspend fun pwd(options: ExecuteCommandOptions): String =
File(".").absolutePath

override suspend fun findExecutable(executable: String): String =
when (platform) {
Platform.WINDOWS -> executeCommandAndCaptureOutput(
listOf("where", executable),
ExecuteCommandOptions(".", true, false, true)
)

else -> executeCommandAndCaptureOutput(
listOf("which", executable),
ExecuteCommandOptions(".", true, false, true)
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.xebia.functional.auto.agents

import com.xebia.functional.io.CommandExecutor
import com.xebia.functional.io.SYSTEM

fun Agent.Companion.wikipedia(): Agent<WikipediaResult> =
Agent.wikipedia(CommandExecutor.SYSTEM)
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.xebia.functional.io


import kotlinx.cinterop.refTo
import kotlinx.cinterop.toKString
import platform.posix.*

val CommandExecutor.Companion.SYSTEM: CommandExecutor
get() = LinuxCommandExecutor

object LinuxCommandExecutor : CommandExecutor {

/**
* https://stackoverflow.com/questions/57123836/kotlin-native-execute-command-and-get-the-output
*/
override suspend fun executeCommandAndCaptureOutput(
command: List<String>,
options: ExecuteCommandOptions
): String {
chdir(options.directory)
val commandToExecute = command.joinToString(separator = " ") { arg ->
if (arg.contains(" ")) "'$arg'" else arg
}
val redirect = if (options.redirectStderr) " 2>&1 " else ""
val fp = popen("$commandToExecute $redirect", "r") ?: error("Failed to run command: $command")

val stdout = buildString {
val buffer = ByteArray(4096)
while (true) {
val input = fgets(buffer.refTo(0), buffer.size, fp) ?: break
append(input.toKString())
}
}

val status = pclose(fp)
if (status != 0 && options.abortOnError) {
println(stdout)
throw Exception("Command `$command` failed with status $status${if (options.redirectStderr) ": $stdout" else ""}")
}

return if (options.trim) stdout.trim() else stdout
}

override suspend fun pwd(options: ExecuteCommandOptions): String =
executeCommandAndCaptureOutput(listOf("pwd"), options).trim()

override suspend fun findExecutable(executable: String): String = executable
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.xebia.functional.auto.agents

import com.xebia.functional.io.CommandExecutor
import com.xebia.functional.io.SYSTEM

fun Agent.Companion.wikipedia(): Agent<WikipediaResult> =
Agent.wikipedia(CommandExecutor.SYSTEM)
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package com.xebia.functional.io

import kotlinx.cinterop.refTo
import kotlinx.cinterop.toKString
import platform.posix.*

val CommandExecutor.Companion.SYSTEM: MacosCommandExecutor
get() = MacosCommandExecutor

object MacosCommandExecutor : CommandExecutor {

/**
* https://stackoverflow.com/questions/57123836/kotlin-native-execute-command-and-get-the-output
*/
override suspend fun executeCommandAndCaptureOutput(command: List<String>, options: ExecuteCommandOptions): String {
chdir(options.directory)
val commandToExecute = command.joinToString(separator = " ") { arg ->
if (arg.contains(" ")) "'$arg'" else arg
}
val redirect = if (options.redirectStderr) " 2>&1 " else ""
val fp = popen("$commandToExecute $redirect", "r") ?: error("Failed to run command: $command")

val stdout = buildString {
val buffer = ByteArray(4096)
while (true) {
val input = fgets(buffer.refTo(0), buffer.size, fp) ?: break
append(input.toKString())
}
}

val status = pclose(fp)
if (status != 0 && options.abortOnError) {
println(stdout)
throw Exception("Command `$command` failed with status $status${if (options.redirectStderr) ": $stdout" else ""}")
}

return if (options.trim) stdout.trim() else stdout
}

override suspend fun pwd(options: ExecuteCommandOptions): String =
executeCommandAndCaptureOutput(listOf("pwd"), options).trim()

override suspend fun findExecutable(executable: String): String =
executable
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.xebia.functional.auto.agents

import com.xebia.functional.io.CommandExecutor
import com.xebia.functional.io.SYSTEM

fun Agent.Companion.wikipedia(): Agent<WikipediaResult> =
Agent.wikipedia(CommandExecutor.SYSTEM)
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package com.xebia.functional.io

import kotlinx.cinterop.refTo
import kotlinx.cinterop.toKString
import kotlinx.coroutines.runBlocking
import platform.posix._pclose
import platform.posix._popen
import platform.posix.chdir
import platform.posix.fgets

val CommandExecutor.Companion.SYSTEM: CommandExecutor
get() = WindowsCommandExecutor

object WindowsCommandExecutor : CommandExecutor {
// TODO Remove RunBlocking
private val platform: Platform by lazy {
val uname = runBlocking {
try {
executeCommandAndCaptureOutput(
listOf("where", "uname"),
ExecuteCommandOptions(
directory = ".",
abortOnError = true,
redirectStderr = false,
trim = true,
),
)
executeCommandAndCaptureOutput(
listOf("uname", "-a"),
ExecuteCommandOptions(
directory = ".",
abortOnError = true,
redirectStderr = true,
trim = true,
),
)
} catch (e: Exception) {
""
}
}
// if (uname.isNotBlank()) println("uname: $uname")
when {
uname.startsWith("MSYS") -> Platform.LINUX
uname.startsWith("MINGW") -> Platform.LINUX
uname.startsWith("CYGWIN") -> Platform.LINUX
else -> Platform.WINDOWS
}//.also { println("platform is $it") }
}

/**
* https://stackoverflow.com/questions/57123836/kotlin-native-execute-command-and-get-the-output
*/
override suspend fun executeCommandAndCaptureOutput(command: List<String>, options: ExecuteCommandOptions): String {
chdir(options.directory)
val commandToExecute = command.joinToString(separator = " ") { arg ->
if (arg.contains(" ") || arg.contains("%")) "\"$arg\"" else arg
}
println("executing: $commandToExecute")
val redirect = if (options.redirectStderr) " 2>&1 " else ""
val fp = _popen("$commandToExecute $redirect", "r") ?: error("Failed to run command: $command")

val stdout = buildString {
val buffer = ByteArray(4096)
while (true) {
val input = fgets(buffer.refTo(0), buffer.size, fp) ?: break
append(input.toKString())
}
}

val status = _pclose(fp)
if (status != 0 && options.abortOnError) {
println(stdout)
println("failed to run: $commandToExecute")
throw Exception("Command `$command` failed with status $status${if (options.redirectStderr) ": $stdout" else ""}")
}

return if (options.trim) stdout.trim() else stdout
}


override suspend fun pwd(options: ExecuteCommandOptions): String = when (platform) {
Platform.WINDOWS -> executeCommandAndCaptureOutput(listOf("echo", "%cd%"), options).trim('"', ' ')
else -> executeCommandAndCaptureOutput(listOf("pwd"), options).trim()
}

override suspend fun findExecutable(executable: String): String =
executable
}

0 comments on commit bb3d9e0

Please sign in to comment.