From d46e052218799ad1b8941eb7001af4d1c3063473 Mon Sep 17 00:00:00 2001 From: Javi Pacheco Date: Wed, 16 Aug 2023 11:26:07 +0200 Subject: [PATCH] New PromptBuilder returning list of messages (#326) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * New PromptBuilder returning list of messages * Comments addressed --------- Co-authored-by: Raรบl Raja Martรญnez --- .../com/xebia/functional/xef/llm/Chat.kt | 16 ++- .../functional/xef/llm/models/chat/Message.kt | 12 +- .../xebia/functional/xef/prompt/Builder.kt | 22 ---- .../functional/xef/prompt/PromptBuilder.kt | 25 ++++ .../xef/prompt/evaluator/PromptEvaluator.kt | 16 +-- .../xef/prompt/experts/ExpertSystem.kt | 20 --- .../xef/prompt/expressions/Expression.kt | 29 ++--- .../com/xebia/functional/xef/prompt/models.kt | 45 ------- .../xef/prompt/templates/templates.kt | 57 +++++---- .../xef/prompt/PromptBuilderSpec.kt | 69 ++++++++++ .../auto/expressions/WorkoutPlanProgram.kt | 32 +++-- .../xef/auto/prompts/ExpertSystemExample.kt | 43 ++++--- .../xef/auto/reasoning/CreatePRDescription.kt | 11 +- .../xef/auto/reasoning/ReActExample.kt | 12 +- .../auto/llm/openai/DeserializerLLMAgent.kt | 42 +++++++ .../auto/llm/openai/OpenAIScopeExtensions.kt | 14 +++ .../xef/reasoning/code/DiffSummary.kt | 35 +++--- .../xef/reasoning/serpapi/SearchTool.kt | 31 ++--- .../xef/reasoning/text/summarize/Summarize.kt | 45 ++++--- .../functional/xef/reasoning/tools/LLMTool.kt | 23 ++-- .../xef/reasoning/tools/ReActAgent.kt | 118 ++++++++---------- .../xef/reasoning/tools/ToolSelection.kt | 39 +++--- .../reasoning/filesystem/ProduceTextFile.kt | 18 +-- 23 files changed, 425 insertions(+), 349 deletions(-) delete mode 100644 core/src/commonMain/kotlin/com/xebia/functional/xef/prompt/Builder.kt create mode 100644 core/src/commonMain/kotlin/com/xebia/functional/xef/prompt/PromptBuilder.kt delete mode 100644 core/src/commonMain/kotlin/com/xebia/functional/xef/prompt/experts/ExpertSystem.kt delete mode 100644 core/src/commonMain/kotlin/com/xebia/functional/xef/prompt/models.kt create mode 100644 core/src/commonTest/kotlin/com/xebia/functional/xef/prompt/PromptBuilderSpec.kt diff --git a/core/src/commonMain/kotlin/com/xebia/functional/xef/llm/Chat.kt b/core/src/commonMain/kotlin/com/xebia/functional/xef/llm/Chat.kt index a4d59b1ba..6aefeb751 100644 --- a/core/src/commonMain/kotlin/com/xebia/functional/xef/llm/Chat.kt +++ b/core/src/commonMain/kotlin/com/xebia/functional/xef/llm/Chat.kt @@ -9,6 +9,9 @@ import com.xebia.functional.xef.auto.PromptConfiguration import com.xebia.functional.xef.llm.models.chat.* import com.xebia.functional.xef.llm.models.functions.CFunction import com.xebia.functional.xef.prompt.Prompt +import com.xebia.functional.xef.prompt.buildPrompt +import com.xebia.functional.xef.prompt.templates.assistant +import com.xebia.functional.xef.prompt.templates.user import com.xebia.functional.xef.vectorstores.Memory import io.ktor.util.date.* import kotlinx.coroutines.flow.Flow @@ -83,6 +86,15 @@ interface Chat : LLM { promptConfiguration: PromptConfiguration = PromptConfiguration.DEFAULTS ): List = promptMessages(Prompt(question), scope, functions, promptConfiguration) + @AiDsl + suspend fun promptMessage( + prompt: Prompt, + scope: Conversation, + promptConfiguration: PromptConfiguration = PromptConfiguration.DEFAULTS + ): String = + promptMessages(prompt, scope, emptyList(), promptConfiguration).firstOrNull() + ?: throw AIError.NoResponse() + @AiDsl suspend fun promptMessages( prompt: Prompt, @@ -160,7 +172,7 @@ interface Chat : LLM { suspend fun String.toMessages(): List = Prompt(this).toMessages() - suspend fun Prompt.toMessages(): List = listOf(Message.userMessage { message }) + suspend fun Prompt.toMessages(): List = buildPrompt { +user(message) } private suspend fun addMemoriesAfterStream( request: ChatCompletionRequest, @@ -332,7 +344,7 @@ interface Chat : LLM { val ctxTruncated: String = modelType.encoding.truncateText(ctx, maxContextTokens) - listOf(Message.assistantMessage { ctxTruncated }) + buildPrompt { +assistant(ctxTruncated) } } else { emptyList() } diff --git a/core/src/commonMain/kotlin/com/xebia/functional/xef/llm/models/chat/Message.kt b/core/src/commonMain/kotlin/com/xebia/functional/xef/llm/models/chat/Message.kt index 2a78143bf..bbee4b6f8 100644 --- a/core/src/commonMain/kotlin/com/xebia/functional/xef/llm/models/chat/Message.kt +++ b/core/src/commonMain/kotlin/com/xebia/functional/xef/llm/models/chat/Message.kt @@ -1,14 +1,10 @@ package com.xebia.functional.xef.llm.models.chat +import kotlin.jvm.JvmStatic + data class Message(val role: Role, val content: String, val name: String) { companion object { - suspend fun systemMessage(message: suspend () -> String) = - Message(role = Role.SYSTEM, content = message(), name = Role.SYSTEM.name) - - suspend fun userMessage(message: suspend () -> String) = - Message(role = Role.USER, content = message(), name = Role.USER.name) - - suspend fun assistantMessage(message: suspend () -> String) = - Message(role = Role.ASSISTANT, content = message(), name = Role.ASSISTANT.name) + @JvmStatic + fun apply(role: Role, content: String, name: String): Message = Message(role, content, name) } } diff --git a/core/src/commonMain/kotlin/com/xebia/functional/xef/prompt/Builder.kt b/core/src/commonMain/kotlin/com/xebia/functional/xef/prompt/Builder.kt deleted file mode 100644 index 39a9f3c7a..000000000 --- a/core/src/commonMain/kotlin/com/xebia/functional/xef/prompt/Builder.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.xebia.functional.xef.prompt - -open class PromptBuilder { - private val items = mutableListOf() - - val emptyLine: String = "" - - operator fun Prompt.unaryPlus() { - items.add(this) - } - - operator fun String.unaryPlus() { - items.add(Prompt(this)) - } - - protected open fun preprocess(elements: List): List = elements - - fun build(): Prompt = - buildString { preprocess(items).forEach { appendLine(it.message) } }.let { Prompt(it) } -} - -fun buildPrompt(block: PromptBuilder.() -> Unit): Prompt = PromptBuilder().apply { block() }.build() diff --git a/core/src/commonMain/kotlin/com/xebia/functional/xef/prompt/PromptBuilder.kt b/core/src/commonMain/kotlin/com/xebia/functional/xef/prompt/PromptBuilder.kt new file mode 100644 index 000000000..559a38409 --- /dev/null +++ b/core/src/commonMain/kotlin/com/xebia/functional/xef/prompt/PromptBuilder.kt @@ -0,0 +1,25 @@ +package com.xebia.functional.xef.prompt + +import com.xebia.functional.xef.llm.models.chat.Message +import com.xebia.functional.xef.llm.models.chat.Role + +open class PromptBuilder { + private val items = mutableListOf() + + operator fun Message.unaryPlus() { + items.add(this) + } + + operator fun List.unaryPlus() { + items.addAll(this) + } + + protected open fun preprocess(elements: List): List = elements + + fun build(): List = preprocess(items) +} + +fun String.message(role: Role): Message = Message(role, this, role.name) + +fun buildPrompt(block: PromptBuilder.() -> Unit): List = + PromptBuilder().apply { block() }.build() diff --git a/core/src/commonMain/kotlin/com/xebia/functional/xef/prompt/evaluator/PromptEvaluator.kt b/core/src/commonMain/kotlin/com/xebia/functional/xef/prompt/evaluator/PromptEvaluator.kt index 30b7cbdb7..f8732243f 100644 --- a/core/src/commonMain/kotlin/com/xebia/functional/xef/prompt/evaluator/PromptEvaluator.kt +++ b/core/src/commonMain/kotlin/com/xebia/functional/xef/prompt/evaluator/PromptEvaluator.kt @@ -3,7 +3,9 @@ package com.xebia.functional.xef.prompt.evaluator import com.xebia.functional.xef.auto.Conversation import com.xebia.functional.xef.auto.PromptConfiguration import com.xebia.functional.xef.llm.Chat -import com.xebia.functional.xef.llm.models.chat.Message +import com.xebia.functional.xef.prompt.buildPrompt +import com.xebia.functional.xef.prompt.templates.system +import com.xebia.functional.xef.prompt.templates.user import kotlinx.serialization.Serializable import kotlinx.serialization.builtins.MapSerializer import kotlinx.serialization.builtins.serializer @@ -162,12 +164,12 @@ ${scoreConfig.joinToString("\n") { printReturn(it) }} val result: List = model.promptMessages( messages = - listOf( - Message.systemMessage { message }, - Message.userMessage { "Set Prompt = $prompt" }, - Message.userMessage { "Set Response = $response" }, - Message.userMessage { "Evaluate(Prompt, Response)" } - ), + buildPrompt { + +system(message) + +user("Set Prompt = $prompt") + +user("Set Response = $response") + +user("Evaluate(Prompt, Response)") + }, scope = conversation, promptConfiguration = promptConfiguration, ) diff --git a/core/src/commonMain/kotlin/com/xebia/functional/xef/prompt/experts/ExpertSystem.kt b/core/src/commonMain/kotlin/com/xebia/functional/xef/prompt/experts/ExpertSystem.kt deleted file mode 100644 index 8f8660a34..000000000 --- a/core/src/commonMain/kotlin/com/xebia/functional/xef/prompt/experts/ExpertSystem.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.xebia.functional.xef.prompt.experts - -import com.xebia.functional.xef.prompt.Prompt -import com.xebia.functional.xef.prompt.PromptBuilder -import com.xebia.functional.xef.prompt.templates.steps -import kotlin.jvm.JvmName -import kotlin.jvm.JvmStatic - -object ExpertSystem { - @JvmName("prompt") - @JvmStatic - operator fun invoke(system: String, query: String, instructions: List): Prompt = - PromptBuilder() - .apply { - +system - +query - +steps { instructions.forEach { +it } } - } - .build() -} diff --git a/core/src/commonMain/kotlin/com/xebia/functional/xef/prompt/expressions/Expression.kt b/core/src/commonMain/kotlin/com/xebia/functional/xef/prompt/expressions/Expression.kt index aa5a22099..00158b55e 100644 --- a/core/src/commonMain/kotlin/com/xebia/functional/xef/prompt/expressions/Expression.kt +++ b/core/src/commonMain/kotlin/com/xebia/functional/xef/prompt/expressions/Expression.kt @@ -4,6 +4,9 @@ import com.xebia.functional.xef.auto.Conversation import com.xebia.functional.xef.auto.PromptConfiguration import com.xebia.functional.xef.llm.ChatWithFunctions import com.xebia.functional.xef.llm.models.chat.Message +import com.xebia.functional.xef.prompt.buildPrompt +import com.xebia.functional.xef.prompt.templates.assistant +import com.xebia.functional.xef.prompt.templates.system import io.github.oshai.kotlinlogging.KLogger import io.github.oshai.kotlinlogging.KotlinLogging @@ -19,16 +22,8 @@ class Expression( private val generationKeys: MutableList = mutableListOf() - suspend fun system(message: suspend () -> String) { - messages.add(Message.systemMessage(message)) - } - - suspend fun user(message: suspend () -> String) { - messages.add(Message.userMessage(message)) - } - - suspend fun assistant(message: suspend () -> String) { - messages.add(Message.assistantMessage(message)) + fun addMessages(newMessages: List) { + messages.addAll(newMessages) } fun prompt(key: String): String { @@ -40,14 +35,12 @@ class Expression( promptConfiguration: PromptConfiguration = PromptConfiguration.DEFAULTS ): ExpressionResult { block() - val prelude = - listOf( - Message.systemMessage { "You are an expert in replacing variables in templates" }, - ) - val instructionMessages = - listOf( - Message.assistantMessage { "I will replace all placeholders in the message" }, - ) + val prelude = buildPrompt { +system("You are an expert in replacing variables in templates") } + + val instructionMessages = buildPrompt { + +assistant("I will replace all placeholders in the message") + } + val values: ReplacedValues = model.prompt( messages = prelude + messages + instructionMessages, diff --git a/core/src/commonMain/kotlin/com/xebia/functional/xef/prompt/models.kt b/core/src/commonMain/kotlin/com/xebia/functional/xef/prompt/models.kt deleted file mode 100644 index 9ade5c4cb..000000000 --- a/core/src/commonMain/kotlin/com/xebia/functional/xef/prompt/models.kt +++ /dev/null @@ -1,45 +0,0 @@ -package com.xebia.functional.xef.prompt - -import kotlinx.serialization.Serializable - -enum class Type { - human, - ai, - system, - chat -} - -@Serializable -sealed class Message { - abstract val content: String - - abstract fun format(): String - - fun type(): Type = - when (this) { - is HumanMessage -> Type.human - is AIMessage -> Type.ai - is SystemMessage -> Type.system - is ChatMessage -> Type.chat - } -} - -data class HumanMessage(override val content: String) : Message() { - override fun format(): String = "${type().name.capitalized()}: $content" -} - -data class AIMessage(override val content: String) : Message() { - override fun format(): String = "${type().name.uppercase()}: $content" -} - -data class SystemMessage(override val content: String) : Message() { - override fun format(): String = "${type().name.capitalized()}: $content" -} - -data class ChatMessage(override val content: String, val role: String) : Message() { - override fun format(): String = "$role: $content" -} - -private fun String.capitalized(): String = replaceFirstChar { - if (it.isLowerCase()) it.titlecase() else it.toString() -} diff --git a/core/src/commonMain/kotlin/com/xebia/functional/xef/prompt/templates/templates.kt b/core/src/commonMain/kotlin/com/xebia/functional/xef/prompt/templates/templates.kt index 1a1de44b7..8958d7e35 100644 --- a/core/src/commonMain/kotlin/com/xebia/functional/xef/prompt/templates/templates.kt +++ b/core/src/commonMain/kotlin/com/xebia/functional/xef/prompt/templates/templates.kt @@ -1,41 +1,48 @@ package com.xebia.functional.xef.prompt.templates -import com.xebia.functional.xef.prompt.Prompt +import com.xebia.functional.xef.llm.models.chat.Message +import com.xebia.functional.xef.llm.models.chat.Role import com.xebia.functional.xef.prompt.PromptBuilder -import com.xebia.functional.xef.prompt.prompt +import com.xebia.functional.xef.prompt.message -fun youAre(role: String, talkingTo: String): Prompt = - "You are a $role talking with a $talkingTo".prompt() +fun system(context: String): Message = context.message(Role.SYSTEM) -class StepsBuilder : PromptBuilder() { - override fun preprocess(elements: List): List = - elements.mapIndexed { ix, elt -> Prompt("${ix + 1} - ${elt.message}") } +fun assistant(context: String): Message = context.message(Role.ASSISTANT) + +fun user(context: String): Message = context.message(Role.USER) + +fun youAre(act: String, talkingTo: String, role: Role = Role.ASSISTANT): Message = + "You are a $act talking with a $talkingTo".message(role) + +class StepsMessageBuilder : PromptBuilder() { + override fun preprocess(elements: List): List = + elements.mapIndexed { ix, elt -> "${ix + 1} - ${elt.content}".message(elt.role) } } -fun steps(inside: PromptBuilder.() -> Unit): Prompt = StepsBuilder().apply { inside() }.build() +fun steps(inside: PromptBuilder.() -> Unit): List = + StepsMessageBuilder().apply { inside() }.build() -fun writeSequenceOf(content: String): Prompt = +fun writeSequenceOf( + content: String, + prefix: String = "Step", + role: Role = Role.ASSISTANT +): Message = """ Write a sequence of $content in the following format: - Step 1 - ... - Step 2 - ... + $prefix 1 - ... + $prefix 2 - ... ... - Step N - ... - """ - .trimIndent() - .prompt() - -fun writeListOf(content: String): Prompt = - """ - Write a list of $content in the following format: - 1. ... - 2. ... - n. ... + $prefix N - ... """ .trimIndent() - .prompt() + .message(role) -fun code(code: String, delimiter: Delimiter?, name: String? = null): Prompt = +fun code( + code: String, + delimiter: Delimiter?, + name: String? = null, + role: Role = Role.ASSISTANT +): Message = """ ${name ?: "" } ${delimiter?.start() ?: ""} @@ -43,7 +50,7 @@ fun code(code: String, delimiter: Delimiter?, name: String? = null): Prompt = ${delimiter?.end() ?: ""} """ .trimIndent() - .prompt() + .message(role) enum class Delimiter { ThreeBackticks, diff --git a/core/src/commonTest/kotlin/com/xebia/functional/xef/prompt/PromptBuilderSpec.kt b/core/src/commonTest/kotlin/com/xebia/functional/xef/prompt/PromptBuilderSpec.kt new file mode 100644 index 000000000..74fe21b79 --- /dev/null +++ b/core/src/commonTest/kotlin/com/xebia/functional/xef/prompt/PromptBuilderSpec.kt @@ -0,0 +1,69 @@ +package com.xebia.functional.xef.prompt + +import com.xebia.functional.xef.llm.models.chat.Role +import com.xebia.functional.xef.prompt.templates.assistant +import com.xebia.functional.xef.prompt.templates.steps +import com.xebia.functional.xef.prompt.templates.system +import com.xebia.functional.xef.prompt.templates.user +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.shouldBe + +class PromptBuilderSpec : + StringSpec({ + "buildMessages should return the expected messages" { + val messages = buildPrompt { + +system("Test System") + +user("Test Query") + +assistant("Test Assistant") + } + + val messagesExpected = + listOf( + "Test System".message(Role.SYSTEM), + "Test Query".message(Role.USER), + "Test Assistant".message(Role.ASSISTANT) + ) + + messages shouldBe messagesExpected + } + + "buildMessages should return the expected messages when using forEach" { + val instructions = listOf("instruction 1", "instruction 2") + + val messages = buildPrompt { + +system("Test System") + +user("Test Query") + instructions.forEach { +assistant(it) } + } + + val messagesExpected = + listOf( + "Test System".message(Role.SYSTEM), + "Test Query".message(Role.USER), + "instruction 1".message(Role.ASSISTANT), + "instruction 2".message(Role.ASSISTANT) + ) + + messages shouldBe messagesExpected + } + + "buildMessages should return the expected messages when using steps with the number for every step" { + val instructions = listOf("instruction 1", "instruction 2") + + val messages = buildPrompt { + +system("Test System") + +user("Test Query") + +steps { instructions.forEach { +assistant(it) } } + } + + val messagesExpected = + listOf( + "Test System".message(Role.SYSTEM), + "Test Query".message(Role.USER), + "1 - instruction 1".message(Role.ASSISTANT), + "2 - instruction 2".message(Role.ASSISTANT) + ) + + messages shouldBe messagesExpected + } + }) diff --git a/examples/kotlin/src/main/kotlin/com/xebia/functional/xef/auto/expressions/WorkoutPlanProgram.kt b/examples/kotlin/src/main/kotlin/com/xebia/functional/xef/auto/expressions/WorkoutPlanProgram.kt index a01959f37..9cf944a9a 100644 --- a/examples/kotlin/src/main/kotlin/com/xebia/functional/xef/auto/expressions/WorkoutPlanProgram.kt +++ b/examples/kotlin/src/main/kotlin/com/xebia/functional/xef/auto/expressions/WorkoutPlanProgram.kt @@ -3,8 +3,12 @@ package com.xebia.functional.xef.auto.expressions import com.xebia.functional.xef.auto.Conversation import com.xebia.functional.xef.auto.llm.openai.OpenAI import com.xebia.functional.xef.llm.ChatWithFunctions +import com.xebia.functional.xef.prompt.buildPrompt import com.xebia.functional.xef.prompt.expressions.Expression import com.xebia.functional.xef.prompt.expressions.ExpressionResult +import com.xebia.functional.xef.prompt.templates.assistant +import com.xebia.functional.xef.prompt.templates.system +import com.xebia.functional.xef.prompt.templates.user import com.xebia.functional.xef.reasoning.serpapi.Search import com.xebia.functional.xef.reasoning.tools.LLMTool import com.xebia.functional.xef.reasoning.tools.Tool @@ -19,26 +23,30 @@ suspend fun taskSplitter( scope = scope, model = model, block = { - system { "You are a professional task planner" } - user { - """ + addMessages( + buildPrompt { + +system("You are a professional task planner") + +user( + """ |I want to achieve: """ - .trimMargin() - } - user { prompt } - assistant { "I have access to all these tool" } - tools.forEach { assistant { "${it.name}: ${it.description}" } } - assistant { - """ + .trimMargin() + ) + +user(prompt) + +assistant("I have access to all these tool") + tools.forEach { +assistant("${it.name}: ${it.description}") } + +assistant( + """ |I will break down your task into 3 tasks to make progress and help you accomplish this goal |using the tools that I have available. |1: ${prompt("task1")} |2: ${prompt("task2")} |3: ${prompt("task3")} """ - .trimMargin() - } + .trimMargin() + ) + } + ) } ) diff --git a/examples/kotlin/src/main/kotlin/com/xebia/functional/xef/auto/prompts/ExpertSystemExample.kt b/examples/kotlin/src/main/kotlin/com/xebia/functional/xef/auto/prompts/ExpertSystemExample.kt index e16b295bb..7cd2e5976 100644 --- a/examples/kotlin/src/main/kotlin/com/xebia/functional/xef/auto/prompts/ExpertSystemExample.kt +++ b/examples/kotlin/src/main/kotlin/com/xebia/functional/xef/auto/prompts/ExpertSystemExample.kt @@ -1,9 +1,11 @@ package com.xebia.functional.xef.auto.prompts -import com.xebia.functional.xef.auto.conversation import com.xebia.functional.xef.auto.llm.openai.OpenAI import com.xebia.functional.xef.auto.llm.openai.prompt -import com.xebia.functional.xef.prompt.experts.ExpertSystem +import com.xebia.functional.xef.prompt.buildPrompt +import com.xebia.functional.xef.prompt.templates.assistant +import com.xebia.functional.xef.prompt.templates.system +import com.xebia.functional.xef.prompt.templates.user import kotlinx.serialization.Serializable @Serializable data class FinalSolution(val resolvedCode: String) @@ -26,25 +28,22 @@ suspend fun main() = println("solution: ${solution}") } -private fun functionProgrammerFix(code: String) = - ExpertSystem( - system = "You are an expert functional programmer.", - query = - """| +private fun functionProgrammerFix(code: String) = buildPrompt { + +system("You are an expert functional programmer") + +user("""| |This code may be unsafe: |$code - """ - .trimMargin(), - instructions = - listOf( - "You never throw exceptions.", - "When you need to use null you always use it in the context of safe access through nullable types.", - "You never use `for` `while` or loops in general, prefer tail recursion.", - "You never use mutable state.", - "Return a concise solution that fixes the problems in the code.", - "Code and text returned in JSON are always properly escaped", - "The code should not be inside a literal string with \"\"\"", - "In your response you return exclusively the fixed code and no other text.", - "Provide the fixed code as a solution that is as concise as possible." - ) - ) + """) + listOf( + "You never throw exceptions.", + "When you need to use null you always use it in the context of safe access through nullable types.", + "You never use `for` `while` or loops in general, prefer tail recursion.", + "You never use mutable state.", + "Return a concise solution that fixes the problems in the code.", + "Code and text returned in JSON are always properly escaped", + "The code should not be inside a literal string with \"\"\"", + "In your response you return exclusively the fixed code and no other text.", + "Provide the fixed code as a solution that is as concise as possible." + ) + .forEach { +assistant(it) } +} diff --git a/examples/kotlin/src/main/kotlin/com/xebia/functional/xef/auto/reasoning/CreatePRDescription.kt b/examples/kotlin/src/main/kotlin/com/xebia/functional/xef/auto/reasoning/CreatePRDescription.kt index fed065be8..32120a561 100644 --- a/examples/kotlin/src/main/kotlin/com/xebia/functional/xef/auto/reasoning/CreatePRDescription.kt +++ b/examples/kotlin/src/main/kotlin/com/xebia/functional/xef/auto/reasoning/CreatePRDescription.kt @@ -1,7 +1,8 @@ package com.xebia.functional.xef.auto.reasoning import com.xebia.functional.xef.auto.llm.openai.OpenAI -import com.xebia.functional.xef.llm.models.chat.Message +import com.xebia.functional.xef.prompt.buildPrompt +import com.xebia.functional.xef.prompt.templates.user import com.xebia.functional.xef.reasoning.code.Code import com.xebia.functional.xef.reasoning.tools.ReActAgent @@ -22,11 +23,11 @@ suspend fun main() { ) val prDescription = agent.run( - listOf( - Message.userMessage { + buildPrompt { + +user( "Create a PR description for https://patch-diff.githubusercontent.com/raw/xebia-functional/xef/pull/283.diff" - } - ) + ) + } ) println(prDescription) } diff --git a/examples/kotlin/src/main/kotlin/com/xebia/functional/xef/auto/reasoning/ReActExample.kt b/examples/kotlin/src/main/kotlin/com/xebia/functional/xef/auto/reasoning/ReActExample.kt index 0498d97e5..6fd788ca7 100644 --- a/examples/kotlin/src/main/kotlin/com/xebia/functional/xef/auto/reasoning/ReActExample.kt +++ b/examples/kotlin/src/main/kotlin/com/xebia/functional/xef/auto/reasoning/ReActExample.kt @@ -1,8 +1,8 @@ package com.xebia.functional.xef.auto.reasoning -import com.xebia.functional.xef.auto.conversation import com.xebia.functional.xef.auto.llm.openai.OpenAI -import com.xebia.functional.xef.llm.models.chat.Message +import com.xebia.functional.xef.prompt.buildPrompt +import com.xebia.functional.xef.prompt.templates.user import com.xebia.functional.xef.reasoning.serpapi.Search import com.xebia.functional.xef.reasoning.tools.LLMTool import com.xebia.functional.xef.reasoning.tools.ReActAgent @@ -33,11 +33,11 @@ suspend fun main() { ) val result = reActAgent.run( - listOf( - Message.userMessage { + buildPrompt { + +user( "Find and multiply the number of Leonardo di Caprio's girlfriends by the number of Metallica albums" - } - ) + ) + } ) println(result) } diff --git a/openai/src/commonMain/kotlin/com/xebia/functional/xef/auto/llm/openai/DeserializerLLMAgent.kt b/openai/src/commonMain/kotlin/com/xebia/functional/xef/auto/llm/openai/DeserializerLLMAgent.kt index 1ad75d3ad..bc474dc7c 100644 --- a/openai/src/commonMain/kotlin/com/xebia/functional/xef/auto/llm/openai/DeserializerLLMAgent.kt +++ b/openai/src/commonMain/kotlin/com/xebia/functional/xef/auto/llm/openai/DeserializerLLMAgent.kt @@ -4,6 +4,7 @@ import com.xebia.functional.xef.auto.AiDsl import com.xebia.functional.xef.auto.Conversation import com.xebia.functional.xef.auto.PromptConfiguration import com.xebia.functional.xef.llm.ChatWithFunctions +import com.xebia.functional.xef.llm.models.chat.Message import com.xebia.functional.xef.prompt.Prompt import kotlinx.serialization.KSerializer import kotlinx.serialization.SerializationException @@ -68,3 +69,44 @@ suspend fun Conversation.prompt( promptConfiguration ) } + +/** + * Run a [List] describes the task you want to solve within the context of [AIScope]. + * Returns a value of [A] where [A] **has to be** annotated with + * [kotlinx.serialization.Serializable]. + * + * @throws SerializationException if serializer cannot be created (provided [A] or its type argument + * is not serializable). + * @throws IllegalArgumentException if any of [A]'s type arguments contains star projection. + */ +@AiDsl +suspend inline fun Conversation.prompt( + model: ChatWithFunctions, + messages: List, + json: Json = Json { + ignoreUnknownKeys = true + isLenient = true + }, + promptConfiguration: PromptConfiguration = PromptConfiguration.DEFAULTS, +): A = prompt(model, messages, serializer(), json, promptConfiguration) + +@AiDsl +suspend fun Conversation.prompt( + model: ChatWithFunctions, + messages: List, + serializer: KSerializer, + json: Json = Json { + ignoreUnknownKeys = true + isLenient = true + }, + promptConfiguration: PromptConfiguration = PromptConfiguration.DEFAULTS, +): A { + val functions = model.generateCFunction(serializer.descriptor) + return model.prompt( + messages = messages, + scope = this, + functions = functions, + serializer = { json.decodeFromString(serializer, it) }, + promptConfiguration = promptConfiguration + ) +} diff --git a/openai/src/commonMain/kotlin/com/xebia/functional/xef/auto/llm/openai/OpenAIScopeExtensions.kt b/openai/src/commonMain/kotlin/com/xebia/functional/xef/auto/llm/openai/OpenAIScopeExtensions.kt index a285ab746..9f96f784e 100644 --- a/openai/src/commonMain/kotlin/com/xebia/functional/xef/auto/llm/openai/OpenAIScopeExtensions.kt +++ b/openai/src/commonMain/kotlin/com/xebia/functional/xef/auto/llm/openai/OpenAIScopeExtensions.kt @@ -5,6 +5,7 @@ import com.xebia.functional.xef.auto.Conversation import com.xebia.functional.xef.auto.PromptConfiguration import com.xebia.functional.xef.llm.Chat import com.xebia.functional.xef.llm.ChatWithFunctions +import com.xebia.functional.xef.llm.models.chat.Message import com.xebia.functional.xef.llm.models.functions.CFunction import com.xebia.functional.xef.prompt.Prompt import kotlinx.serialization.serializer @@ -70,6 +71,19 @@ suspend inline fun Conversation.prompt( promptConfiguration = promptConfiguration ) +@AiDsl +suspend inline fun Conversation.prompt( + messages: List, + model: ChatWithFunctions = OpenAI().DEFAULT_SERIALIZATION, + promptConfiguration: PromptConfiguration = PromptConfiguration.DEFAULTS, +): A = + prompt( + model = model, + messages = messages, + serializer = serializer(), + promptConfiguration = promptConfiguration + ) + @AiDsl suspend inline fun Conversation.image( prompt: String, diff --git a/reasoning/src/commonMain/kotlin/com/xebia/functional/xef/reasoning/code/DiffSummary.kt b/reasoning/src/commonMain/kotlin/com/xebia/functional/xef/reasoning/code/DiffSummary.kt index d7a495485..f1ed80bb7 100644 --- a/reasoning/src/commonMain/kotlin/com/xebia/functional/xef/reasoning/code/DiffSummary.kt +++ b/reasoning/src/commonMain/kotlin/com/xebia/functional/xef/reasoning/code/DiffSummary.kt @@ -7,6 +7,10 @@ import com.xebia.functional.xef.llm.Chat import com.xebia.functional.xef.llm.ChatWithFunctions import com.xebia.functional.xef.llm.models.chat.Message import com.xebia.functional.xef.prompt.Prompt +import com.xebia.functional.xef.prompt.buildPrompt +import com.xebia.functional.xef.prompt.templates.assistant +import com.xebia.functional.xef.prompt.templates.system +import com.xebia.functional.xef.prompt.templates.user import com.xebia.functional.xef.reasoning.text.summarize.Summarize import com.xebia.functional.xef.reasoning.text.summarize.SummaryLength import com.xebia.functional.xef.reasoning.tools.Tool @@ -76,21 +80,21 @@ class DiffSummary( private suspend fun createPRDescription(summary: String): String = chat.promptMessage( messages = - listOf( - Message.systemMessage { "Create Pull Request Description" }, - Message.assistantMessage { + buildPrompt { + +system("Create Pull Request Description") + +assistant( "I will roleplay as an expert software engineer implementing a service to read a .diff file from a URL and create a Pull Request description with an automatically inferred user intent." - }, - Message.assistantMessage { + ) + +assistant( "I will use the following program to return a Pull Request description to the user:" - }, - systemPrompt(), - Message.userMessage { "Set Summary = $summary" }, - Message.userMessage { "CreatePRDescription()" }, - Message.assistantMessage { + ) + +systemPrompt() + +user("Set Summary = $summary") + +user("CreatePRDescription()") + +assistant( "A great, concise and neutral toned Pull Request description for this summary is:" - } - ), + ) + }, scope = scope ) @@ -99,8 +103,8 @@ class DiffSummary( } companion object { - suspend fun systemPrompt(): Message = - Message.systemMessage { + fun systemPrompt(): List = buildPrompt { + +system( // language=yaml """ # CreatePRDescription @@ -125,6 +129,7 @@ class DiffSummary( Instructions: "When asked to implement this functionality, please carefully follow the instructions above, ensuring that the user's intent is automatically inferred and added to the PR description. ๐Ÿ™" """ .trimIndent() - } + ) + } } } diff --git a/reasoning/src/commonMain/kotlin/com/xebia/functional/xef/reasoning/serpapi/SearchTool.kt b/reasoning/src/commonMain/kotlin/com/xebia/functional/xef/reasoning/serpapi/SearchTool.kt index 5d66132d2..b55b2eeb9 100644 --- a/reasoning/src/commonMain/kotlin/com/xebia/functional/xef/reasoning/serpapi/SearchTool.kt +++ b/reasoning/src/commonMain/kotlin/com/xebia/functional/xef/reasoning/serpapi/SearchTool.kt @@ -2,7 +2,10 @@ package com.xebia.functional.xef.reasoning.serpapi import com.xebia.functional.xef.auto.Conversation import com.xebia.functional.xef.llm.Chat -import com.xebia.functional.xef.llm.models.chat.Message +import com.xebia.functional.xef.prompt.buildPrompt +import com.xebia.functional.xef.prompt.templates.assistant +import com.xebia.functional.xef.prompt.templates.system +import com.xebia.functional.xef.prompt.templates.user import com.xebia.functional.xef.reasoning.tools.Tool import kotlin.jvm.JvmSynthetic @@ -26,20 +29,18 @@ interface SearchTool : Tool { return model .promptMessages( messages = - listOf(Message.systemMessage { "Search results:" }) + - docs.searchResults.take(maxResultsInContext).flatMap { - listOf( - Message.systemMessage { "Title: ${it.title}" }, - Message.systemMessage { "Source: ${it.source}" }, - Message.systemMessage { "Content: ${it.document}" }, - ) - } + - listOf( - Message.userMessage { "input: $input" }, - Message.assistantMessage { - "I will select the best search results and reply with information relevant to the `input`" - } - ), + buildPrompt { + +system("Search results:") + docs.searchResults.take(maxResultsInContext).forEach { + +system("Title: ${it.title}") + +system("Source: ${it.source}") + +system("Content: ${it.document}") + } + +user("input: $input") + +assistant( + "I will select the best search results and reply with information relevant to the `input`" + ) + }, scope = scope, ) .firstOrNull() diff --git a/reasoning/src/commonMain/kotlin/com/xebia/functional/xef/reasoning/text/summarize/Summarize.kt b/reasoning/src/commonMain/kotlin/com/xebia/functional/xef/reasoning/text/summarize/Summarize.kt index 17df264cc..56d516173 100644 --- a/reasoning/src/commonMain/kotlin/com/xebia/functional/xef/reasoning/text/summarize/Summarize.kt +++ b/reasoning/src/commonMain/kotlin/com/xebia/functional/xef/reasoning/text/summarize/Summarize.kt @@ -4,7 +4,11 @@ import arrow.fx.coroutines.parMap import com.xebia.functional.tokenizer.truncateText import com.xebia.functional.xef.auto.Conversation import com.xebia.functional.xef.llm.Chat -import com.xebia.functional.xef.prompt.experts.ExpertSystem +import com.xebia.functional.xef.prompt.buildPrompt +import com.xebia.functional.xef.prompt.templates.assistant +import com.xebia.functional.xef.prompt.templates.steps +import com.xebia.functional.xef.prompt.templates.system +import com.xebia.functional.xef.prompt.templates.user import com.xebia.functional.xef.reasoning.tools.Tool import io.github.oshai.kotlinlogging.KotlinLogging import kotlin.jvm.JvmField @@ -42,32 +46,33 @@ class Summarize( "๐Ÿ“ Summarizing chunk with prompt tokens $promptTokens for length $summaryLength" } val remainingTokens: Int = maxContextLength - promptTokens - return model - .promptMessage( - ExpertSystem( - system = - "You are an expert information summarizer that is able to provide a summary of a text in an exact number of words", - query = - """| + + val messages = buildPrompt { + +system( + "You are an expert information summarizer that is able to provide a summary of a text in less than a maximum number of words" + ) + +user( + """| |Given the following text: |```text |${model.modelType.encoding.truncateText(chunk, remainingTokens)} |``` """ - .trimMargin(), - instructions = - listOf( - "Summarize the `text` in max $summaryLength words", - "Reply with an empty response: ` ` if the text can't be summarized" - ) + instructions - ) - .message, - scope + .trimMargin() ) - .also { - val tokens: Int = model.modelType.encoding.countTokens(it) - logger.info { "๐Ÿ“ Summarized chunk in tokens: $tokens" } + +steps { + (listOf( + "Summarize the `text` in max $summaryLength words", + "Reply with an empty response: ` ` if the text can't be summarized" + ) + instructions) + .forEach { +assistant(it) } } + } + + return model.promptMessage(messages, scope).also { + val tokens: Int = model.modelType.encoding.countTokens(it) + logger.info { "๐Ÿ“ Summarized chunk in tokens: $tokens" } + } } private fun chunkText(text: String): List { diff --git a/reasoning/src/commonMain/kotlin/com/xebia/functional/xef/reasoning/tools/LLMTool.kt b/reasoning/src/commonMain/kotlin/com/xebia/functional/xef/reasoning/tools/LLMTool.kt index 3069d72c2..cf82aeace 100644 --- a/reasoning/src/commonMain/kotlin/com/xebia/functional/xef/reasoning/tools/LLMTool.kt +++ b/reasoning/src/commonMain/kotlin/com/xebia/functional/xef/reasoning/tools/LLMTool.kt @@ -2,7 +2,10 @@ package com.xebia.functional.xef.reasoning.tools import com.xebia.functional.xef.auto.Conversation import com.xebia.functional.xef.llm.Chat -import com.xebia.functional.xef.llm.models.chat.Message +import com.xebia.functional.xef.prompt.buildPrompt +import com.xebia.functional.xef.prompt.templates.assistant +import com.xebia.functional.xef.prompt.templates.system +import com.xebia.functional.xef.prompt.templates.user import com.xebia.functional.xef.reasoning.internals.callModel import io.github.oshai.kotlinlogging.KotlinLogging import kotlin.jvm.JvmOverloads @@ -24,16 +27,14 @@ abstract class LLMTool( model, scope, prompt = - listOf( - Message.systemMessage { "You are an expert in executing tool:" }, - Message.systemMessage { "Tool: $name" }, - Message.systemMessage { "Description: $description" }, - ) + - instructions.map { Message.systemMessage { it } } + - listOf( - Message.userMessage { "input: $input" }, - Message.assistantMessage { "output:" }, - ) + buildPrompt { + +system("You are an expert in executing the tool:") + +system("Tool: $name") + +system("Description: $description") + instructions.forEach { +system(it) } + +user("input: $input") + +assistant("output:") + } ) } diff --git a/reasoning/src/commonMain/kotlin/com/xebia/functional/xef/reasoning/tools/ReActAgent.kt b/reasoning/src/commonMain/kotlin/com/xebia/functional/xef/reasoning/tools/ReActAgent.kt index dfe40053c..206f671c9 100644 --- a/reasoning/src/commonMain/kotlin/com/xebia/functional/xef/reasoning/tools/ReActAgent.kt +++ b/reasoning/src/commonMain/kotlin/com/xebia/functional/xef/reasoning/tools/ReActAgent.kt @@ -5,9 +5,10 @@ import com.xebia.functional.xef.auto.Description import com.xebia.functional.xef.auto.PromptConfiguration import com.xebia.functional.xef.llm.ChatWithFunctions import com.xebia.functional.xef.llm.models.chat.Message -import com.xebia.functional.xef.llm.models.chat.Message.Companion.assistantMessage -import com.xebia.functional.xef.llm.models.chat.Message.Companion.systemMessage -import com.xebia.functional.xef.llm.models.chat.Message.Companion.userMessage +import com.xebia.functional.xef.prompt.buildPrompt +import com.xebia.functional.xef.prompt.templates.assistant +import com.xebia.functional.xef.prompt.templates.system +import com.xebia.functional.xef.prompt.templates.user import io.github.oshai.kotlinlogging.KotlinLogging import kotlinx.serialization.Serializable @@ -43,17 +44,15 @@ class ReActAgent( scope = scope, serializer = AgentFinish.serializer(), messages = - listOf( - systemMessage { "You are an expert in providing answers" }, - ) + - chain.chainToMessages() + - listOf( - userMessage { "Provide the final answer to the `input` in a sentence or paragraph" }, - userMessage { "input: $input" }, - assistantMessage { - "I should create a AgentFinish object with the final answer based on the thoughts and observations" - } + buildPrompt { + +system("You are an expert in providing answers") + +chain.chainToMessages() + +user("Provide the final answer to the `input` in a sentence or paragraph") + +user("input: $input") + +assistant( + "I should create a AgentFinish object with the final answer based on the thoughts and observations" ) + } ) private suspend fun agentAction( @@ -64,40 +63,32 @@ class ReActAgent( scope = scope, serializer = AgentAction.serializer(), messages = - listOf( - systemMessage { + buildPrompt { + +system( "You are an expert in tool selection. You are given a `input` and a `chain` of thoughts and observations." - }, - userMessage { "input:" }, - ) + - input + - listOf( - assistantMessage { "chain:" }, - ) + - chain.chainToMessages() + - listOf( - assistantMessage { "I can only use this tools:" }, - ) + - tools.toolsToMessages() + - listOf( - assistantMessage { - "I will not repeat the `toolInput` if the same one produced no satisfactory results in the observations" - }, - userMessage { "Provide the next tool to use and the `toolInput` for the tool" }, ) + +user("input:") + +input + +assistant("chain:") + +chain.chainToMessages() + +assistant("I can only use this tools:") + +tools.toolsToMessages() + +assistant( + "I will not repeat the `toolInput` if the same one produced no satisfactory results in the observations" + ) + +user("Provide the next tool to use and the `toolInput` for the tool") + } ) - private suspend fun List.toolsToMessages(): List = flatMap { - listOf( - assistantMessage { "${it.name}: ${it.description}" }, - ) + private fun List.toolsToMessages(): List = flatMap { + buildPrompt { +assistant("${it.name}: ${it.description}") } } - private suspend fun List.chainToMessages(): List = flatMap { - listOf( - assistantMessage { "Thought: ${it.thought}" }, - assistantMessage { "Observation: ${it.observation}" }, - ) + private fun List.chainToMessages(): List = flatMap { + buildPrompt { + +assistant("Thought: ${it.thought}") + +assistant("Observation: ${it.observation}") + } } private suspend fun agentChoice( @@ -110,19 +101,14 @@ class ReActAgent( serializer = AgentChoice.serializer(), promptConfiguration = promptConfiguration, messages = - input + - listOf( - assistantMessage { "chain:" }, - ) + - chain.chainToMessages() + - listOf( - assistantMessage { - "`CONTINUE` if the `input` has not been answered by the observations in the `chain`" - }, - assistantMessage { - "`FINISH` if the `input` has been answered by the observations in the `chain`" - }, + buildPrompt { + +input + +assistant("chain:") + +assistant( + "`CONTINUE` if the `input` has not been answered by the observations in the `chain`" ) + +assistant("`FINISH` if the `input` has been answered by the observations in the `chain`") + } ) private suspend fun createInitialThought( @@ -134,22 +120,16 @@ class ReActAgent( serializer = Thought.serializer(), promptConfiguration = promptConfiguration, messages = - listOf( - systemMessage { "You are an expert in providing next steps to solve a problem" }, - systemMessage { "You are given a `input` provided by the user" }, - userMessage { "input:" }, - ) + - input + - listOf( - assistantMessage { "I have access to tools:" }, - ) + - tools.toolsToMessages() + - listOf( - assistantMessage { - "I should create a Thought object with the next thought based on the `input`" - }, - userMessage { "Provide the next thought based on the `input`" }, - ) + buildPrompt { + +system("You are an expert in providing next steps to solve a problem") + +system("You are given a `input` provided by the user") + +user("input:") + +input + +assistant("I have access to tools:") + +tools.toolsToMessages() + +assistant("I should create a Thought object with the next thought based on the `input`") + +user("Provide the next thought based on the `input`") + } ) } diff --git a/reasoning/src/commonMain/kotlin/com/xebia/functional/xef/reasoning/tools/ToolSelection.kt b/reasoning/src/commonMain/kotlin/com/xebia/functional/xef/reasoning/tools/ToolSelection.kt index 6f7ac3e46..011256d73 100644 --- a/reasoning/src/commonMain/kotlin/com/xebia/functional/xef/reasoning/tools/ToolSelection.kt +++ b/reasoning/src/commonMain/kotlin/com/xebia/functional/xef/reasoning/tools/ToolSelection.kt @@ -3,6 +3,10 @@ package com.xebia.functional.xef.reasoning.tools import com.xebia.functional.xef.auto.Conversation import com.xebia.functional.xef.llm.ChatWithFunctions import com.xebia.functional.xef.llm.models.chat.Message +import com.xebia.functional.xef.prompt.buildPrompt +import com.xebia.functional.xef.prompt.templates.assistant +import com.xebia.functional.xef.prompt.templates.system +import com.xebia.functional.xef.prompt.templates.user import io.github.oshai.kotlinlogging.KotlinLogging class ToolSelection( @@ -80,26 +84,21 @@ class ToolSelection( suspend fun createExecutionPlan(task: String): ToolsExecutionPlan { logger.info { "๐Ÿ” Creating execution plan for task: $task" } - val messages: List = - listOf( - Message.systemMessage { - "You are an expert in tool selection that can choose the best tools for a specific task based on the tools descriptions" - }, - Message.assistantMessage { "Given the following task:" }, - Message.assistantMessage { task }, - Message.assistantMessage { "Given the following tools:" }, - ) + - tools.map { Message.assistantMessage { "${it.name}: ${it.description}" } } + - listOf( - Message.userMessage { "Follow the next instructions" }, - Message.userMessage { - "Select the best execution plan with tools for the `task` based on the `tools`" - }, - Message.userMessage { - "Your `RESPONSE` MUST be a `ToolsExecutionPlan` object, where the `steps` determine how the execution plan will run the tools" - }, - ) + - instructions.map { Message.userMessage { it } } + val messages: List = buildPrompt { + +system( + "You are an expert in tool selection that can choose the best tools for a specific task based on the tools descriptions" + ) + +assistant("Given the following task:") + +assistant(task) + +assistant("Given the following tools:") + tools.forEach { +assistant("${it.name}: ${it.description}") } + +user("Follow the next instructions") + +user("Select the best execution plan with tools for the `task` based on the `tools`") + +user( + "Your `RESPONSE` MUST be a `ToolsExecutionPlan` object, where the `steps` determine how the execution plan will run the tools" + ) + instructions.forEach { +user(it) } + } return model.prompt( scope = scope, diff --git a/reasoning/src/jvmMain/kotlin/com/xebia/functional/xef/reasoning/filesystem/ProduceTextFile.kt b/reasoning/src/jvmMain/kotlin/com/xebia/functional/xef/reasoning/filesystem/ProduceTextFile.kt index ed7a8502b..a0c13ce03 100644 --- a/reasoning/src/jvmMain/kotlin/com/xebia/functional/xef/reasoning/filesystem/ProduceTextFile.kt +++ b/reasoning/src/jvmMain/kotlin/com/xebia/functional/xef/reasoning/filesystem/ProduceTextFile.kt @@ -3,7 +3,11 @@ package com.xebia.functional.xef.reasoning.filesystem import com.xebia.functional.xef.auto.Conversation import com.xebia.functional.xef.io.DEFAULT import com.xebia.functional.xef.llm.ChatWithFunctions -import com.xebia.functional.xef.prompt.experts.ExpertSystem +import com.xebia.functional.xef.prompt.buildPrompt +import com.xebia.functional.xef.prompt.templates.assistant +import com.xebia.functional.xef.prompt.templates.steps +import com.xebia.functional.xef.prompt.templates.system +import com.xebia.functional.xef.prompt.templates.user import com.xebia.functional.xef.reasoning.tools.Tool import kotlinx.uuid.UUID import kotlinx.uuid.generateUUID @@ -21,12 +25,12 @@ class ProduceTextFile( override suspend fun invoke(input: String): String { val file: TxtFile = model.prompt( - prompt = - ExpertSystem( - system = "Convert output for a Text File", - query = input, - instructions = instructions, - ), + messages = + buildPrompt { + +system("Convert output for a Text File") + +user(input) + +steps { instructions.forEach { +assistant(it) } } + }, scope = scope, serializer = TxtFile.serializer() )