diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..6ac712b --- /dev/null +++ b/.editorconfig @@ -0,0 +1,45 @@ +# https://editorconfig.org +root = true + +[*] +indent_style = space +indent_size = 2 +max_line_length = 140 + +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.{java,kt,kts,scala,rs,xml,kt.spec,kts.spec}] +indent_size = 4 + +[*.{kt,kts}] +ktlint_code_style = ktlint_official +ktlint_ignore_back_ticked_identifier = true + +ktlint_standard = enabled +ktlint_standard_property-naming = disabled + +# Experimental rules run by default run on the ktlint code base itself. Experimental rules should not be released if +# we are not pleased ourselves with the results on the ktlint code base. +ktlint_experimental = enabled + +# Don't allow any wildcard imports +ij_kotlin_packages_to_use_import_on_demand = unset + +# Prevent wildcard imports +ij_kotlin_name_count_to_use_star_import = 99 +ij_kotlin_name_count_to_use_star_import_for_members = 99 + + + +[*.md] +trim_trailing_whitespace = false +max_line_length = unset + +[gradle/verification-metadata.xml] +indent_size = 3 + +[*.yml] +ij_yaml_spaces_within_brackets = false \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 651351a..4890d43 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -18,6 +18,7 @@ subprojects { target("**/*.kt") trimTrailingWhitespace() endWithNewline() + ktlint() } } } diff --git a/src/core/src/commonMain/kotlin/community/flock/aigentic/core/agent/Agent.kt b/src/core/src/commonMain/kotlin/community/flock/aigentic/core/agent/Agent.kt index e5ddda9..a43b7b3 100644 --- a/src/core/src/commonMain/kotlin/community/flock/aigentic/core/agent/Agent.kt +++ b/src/core/src/commonMain/kotlin/community/flock/aigentic/core/agent/Agent.kt @@ -2,10 +2,10 @@ package community.flock.aigentic.core.agent import community.flock.aigentic.core.agent.prompt.SystemPromptBuilder import community.flock.aigentic.core.message.Message -import community.flock.aigentic.core.tool.ToolName import community.flock.aigentic.core.model.Model import community.flock.aigentic.core.tool.InternalTool import community.flock.aigentic.core.tool.Tool +import community.flock.aigentic.core.tool.ToolName import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asSharedFlow @@ -15,13 +15,14 @@ import kotlinx.datetime.Instant data class Task( val description: String, - val instructions: List + val instructions: List, ) data class Instruction(val text: String) sealed interface Context { data class Text(val text: String) : Context + data class Image(val base64: String) : Context } @@ -54,4 +55,5 @@ data class Agent( } fun Agent.getMessages() = messages.asSharedFlow() + fun Agent.getStatus() = status.asStateFlow() diff --git a/src/core/src/commonMain/kotlin/community/flock/aigentic/core/agent/AgentExecutor.kt b/src/core/src/commonMain/kotlin/community/flock/aigentic/core/agent/AgentExecutor.kt index 373ac30..05a23b2 100644 --- a/src/core/src/commonMain/kotlin/community/flock/aigentic/core/agent/AgentExecutor.kt +++ b/src/core/src/commonMain/kotlin/community/flock/aigentic/core/agent/AgentExecutor.kt @@ -4,36 +4,43 @@ import community.flock.aigentic.core.agent.events.toEvents import community.flock.aigentic.core.agent.tool.FinishReason import community.flock.aigentic.core.agent.tool.FinishedOrStuck import community.flock.aigentic.core.agent.tool.finishOrStuckTool -import community.flock.aigentic.core.message.* -import community.flock.aigentic.core.tool.ToolName +import community.flock.aigentic.core.message.Message +import community.flock.aigentic.core.message.Sender +import community.flock.aigentic.core.message.ToolCall +import community.flock.aigentic.core.message.ToolResultContent +import community.flock.aigentic.core.message.argumentsAsJson import community.flock.aigentic.core.model.ModelResponse import community.flock.aigentic.core.tool.Tool -import kotlinx.coroutines.* -import kotlinx.coroutines.flow.MutableSharedFlow +import community.flock.aigentic.core.tool.ToolName +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.asFlow import kotlinx.coroutines.flow.flatMapConcat -import kotlinx.coroutines.flow.map import kotlinx.datetime.Clock data class ToolInterceptorResult(val cancelExecution: Boolean, val reason: String?) interface ToolInterceptor { - suspend fun intercept(agent: Agent, tool: Tool, toolCall: ToolCall): ToolInterceptorResult + suspend fun intercept( + agent: Agent, + tool: Tool, + toolCall: ToolCall, + ): ToolInterceptorResult } -suspend fun Agent.run(): FinishedOrStuck = coroutineScope { - - async { - getMessages().flatMapConcat{ it.toEvents().asFlow() }.collect{ - println(it.text) +suspend fun Agent.run(): FinishedOrStuck = + coroutineScope { + async { + getMessages().flatMapConcat { it.toEvents().asFlow() }.collect { + println(it.text) + } } - } - AgentExecutor().runAgent(this@run) -} + AgentExecutor().runAgent(this@run) + } class AgentExecutor(private val toolInterceptors: List = emptyList()) { - suspend fun runAgent(agent: Agent): FinishedOrStuck { agent.setRunningState(AgentRunningState.RUNNING) @@ -45,14 +52,15 @@ class AgentExecutor(private val toolInterceptors: List = emptyL val resultState = result.await() agent.updateStatus { - val endRunningState = if (resultState.reason is FinishReason.ImStuck) { - AgentRunningState.STUCK - } else { - AgentRunningState.COMPLETED - } + val endRunningState = + if (resultState.reason is FinishReason.ImStuck) { + AgentRunningState.STUCK + } else { + AgentRunningState.COMPLETED + } it.copy( runningState = endRunningState, - endTimestamp = Clock.System.now() + endTimestamp = Clock.System.now(), ) } return resultState @@ -69,29 +77,34 @@ class AgentExecutor(private val toolInterceptors: List = emptyL }.forEach { messages.emit(it) } } - private suspend fun processResponse(agent: Agent, response: ModelResponse, onFinished: (FinishedOrStuck) -> Unit) { + private suspend fun processResponse( + agent: Agent, + response: ModelResponse, + onFinished: (FinishedOrStuck) -> Unit, + ) { val message = response.message agent.messages.emit(message) when (message) { is Message.ToolCalls -> { - val shouldSendNextRequest = message.toolCalls - .map { toolCall -> - when (toolCall.name) { - finishOrStuckTool.name.value -> { - val finishedOrStuck = finishOrStuckTool.handler(toolCall.argumentsAsJson()) - onFinished(finishedOrStuck) - false - } - - else -> { - val toolResult = agent.execute(toolCall) - agent.messages.emit(toolResult) - true + val shouldSendNextRequest = + message.toolCalls + .map { toolCall -> + when (toolCall.name) { + finishOrStuckTool.name.value -> { + val finishedOrStuck = finishOrStuckTool.handler(toolCall.argumentsAsJson()) + onFinished(finishedOrStuck) + false + } + + else -> { + val toolResult = agent.execute(toolCall) + agent.messages.emit(toolResult) + true + } } } - } - .contains(true) + .contains(true) if (shouldSendNextRequest) { sendToolResponse(agent, onFinished) @@ -121,18 +134,22 @@ class AgentExecutor(private val toolInterceptors: List = emptyL private suspend fun runInterceptors( agent: Agent, tool: Tool, - toolCall: ToolCall - ): Message.ToolResult? = toolInterceptors - .map { it.intercept(agent, tool, toolCall) } - .firstOrNull { it.cancelExecution }?.let { - Message.ToolResult( - toolCall.id, - toolCall.name, - ToolResultContent(it.reason ?: "Tool execution blocked by interceptor") - ) - } + toolCall: ToolCall, + ): Message.ToolResult? = + toolInterceptors + .map { it.intercept(agent, tool, toolCall) } + .firstOrNull { it.cancelExecution }?.let { + Message.ToolResult( + toolCall.id, + toolCall.name, + ToolResultContent(it.reason ?: "Tool execution blocked by interceptor"), + ) + } - private suspend fun sendToolResponse(agent: Agent, onFinished: (FinishedOrStuck) -> Unit) { + private suspend fun sendToolResponse( + agent: Agent, + onFinished: (FinishedOrStuck) -> Unit, + ) { val response = agent.sendModelRequest() processResponse(agent, response, onFinished) } diff --git a/src/core/src/commonMain/kotlin/community/flock/aigentic/core/agent/events/AgentEvents.kt b/src/core/src/commonMain/kotlin/community/flock/aigentic/core/agent/events/AgentEvents.kt index ace2aa1..7d1db60 100644 --- a/src/core/src/commonMain/kotlin/community/flock/aigentic/core/agent/events/AgentEvents.kt +++ b/src/core/src/commonMain/kotlin/community/flock/aigentic/core/agent/events/AgentEvents.kt @@ -1,13 +1,12 @@ package community.flock.aigentic.core.agent.events +import community.flock.aigentic.core.agent.tool.FinishReason +import community.flock.aigentic.core.agent.tool.finishOrStuckTool import community.flock.aigentic.core.message.Message import community.flock.aigentic.core.message.ToolCall import community.flock.aigentic.core.message.argumentsAsJson -import community.flock.aigentic.core.agent.tool.FinishReason -import community.flock.aigentic.core.agent.tool.finishOrStuckTool sealed interface AgentEvent { - val text: String data object Started : AgentEvent { @@ -31,25 +30,27 @@ sealed interface AgentEvent { } data object SendingResponse : AgentEvent { - override val text = """ - 📡 Sending response to model + override val text = + """ + |📡 Sending response to model ----------------------------------- - """.trimIndent() + """.trimMargin() } - } -suspend fun Message.toEvents(): List = when (this) { - is Message.SystemPrompt -> listOf(AgentEvent.Started) - is Message.Text, is Message.Image -> emptyList() - is Message.ToolCalls -> this.toolCalls.map { - when(it.name) { - finishOrStuckTool.name.value -> getFinishEvent(it) - else -> AgentEvent.ExecuteTool(it) - } +suspend fun Message.toEvents(): List = + when (this) { + is Message.SystemPrompt -> listOf(AgentEvent.Started) + is Message.Text, is Message.Image -> emptyList() + is Message.ToolCalls -> + this.toolCalls.map { + when (it.name) { + finishOrStuckTool.name.value -> getFinishEvent(it) + else -> AgentEvent.ExecuteTool(it) + } + } + is Message.ToolResult -> listOf(AgentEvent.ToolResult(this)) } - is Message.ToolResult -> listOf(AgentEvent.ToolResult(this)) -} suspend fun getFinishEvent(it: ToolCall): AgentEvent { val finishedOrStuck = finishOrStuckTool.handler(it.argumentsAsJson()) diff --git a/src/core/src/commonMain/kotlin/community/flock/aigentic/core/agent/prompt/SystemPrompt.kt b/src/core/src/commonMain/kotlin/community/flock/aigentic/core/agent/prompt/SystemPrompt.kt index ed3fda2..1aa1513 100644 --- a/src/core/src/commonMain/kotlin/community/flock/aigentic/core/agent/prompt/SystemPrompt.kt +++ b/src/core/src/commonMain/kotlin/community/flock/aigentic/core/agent/prompt/SystemPrompt.kt @@ -1,42 +1,42 @@ package community.flock.aigentic.core.agent.prompt -import community.flock.aigentic.core.message.Message import community.flock.aigentic.core.agent.Agent import community.flock.aigentic.core.agent.tool.finishOrStuckTool +import community.flock.aigentic.core.message.Message interface SystemPromptBuilder { fun buildSystemPrompt(agent: Agent): Message.SystemPrompt } data object DefaultSystemPromptBuilder : SystemPromptBuilder { - - override fun buildSystemPrompt(agent: Agent): Message.SystemPrompt = - agent.createSystemPrompt() + override fun buildSystemPrompt(agent: Agent): Message.SystemPrompt = agent.createSystemPrompt() } private fun Agent.createSystemPrompt(): Message.SystemPrompt { - val baseInstruction = - "You are an agent which helps the user to accomplish different tasks. These tasks are outlined by the user below. The user has defined a set of tools which are available to achieve those tasks. The user also gives you information which gives you context, these are the first messages. Please execute one of these tools and the given context to fulfil these tasks. Don't send any text messages only use tools" + """ + |You are an agent which helps the user to accomplish different tasks. These tasks are outlined by the user below. + |The user also gives you information which gives you context, these are the first messages. + |Please execute one of these tools and the given context to fulfil these tasks. Don't send any text messages only use tools + """.trimMargin() val instructions = task.instructions.joinToString(separator = "\n\n") val finishConditionDescription = - """You are finished when the task is executed successfully: ${task.description} - - If you meet this condition, call the ${finishOrStuckTool.name} tool to indicate that you are done and have finished all tasks. - - When you don't know what to do also call the ${finishOrStuckTool.name} tool to indicate that you are stuck and need help. - """.trimIndent() + """ + |You are finished when the task is executed successfully: ${task.description} + |If you meet this condition, call the ${finishOrStuckTool.name.value} tool to indicate that you are done and have finished all tasks. + |When you don't know what to do also call the ${finishOrStuckTool.name.value} tool to indicate that you are stuck and need help. + """.trimMargin() return Message.SystemPrompt( """ - $baseInstruction + |$baseInstruction - Instructions: - $instructions + |Instructions: + |$instructions - $finishConditionDescription - """.trimIndent() + |$finishConditionDescription + """.trimMargin(), ) } diff --git a/src/core/src/commonMain/kotlin/community/flock/aigentic/core/agent/tool/CoreTools.kt b/src/core/src/commonMain/kotlin/community/flock/aigentic/core/agent/tool/CoreTools.kt index 1217db8..f8461f8 100644 --- a/src/core/src/commonMain/kotlin/community/flock/aigentic/core/agent/tool/CoreTools.kt +++ b/src/core/src/commonMain/kotlin/community/flock/aigentic/core/agent/tool/CoreTools.kt @@ -1,42 +1,54 @@ package community.flock.aigentic.core.agent.tool -import community.flock.aigentic.core.tool.* +import community.flock.aigentic.core.tool.InternalTool +import community.flock.aigentic.core.tool.Parameter +import community.flock.aigentic.core.tool.ParameterType +import community.flock.aigentic.core.tool.PrimitiveValue +import community.flock.aigentic.core.tool.ToolName +import community.flock.aigentic.core.tool.getStringValue import kotlinx.serialization.json.JsonObject -internal val finishOrStuckTool = object : InternalTool { - - val finishReasonParameter = Parameter.Complex.Enum( - name = "finishReason", - description = "The telephone number of the receiver of this message", - isRequired = true, - default = null, - values = FinishReason.getAllValues().map { PrimitiveValue.String.fromString(it::class.simpleName!!) }, - valueType = ParameterType.Primitive.String - ) - - val descriptionParameter = Parameter.Primitive( - name = "description", - description = "Depending on the finish reason a description of the executed work OR a description of why you're stuck", - isRequired = true, - type = ParameterType.Primitive.String - ) - - override val name = ToolName("finishedOrStuck") - override val description = - "When you've finished all tasks and met the finish condition OR when you are stuck call this tool. In the case you've finished all tasks please provide a description of the work which has been done. In case you're stuck please provide a description of the problem. When you're stuck don't call any other functions afterwards!" - override val parameters = listOf(finishReasonParameter, descriptionParameter) - override val handler: suspend (map: JsonObject) -> FinishedOrStuck = { arguments -> - - val stringValue = finishReasonParameter.getStringValue(arguments) - val finishReason = FinishReason.getAllValues().first { it::class.simpleName == stringValue } - val description = descriptionParameter.getStringValue(arguments) - FinishedOrStuck(finishReason, description) +internal val finishOrStuckTool = + object : InternalTool { + val finishReasonParameter = + Parameter.Complex.Enum( + name = "finishReason", + description = null, + isRequired = true, + default = null, + values = FinishReason.getAllValues().map { PrimitiveValue.String.fromString(it::class.simpleName!!) }, + valueType = ParameterType.Primitive.String, + ) + + val descriptionParameter = + Parameter.Primitive( + name = "description", + description = "Depending on the finish reason a description of the executed work or a description of why you're stuck", + isRequired = true, + type = ParameterType.Primitive.String, + ) + + override val name = ToolName("finishedOrStuck") + override val description = + """ + |When you've finished all tasks and met the finish condition OR when you are stuck call this tool. + |In the case you've finished all tasks please provide a description of the work which has been done. + |In case you're stuck please provide a description of the problem. + """.trimMargin() + + override val parameters = listOf(finishReasonParameter, descriptionParameter) + override val handler: suspend (map: JsonObject) -> FinishedOrStuck = { arguments -> + + val stringValue = finishReasonParameter.getStringValue(arguments) + val finishReason = FinishReason.getAllValues().first { it::class.simpleName == stringValue } + val description = descriptionParameter.getStringValue(arguments) + FinishedOrStuck(finishReason, description) + } } -} data class FinishedOrStuck(val reason: FinishReason, val description: String) -sealed interface FinishReason { +sealed interface FinishReason { data object FinishedAllTasks : FinishReason data object ImStuck : FinishReason diff --git a/src/core/src/commonMain/kotlin/community/flock/aigentic/core/dsl/AgentConfig.kt b/src/core/src/commonMain/kotlin/community/flock/aigentic/core/dsl/AgentConfig.kt index ddf481a..a978e33 100644 --- a/src/core/src/commonMain/kotlin/community/flock/aigentic/core/dsl/AgentConfig.kt +++ b/src/core/src/commonMain/kotlin/community/flock/aigentic/core/dsl/AgentConfig.kt @@ -1,6 +1,5 @@ package community.flock.aigentic.core.dsl -import community.flock.aigentic.core.tool.Tool import community.flock.aigentic.core.agent.Agent import community.flock.aigentic.core.agent.Context import community.flock.aigentic.core.agent.Instruction @@ -8,13 +7,12 @@ import community.flock.aigentic.core.agent.Task import community.flock.aigentic.core.agent.prompt.DefaultSystemPromptBuilder import community.flock.aigentic.core.agent.prompt.SystemPromptBuilder import community.flock.aigentic.core.model.Model +import community.flock.aigentic.core.tool.Tool -fun agent(agentConfig: AgentConfig.() -> Unit): Agent = - AgentConfig().apply(agentConfig).build() +fun agent(agentConfig: AgentConfig.() -> Unit): Agent = AgentConfig().apply(agentConfig).build() @AgentDSL class AgentConfig : Config { - var model: Model? = null private var id: String = "AgentId" private var task: TaskConfig? = null @@ -27,13 +25,14 @@ class AgentConfig : Config { this.id = id } - fun AgentConfig.addTool(tool: Tool) = - tools.add(tool) + fun AgentConfig.addTool(tool: Tool) = tools.add(tool) - fun AgentConfig.context(contextConfig: ContextConfig.() -> Unit) = - ContextConfig().apply(contextConfig).build().also { contexts = it } + fun AgentConfig.context(contextConfig: ContextConfig.() -> Unit) = ContextConfig().apply(contextConfig).build().also { contexts = it } - fun AgentConfig.task(description: String, taskConfig: TaskConfig.() -> Unit): TaskConfig = + fun AgentConfig.task( + description: String, + taskConfig: TaskConfig.() -> Unit, + ): TaskConfig = TaskConfig(description).apply(taskConfig) .also { task = it } @@ -41,34 +40,30 @@ class AgentConfig : Config { this.systemPromptBuilder = systemPromptBuilder } - override fun build(): Agent = Agent( - id = id, - systemPromptBuilder = systemPromptBuilder, - model = checkNotNull(model), - task = checkNotNull(task?.build()), - tools = tools.associateBy { it.name }, - contexts = contexts - ) + override fun build(): Agent = + Agent( + id = id, + systemPromptBuilder = systemPromptBuilder, + model = checkNotNull(model), + task = checkNotNull(task?.build()), + tools = tools.associateBy { it.name }, + contexts = contexts, + ) } - @AgentDSL class TaskConfig( - val description: String + val description: String, ) : Config { - private val instructions = mutableListOf() - fun TaskConfig.addInstruction(instruction: String) = - instructions.add(Instruction(instruction)) + fun TaskConfig.addInstruction(instruction: String) = instructions.add(Instruction(instruction)) - override fun build(): Task = - Task(description, instructions) + override fun build(): Task = Task(description, instructions) } @AgentDSL class ContextConfig : Config> { - private val contexts = mutableListOf() fun ContextConfig.addText(text: String) = diff --git a/src/core/src/commonMain/kotlin/community/flock/aigentic/core/logging/Logger.kt b/src/core/src/commonMain/kotlin/community/flock/aigentic/core/logging/Logger.kt index 8395139..29fbe4d 100644 --- a/src/core/src/commonMain/kotlin/community/flock/aigentic/core/logging/Logger.kt +++ b/src/core/src/commonMain/kotlin/community/flock/aigentic/core/logging/Logger.kt @@ -1,7 +1,6 @@ package community.flock.aigentic.core.logging interface Logger { - fun warning(message: String) } diff --git a/src/core/src/commonMain/kotlin/community/flock/aigentic/core/message/Message.kt b/src/core/src/commonMain/kotlin/community/flock/aigentic/core/message/Message.kt index e7fb04b..46db162 100644 --- a/src/core/src/commonMain/kotlin/community/flock/aigentic/core/message/Message.kt +++ b/src/core/src/commonMain/kotlin/community/flock/aigentic/core/message/Message.kt @@ -5,16 +5,15 @@ import kotlinx.serialization.json.JsonObject import kotlin.jvm.JvmInline sealed class Message( - open val sender: Sender + open val sender: Sender, ) { - data class SystemPrompt( - val prompt: String + val prompt: String, ) : Message(Sender.Aigentic) data class Text( override val sender: Sender, - val text: String + val text: String, ) : Message(sender) data class Image( @@ -23,21 +22,20 @@ sealed class Message( ) : Message(Sender.Aigentic) data class ToolCalls( - val toolCalls: List + val toolCalls: List, ) : Message(Sender.Model) data class ToolResult( val toolCallId: ToolCallId, val toolName: String, - val response: ToolResultContent + val response: ToolResultContent, ) : Message(Sender.Aigentic) - } data class ToolCall( val id: ToolCallId, val name: String, - val arguments: String + val arguments: String, ) @JvmInline diff --git a/src/core/src/commonMain/kotlin/community/flock/aigentic/core/model/Model.kt b/src/core/src/commonMain/kotlin/community/flock/aigentic/core/model/Model.kt index 2bc06e4..faf373a 100644 --- a/src/core/src/commonMain/kotlin/community/flock/aigentic/core/model/Model.kt +++ b/src/core/src/commonMain/kotlin/community/flock/aigentic/core/model/Model.kt @@ -13,9 +13,12 @@ interface Model { val authentication: Authentication val modelIdentifier: ModelIdentifier - suspend fun sendRequest(messages: List, tools: List): ModelResponse + suspend fun sendRequest( + messages: List, + tools: List, + ): ModelResponse } data class ModelResponse( - val message: Message + val message: Message, ) diff --git a/src/core/src/commonMain/kotlin/community/flock/aigentic/core/tool/Parameter.kt b/src/core/src/commonMain/kotlin/community/flock/aigentic/core/tool/Parameter.kt index a721c44..baf2cdd 100644 --- a/src/core/src/commonMain/kotlin/community/flock/aigentic/core/tool/Parameter.kt +++ b/src/core/src/commonMain/kotlin/community/flock/aigentic/core/tool/Parameter.kt @@ -1,13 +1,16 @@ package community.flock.aigentic.core.tool -import kotlinx.serialization.json.* +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.decodeFromJsonElement +import kotlinx.serialization.json.int +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive import kotlin.jvm.JvmInline -fun Parameter.getStringValue(arguments: JsonObject): String = - arguments.getValue(name).jsonPrimitive.content +fun Parameter.getStringValue(arguments: JsonObject): String = arguments.getValue(name).jsonPrimitive.content -fun Parameter.getIntValue(arguments: JsonObject): Int = - arguments.getValue(name).jsonPrimitive.int +fun Parameter.getIntValue(arguments: JsonObject): Int = arguments.getValue(name).jsonPrimitive.int inline fun Parameter.Complex.Object.getObject(arguments: JsonObject): T { val arg = arguments.getValue(name) @@ -20,44 +23,41 @@ sealed class Parameter( open val isRequired: Boolean, open val type: ParameterType, ) { - data class Primitive( override val name: String, override val description: String?, override val isRequired: Boolean, - override val type: ParameterType.Primitive + override val type: ParameterType.Primitive, ) : Parameter(name, description, isRequired, type) sealed class Complex(name: String, description: String?, isRequired: Boolean, type: ParameterType) : Parameter(name, description, isRequired, type) { - - data class Enum( - override val name: String, - override val description: String?, - override val isRequired: Boolean, - val default: PrimitiveValue<*>?, - val values: List>, - val valueType: ParameterType.Primitive - ) : Complex(name, description, isRequired, ParameterType.Complex.Enum) - - data class Object( - override val name: String, - override val description: String?, - override val isRequired: Boolean, - val parameters: List - ) : Complex(name, description, isRequired, ParameterType.Complex.Object) - - data class Array( - override val name: String, - override val description: String?, - override val isRequired: Boolean, - val itemDefinition: Parameter - ) : Complex(name, description, isRequired, ParameterType.Complex.Array) - } + data class Enum( + override val name: String, + override val description: String?, + override val isRequired: Boolean, + val default: PrimitiveValue<*>?, + val values: List>, + val valueType: ParameterType.Primitive, + ) : Complex(name, description, isRequired, ParameterType.Complex.Enum) + + data class Object( + override val name: String, + override val description: String?, + override val isRequired: Boolean, + val parameters: List, + ) : Complex(name, description, isRequired, ParameterType.Complex.Object) + + data class Array( + override val name: String, + override val description: String?, + override val isRequired: Boolean, + val itemDefinition: Parameter, + ) : Complex(name, description, isRequired, ParameterType.Complex.Array) + } } sealed interface ParameterType { - sealed interface Primitive : ParameterType { data object String : Primitive data object Number : Primitive @@ -73,7 +73,6 @@ sealed interface ParameterType { } sealed interface PrimitiveValue { - val value: Type @JvmInline diff --git a/src/core/src/commonMain/kotlin/community/flock/aigentic/core/tool/Tool.kt b/src/core/src/commonMain/kotlin/community/flock/aigentic/core/tool/Tool.kt index f95d522..4650db0 100644 --- a/src/core/src/commonMain/kotlin/community/flock/aigentic/core/tool/Tool.kt +++ b/src/core/src/commonMain/kotlin/community/flock/aigentic/core/tool/Tool.kt @@ -32,5 +32,8 @@ interface Tool : ToolConfigurationSupport, ToolDescription, Handler internal interface InternalTool : ToolDescription, Handler interface ToolPermissionHandler { - suspend fun hasPermission(toolConfiguration: ToolConfiguration, toolCall: ToolCall): Boolean + suspend fun hasPermission( + toolConfiguration: ToolConfiguration, + toolCall: ToolCall, + ): Boolean } diff --git a/src/example/src/commonMain/kotlin/community/flock/aigentic/example/AdministrativeAgentExample.kt b/src/example/src/commonMain/kotlin/community/flock/aigentic/example/AdministrativeAgentExample.kt index 3a916b3..7d97d75 100644 --- a/src/example/src/commonMain/kotlin/community/flock/aigentic/example/AdministrativeAgentExample.kt +++ b/src/example/src/commonMain/kotlin/community/flock/aigentic/example/AdministrativeAgentExample.kt @@ -1,9 +1,16 @@ +@file:Suppress("ktlint:standard:max-line-length") + package community.flock.aigentic.example import community.flock.aigentic.core.agent.run import community.flock.aigentic.core.dsl.agent -import community.flock.aigentic.core.tool.* +import community.flock.aigentic.core.tool.Parameter +import community.flock.aigentic.core.tool.ParameterType import community.flock.aigentic.core.tool.ParameterType.Primitive +import community.flock.aigentic.core.tool.Tool +import community.flock.aigentic.core.tool.ToolName +import community.flock.aigentic.core.tool.getIntValue +import community.flock.aigentic.core.tool.getStringValue import community.flock.aigentic.dsl.openAIModel import community.flock.aigentic.model.OpenAIModelIdentifier import kotlinx.serialization.json.JsonObject @@ -12,9 +19,15 @@ suspend fun runAdministrativeAgentExample(openAIAPIKey: String) { agent { openAIModel(openAIAPIKey, OpenAIModelIdentifier.GPT4Turbo) task("Retrieve all employees to inspect their hour status") { - addInstruction("For all employees: only when the employee has not yet received 5 reminders to completed his hours send him a reminder through Signal. Base the tone of the message on the number of reminders sent") - addInstruction("If the employee is reminded 5 times and still has still not completed the hours don't send the employee a message but ask the manager on how to respond and send the manager's response to the employee") - addInstruction("When you for sure know that the signal message is successfully sent (which means the tool call returns a success response), make sure that you update the numberOfRemindersSent for each and every the specific employee.") + addInstruction( + "For all employees: only when the employee has not yet received 5 reminders to completed his hours send him a reminder through Signal. Base the tone of the message on the number of reminders sent", + ) + addInstruction( + "If the employee is reminded 5 times and still has still not completed the hours don't send the employee a message but ask the manager on how to respond and send the manager's response to the employee", + ) + addInstruction( + "When you for sure know that the signal message is successfully sent (which means the tool call returns a success response), make sure that you update the numberOfRemindersSent for each and every the specific employee.", + ) } addTool(getAllEmployeesOverviewTool) addTool(getEmployeeDetailByNameTool) @@ -24,122 +37,147 @@ suspend fun runAdministrativeAgentExample(openAIAPIKey: String) { }.run() } -val getAllEmployeesOverviewTool = object : Tool { - override val name = ToolName("getAllEmployeesOverview") - override val description = "Returns a list of all employees" - override val parameters = emptyList() - override val handler: suspend (map: JsonObject) -> String = { - """ - Employee: Niels - Telephone number: 0612345678 - - Employee: Henk - Telephone number: 0687654321 - - Employee: Jan - Telephone number: 0643211234 - """.trimIndent() +val getAllEmployeesOverviewTool = + object : Tool { + override val name = ToolName("getAllEmployeesOverview") + override val description = "Returns a list of all employees" + override val parameters = emptyList() + override val handler: suspend (map: JsonObject) -> String = { + """ + |Employee: Niels + |Telephone number: 0612345678 + + |Employee: Henk + |Telephone number: 0687654321 + + |Employee: Jan + |Telephone number: 0643211234 + """.trimMargin() + } } -} -val getEmployeeDetailByNameTool = object : Tool { - - val nameParameter = Parameter.Primitive( - "name", "The name of the employee", true, Primitive.String - ) - - override val name = ToolName("getEmployeeDetailByName") - override val description = "Returns the hour status of an employee by name" - override val parameters = listOf(nameParameter) - override val handler: suspend (map: JsonObject) -> String = { - - val name = nameParameter.getStringValue(it) - - when (name) { - "Niels" -> """ - Employee: Niels - Telephone number: 0612345678 - Has completed hours: NO - Number of reminders sent: 1 - """.trimIndent() - - "Henk" -> """ - Employee: Henk - Telephone number: 0687654321 - Has completed hours: YES - Number of reminders sent: 2 - """.trimIndent() - - "Jan" -> """ - Employee: Jan - Telephone number: 0643211234 - Has completed hours: NO - Number of reminders sent: 5 - """.trimIndent() - - else -> "Unknown employee" +val getEmployeeDetailByNameTool = + object : Tool { + val nameParameter = + Parameter.Primitive( + "name", + "The name of the employee", + true, + Primitive.String, + ) + + override val name = ToolName("getEmployeeDetailByName") + override val description = "Returns the hour status of an employee by name" + override val parameters = listOf(nameParameter) + override val handler: suspend (map: JsonObject) -> String = { + + val name = nameParameter.getStringValue(it) + + when (name) { + "Niels" -> + """ + |Employee: Niels + |Telephone number: 0612345678 + |Has completed hours: NO + |Number of reminders sent: 1 + """.trimMargin() + + "Henk" -> + """ + |Employee: Henk + |Telephone number: 0687654321 + |Has completed hours: YES + |Number of reminders sent: 2 + """.trimMargin() + + "Jan" -> + """ + |Employee: Jan + |Telephone number: 0643211234 + |Has completed hours: NO + |Number of reminders sent: 5 + """.trimMargin() + + else -> "Unknown employee" + } } } -} - -val askManagerForResponseTool = object : Tool { - val nameParameter = Parameter.Primitive( - "name", "The name of the employee", true, Primitive.String - ) - - override val name = ToolName("askManagerForResponse") - override val description = "Ask to manager how to respond" - override val parameters = listOf(nameParameter) - override val handler: suspend (map: JsonObject) -> String = { - - val name = nameParameter.getStringValue(it) - "$name, je moet nu echt je uren invullen anders word je ontslagen!" +val askManagerForResponseTool = + object : Tool { + val nameParameter = + Parameter.Primitive( + "name", + "The name of the employee", + true, + Primitive.String, + ) + + override val name = ToolName("askManagerForResponse") + override val description = "Ask to manager how to respond" + override val parameters = listOf(nameParameter) + override val handler: suspend (map: JsonObject) -> String = { + + val name = nameParameter.getStringValue(it) + "$name, je moet nu echt je uren invullen anders word je ontslagen!" + } } -} -val updateEmployeeTool = object : Tool { - - val nameParameter = Parameter.Primitive( - "name", "The name of the employee", true, Primitive.String - ) - - val numberOfRemindersSentParameter = Parameter.Primitive( - "numberOfRemindersSent", - "The updated value of the number of reminders sent to the employee", - true, - ParameterType.Primitive.Integer - ) - - override val name = ToolName("updateEmployee") - override val description = "Update the employee status" - override val parameters = listOf(nameParameter, numberOfRemindersSentParameter) - override val handler: suspend (map: JsonObject) -> String = { - val name = nameParameter.getStringValue(it) - val numberOfRemindersSent = numberOfRemindersSentParameter.getIntValue(it) - "Updated number of reminders sent for '$name' to '$numberOfRemindersSent'" +val updateEmployeeTool = + object : Tool { + val nameParameter = + Parameter.Primitive( + "name", + "The name of the employee", + true, + Primitive.String, + ) + + val numberOfRemindersSentParameter = + Parameter.Primitive( + "numberOfRemindersSent", + "The updated value of the number of reminders sent to the employee", + true, + ParameterType.Primitive.Integer, + ) + + override val name = ToolName("updateEmployee") + override val description = "Update the employee status" + override val parameters = listOf(nameParameter, numberOfRemindersSentParameter) + override val handler: suspend (map: JsonObject) -> String = { + val name = nameParameter.getStringValue(it) + val numberOfRemindersSent = numberOfRemindersSentParameter.getIntValue(it) + "Updated number of reminders sent for '$name' to '$numberOfRemindersSent'" + } } -} - -val sendSignalMessageTool = object : Tool { - - val phoneNumberParam = Parameter.Primitive( - "phoneNumber", "The telephone number of the receiver of this message", true, Primitive.String - ) - - val messageParam = Parameter.Primitive( - "message", null, true, Primitive.String - ) - - override val name = ToolName("sendSignalMessage") - override val description = "Sends a Signal message to the provided person" - override val parameters = listOf(phoneNumberParam, messageParam) - override val handler: suspend (JsonObject) -> String = { arguments -> - - val phoneNumber = phoneNumberParam.getStringValue(arguments) - val message = messageParam.getStringValue(arguments) - - "✉️ Sending: '$message' to '$phoneNumber'" +val sendSignalMessageTool = + object : Tool { + val phoneNumberParam = + Parameter.Primitive( + "phoneNumber", + "The telephone number of the receiver of this message", + true, + Primitive.String, + ) + + val messageParam = + Parameter.Primitive( + "message", + null, + true, + Primitive.String, + ) + + override val name = ToolName("sendSignalMessage") + override val description = "Sends a Signal message to the provided person" + override val parameters = listOf(phoneNumberParam, messageParam) + + override val handler: suspend (JsonObject) -> String = { arguments -> + + val phoneNumber = phoneNumberParam.getStringValue(arguments) + val message = messageParam.getStringValue(arguments) + + "✉️ Sending: '$message' to '$phoneNumber'" + } } -} diff --git a/src/example/src/commonMain/kotlin/community/flock/aigentic/example/OpenAPIAgentExample.kt b/src/example/src/commonMain/kotlin/community/flock/aigentic/example/OpenAPIAgentExample.kt index f291782..f631f4d 100644 --- a/src/example/src/commonMain/kotlin/community/flock/aigentic/example/OpenAPIAgentExample.kt +++ b/src/example/src/commonMain/kotlin/community/flock/aigentic/example/OpenAPIAgentExample.kt @@ -12,8 +12,10 @@ import community.flock.aigentic.model.OpenAIModelIdentifier import community.flock.aigentic.tools.openapi.dsl.openApiTools import kotlinx.serialization.json.JsonObject -suspend fun runOpenAPIAgent(openAIAPIKey: String, hackerNewsOpenAPISpec: String) { - +suspend fun runOpenAPIAgent( + openAIAPIKey: String, + hackerNewsOpenAPISpec: String, +) { agent { openAIModel(openAIAPIKey, OpenAIModelIdentifier.GPT4Turbo) task("Send Hacker News stories about AI") { @@ -25,30 +27,42 @@ suspend fun runOpenAPIAgent(openAIAPIKey: String, hackerNewsOpenAPISpec: String) }.run() } -val sendEmailTool = object : Tool { - - val emailAddressParam = Parameter.Primitive( - "emailAddress", "The recipient email address", true, Primitive.String - ) +val sendEmailTool = + object : Tool { + val emailAddressParam = + Parameter.Primitive( + "emailAddress", + "The recipient email address", + true, + Primitive.String, + ) - val subjectParam = Parameter.Primitive( - "subject", "Email subject", true, Primitive.String - ) + val subjectParam = + Parameter.Primitive( + "subject", + "Email subject", + true, + Primitive.String, + ) - val messageParam = Parameter.Primitive( - "message", "Email message", true, Primitive.String - ) + val messageParam = + Parameter.Primitive( + "message", + "Email message", + true, + Primitive.String, + ) - override val name = ToolName("SendEmail") - override val description = "Sends a Email to the provided recipient" - override val parameters = listOf(emailAddressParam, subjectParam, messageParam) + override val name = ToolName("SendEmail") + override val description = "Sends a Email to the provided recipient" + override val parameters = listOf(emailAddressParam, subjectParam, messageParam) - override val handler: suspend (JsonObject) -> String = { arguments -> + override val handler: suspend (JsonObject) -> String = { arguments -> - val emailAddress = emailAddressParam.getStringValue(arguments) - val subject = subjectParam.getStringValue(arguments) - val message = messageParam.getStringValue(arguments) + val emailAddress = emailAddressParam.getStringValue(arguments) + val subject = subjectParam.getStringValue(arguments) + val message = messageParam.getStringValue(arguments) - "✉️ Sending email: '$message' with subject: '$subject' to recipient: $emailAddress" + "✉️ Sending email: '$message' with subject: '$subject' to recipient: $emailAddress" + } } -} diff --git a/src/example/src/jvmMain/kotlin/community/flock/aigentic/example/AdministrativeAgentExampleJvm.kt b/src/example/src/jvmMain/kotlin/community/flock/aigentic/example/AdministrativeAgentExampleJvm.kt index 91899ea..8f272e0 100644 --- a/src/example/src/jvmMain/kotlin/community/flock/aigentic/example/AdministrativeAgentExampleJvm.kt +++ b/src/example/src/jvmMain/kotlin/community/flock/aigentic/example/AdministrativeAgentExampleJvm.kt @@ -2,11 +2,12 @@ package community.flock.aigentic.example import kotlinx.coroutines.runBlocking - -private val openAIAPIKey = System.getenv("OPENAI_KEY").also { - if (it.isNullOrEmpty()) error("Set 'OPENAI_KEY' environment variable!") -} - -fun main(): Unit = runBlocking { - runAdministrativeAgentExample(openAIAPIKey) -} +private val openAIAPIKey = + System.getenv("OPENAI_KEY").also { + if (it.isNullOrEmpty()) error("Set 'OPENAI_KEY' environment variable!") + } + +fun main(): Unit = + runBlocking { + runAdministrativeAgentExample(openAIAPIKey) + } diff --git a/src/example/src/jvmMain/kotlin/community/flock/aigentic/example/OpenAPIAgentExampleJvm.kt b/src/example/src/jvmMain/kotlin/community/flock/aigentic/example/OpenAPIAgentExampleJvm.kt index 9517fa4..72cc0c0 100644 --- a/src/example/src/jvmMain/kotlin/community/flock/aigentic/example/OpenAPIAgentExampleJvm.kt +++ b/src/example/src/jvmMain/kotlin/community/flock/aigentic/example/OpenAPIAgentExampleJvm.kt @@ -3,14 +3,16 @@ package community.flock.aigentic.example import community.flock.aigentic.example.HackerNewsSpec.spec import kotlinx.coroutines.runBlocking -private val openAIAPIKey = System.getenv("OPENAI_KEY").also { - if (it.isNullOrEmpty()) error("Set 'OPENAI_KEY' environment variable!") -} +private val openAIAPIKey = + System.getenv("OPENAI_KEY").also { + if (it.isNullOrEmpty()) error("Set 'OPENAI_KEY' environment variable!") + } object HackerNewsSpec { val spec = this::class.java.getResource("/hackernews.json")!!.readText(Charsets.UTF_8) } -fun main(): Unit = runBlocking { - runOpenAPIAgent(openAIAPIKey, spec) -} +fun main(): Unit = + runBlocking { + runOpenAPIAgent(openAIAPIKey, spec) + } diff --git a/src/providers/openai/src/commonMain/kotlin/community/flock/aigentic/dsl/OpenAIDsl.kt b/src/providers/openai/src/commonMain/kotlin/community/flock/aigentic/dsl/OpenAIDsl.kt index f8da2c7..f093583 100644 --- a/src/providers/openai/src/commonMain/kotlin/community/flock/aigentic/dsl/OpenAIDsl.kt +++ b/src/providers/openai/src/commonMain/kotlin/community/flock/aigentic/dsl/OpenAIDsl.kt @@ -7,7 +7,6 @@ import community.flock.aigentic.model.OpenAIModelIdentifier fun AgentConfig.openAIModel( apiKey: String, - identifier: OpenAIModelIdentifier -) = - OpenAIModel(Authentication.APIKey(apiKey), identifier) - .also { model = it } + identifier: OpenAIModelIdentifier, +) = OpenAIModel(Authentication.APIKey(apiKey), identifier) + .also { model = it } diff --git a/src/providers/openai/src/commonMain/kotlin/community/flock/aigentic/mapper/ChatCompletionMapper.kt b/src/providers/openai/src/commonMain/kotlin/community/flock/aigentic/mapper/ChatCompletionMapper.kt index 86113ed..1a9e5b7 100644 --- a/src/providers/openai/src/commonMain/kotlin/community/flock/aigentic/mapper/ChatCompletionMapper.kt +++ b/src/providers/openai/src/commonMain/kotlin/community/flock/aigentic/mapper/ChatCompletionMapper.kt @@ -5,8 +5,7 @@ import community.flock.aigentic.core.model.ModelResponse import community.flock.aigentic.mapper.DomainMapper.toMessage internal fun ChatCompletion.toModelResponse(): ModelResponse { - return ModelResponse( - message = choices.first().message.toMessage() + message = choices.first().message.toMessage(), ) } diff --git a/src/providers/openai/src/commonMain/kotlin/community/flock/aigentic/mapper/MessageMappers.kt b/src/providers/openai/src/commonMain/kotlin/community/flock/aigentic/mapper/MessageMappers.kt index 2838786..174982e 100644 --- a/src/providers/openai/src/commonMain/kotlin/community/flock/aigentic/mapper/MessageMappers.kt +++ b/src/providers/openai/src/commonMain/kotlin/community/flock/aigentic/mapper/MessageMappers.kt @@ -3,32 +3,37 @@ package community.flock.aigentic.mapper import com.aallam.openai.api.chat.ChatMessage import com.aallam.openai.api.chat.ChatRole import com.aallam.openai.api.chat.FunctionCall +import com.aallam.openai.api.chat.ImagePart import com.aallam.openai.api.chat.TextContent import com.aallam.openai.api.chat.ToolId import com.aallam.openai.api.core.Role +import community.flock.aigentic.core.message.Message +import community.flock.aigentic.core.message.Sender +import community.flock.aigentic.core.message.ToolCall +import community.flock.aigentic.core.message.ToolCallId +import community.flock.aigentic.core.message.ToolResultContent import com.aallam.openai.api.chat.ToolCall as OpenAIToolCall -import com.aallam.openai.api.chat.ImagePart -import community.flock.aigentic.core.message.* object DomainMapper { + internal fun ChatMessage.toMessage(): Message = + when { + isSystemMessage() -> Message.SystemPrompt(content!!) + isToolCallsMessage() -> Message.ToolCalls(toolCalls.mapToolCalls()) + isToolResult() -> + Message.ToolResult( + toolCallId = ToolCallId(this.toolCallId!!.id), + toolName = this.name!!, + response = ToolResultContent(this.content!!), + ) - internal fun ChatMessage.toMessage(): Message = when { - - isSystemMessage() -> Message.SystemPrompt(content!!) - isToolCallsMessage() -> Message.ToolCalls(toolCalls.mapToolCalls()) - isToolResult() -> Message.ToolResult( - toolCallId = ToolCallId(this.toolCallId!!.id), - toolName = this.name!!, - response = ToolResultContent(this.content!!) - ) - - isTextMessage() -> Message.Text( - sender = role.mapToSender(), - text = content!! - ) + isTextMessage() -> + Message.Text( + sender = role.mapToSender(), + text = content!!, + ) - else -> error("Cannot map OpenAI ChatMessage, unknown type: $this") - } + else -> error("Cannot map OpenAI ChatMessage, unknown type: $this") + } private fun ChatMessage.isTextMessage() = messageContent?.let { it is TextContent } ?: false @@ -45,67 +50,70 @@ object DomainMapper { ToolCall( id = ToolCallId(it.id.id), name = it.function.name, - arguments = it.function.arguments + arguments = it.function.arguments, ) } ?: emptyList() - private fun ChatRole.mapToSender(): Sender = when (this) { - Role.Assistant -> Sender.Model - Role.User -> Sender.Aigentic - else -> error("Unexpected role: $this") - } - + private fun ChatRole.mapToSender(): Sender = + when (this) { + Role.Assistant -> Sender.Model + Role.User -> Sender.Aigentic + else -> error("Unexpected role: $this") + } } object OpenAIMapper { - internal fun Message.toOpenAIMessage(): ChatMessage { val role = mapChatTextRole() return when (this) { is Message.SystemPrompt -> ChatMessage(role, prompt) is Message.Text -> ChatMessage(role, text) is Message.Image -> ChatMessage(role = role, listOf(ImagePart(image))) - is Message.ToolCalls -> ChatMessage( - role = role, - content = null as String?, - toolCalls = mapToolCalls(), - ) - - is Message.ToolResult -> ChatMessage( - role = role, - toolCallId = ToolId(toolCallId.id), - name = toolName, - content = response.result - ) + is Message.ToolCalls -> + ChatMessage( + role = role, + content = null as String?, + toolCalls = mapToolCalls(), + ) + + is Message.ToolResult -> + ChatMessage( + role = role, + toolCallId = ToolId(toolCallId.id), + name = toolName, + content = response.result, + ) } } - private fun Message.mapChatTextRole(): ChatRole = when (this) { - is Message.SystemPrompt -> ChatRole.System - is Message.Text -> mapChatTextRole() - is Message.ToolCalls -> ChatRole.Assistant - is Message.ToolResult -> ChatRole.Tool - is Message.Image -> mapImageTextRole() - } + private fun Message.mapChatTextRole(): ChatRole = + when (this) { + is Message.SystemPrompt -> ChatRole.System + is Message.Text -> mapChatTextRole() + is Message.ToolCalls -> ChatRole.Assistant + is Message.ToolResult -> ChatRole.Tool + is Message.Image -> mapImageTextRole() + } - private fun Message.Text.mapChatTextRole() = when (this.sender) { - Sender.Aigentic -> ChatRole.User - Sender.Model -> ChatRole.Assistant - } + private fun Message.Text.mapChatTextRole() = + when (this.sender) { + Sender.Aigentic -> ChatRole.User + Sender.Model -> ChatRole.Assistant + } - private fun Message.Image.mapImageTextRole() = when (this.sender) { - Sender.Aigentic -> ChatRole.User - Sender.Model -> ChatRole.Assistant - } + private fun Message.Image.mapImageTextRole() = + when (this.sender) { + Sender.Aigentic -> ChatRole.User + Sender.Model -> ChatRole.Assistant + } private fun Message.ToolCalls.mapToolCalls(): List = toolCalls .map { OpenAIToolCall.Function( id = ToolId(it.id.id), - function = FunctionCall(it.name, it.arguments) + function = FunctionCall(it.name, it.arguments), ) } - } diff --git a/src/providers/openai/src/commonMain/kotlin/community/flock/aigentic/mapper/ToolMapper.kt b/src/providers/openai/src/commonMain/kotlin/community/flock/aigentic/mapper/ToolMapper.kt index e31fae4..9b72a67 100644 --- a/src/providers/openai/src/commonMain/kotlin/community/flock/aigentic/mapper/ToolMapper.kt +++ b/src/providers/openai/src/commonMain/kotlin/community/flock/aigentic/mapper/ToolMapper.kt @@ -12,36 +12,40 @@ import kotlinx.serialization.json.putJsonArray import kotlinx.serialization.json.putJsonObject internal fun ToolDescription.toOpenAITool(): Tool { - - val toolParameters = if (parameters.isEmpty()) { - com.aallam.openai.api.core.Parameters.Empty - } else { - com.aallam.openai.api.core.Parameters.buildJsonObject { - put("type", "object") - emitPropertiesAndRequired(parameters) + val toolParameters = + if (parameters.isEmpty()) { + com.aallam.openai.api.core.Parameters.Empty + } else { + com.aallam.openai.api.core.Parameters.buildJsonObject { + put("type", "object") + emitPropertiesAndRequired(parameters) + } } - } return Tool.function( - name = name.value, description = description, parameters = toolParameters + name = name.value, + description = description, + parameters = toolParameters, ) } -private fun JsonObjectBuilder.emitType(definition: Parameter) = when (definition) { - is Parameter.Complex.Array -> "array" - is Parameter.Complex.Object -> "object" - is Parameter.Complex.Enum -> mapPrimitiveType(definition.valueType) - is Parameter.Primitive -> mapPrimitiveType(definition.type) -}.run { - put("type", this) -} +private fun JsonObjectBuilder.emitType(definition: Parameter) = + when (definition) { + is Parameter.Complex.Array -> "array" + is Parameter.Complex.Object -> "object" + is Parameter.Complex.Enum -> mapPrimitiveType(definition.valueType) + is Parameter.Primitive -> mapPrimitiveType(definition.type) + }.run { + put("type", this) + } -private fun mapPrimitiveType(type: ParameterType.Primitive) = when (type) { - ParameterType.Primitive.Boolean -> "boolean" - ParameterType.Primitive.Integer -> "integer" - ParameterType.Primitive.Number -> "number" - ParameterType.Primitive.String -> "string" -} +private fun mapPrimitiveType(type: ParameterType.Primitive) = + when (type) { + ParameterType.Primitive.Boolean -> "boolean" + ParameterType.Primitive.Integer -> "integer" + ParameterType.Primitive.Number -> "number" + ParameterType.Primitive.String -> "string" + } private fun JsonObjectBuilder.emit(definition: Parameter) { putJsonObject(definition.name) { @@ -53,10 +57,11 @@ private fun JsonObjectBuilder.emit(definition: Parameter) { } } -private fun JsonObjectBuilder.emitSpecificPropertiesIfNecessary(definition: Parameter) = when (definition) { - is Parameter.Complex -> emitSpecificProperties(definition) - is Parameter.Primitive -> Unit // Primitive types don't have any other properties -} +private fun JsonObjectBuilder.emitSpecificPropertiesIfNecessary(definition: Parameter) = + when (definition) { + is Parameter.Complex -> emitSpecificProperties(definition) + is Parameter.Primitive -> Unit // Primitive types don't have any other properties + } private fun JsonObjectBuilder.emitSpecificProperties(definition: Parameter.Complex): Unit = when (definition) { @@ -89,9 +94,10 @@ private fun JsonObjectBuilder.emitSpecificProperties(definition: Parameter.Compl emitPropertiesAndRequired(definition.parameters) } -private fun JsonObjectBuilder.emit(definitions: List): Unit = definitions.forEach { parameter -> - emit(parameter) -} +private fun JsonObjectBuilder.emit(definitions: List): Unit = + definitions.forEach { parameter -> + emit(parameter) + } fun JsonObjectBuilder.emitPropertiesAndRequired(parameters: List) { putJsonObject("properties") { diff --git a/src/providers/openai/src/commonMain/kotlin/community/flock/aigentic/model/OpenAIModel.kt b/src/providers/openai/src/commonMain/kotlin/community/flock/aigentic/model/OpenAIModel.kt index 04f7112..adf0dd5 100644 --- a/src/providers/openai/src/commonMain/kotlin/community/flock/aigentic/model/OpenAIModel.kt +++ b/src/providers/openai/src/commonMain/kotlin/community/flock/aigentic/model/OpenAIModel.kt @@ -14,6 +14,7 @@ import community.flock.aigentic.mapper.toModelResponse import community.flock.aigentic.request.createChatCompletionsRequest import kotlin.time.Duration.Companion.seconds +@Suppress("ktlint") sealed class OpenAIModelIdentifier( val stringValue: String ) : ModelIdentifier { @@ -24,24 +25,31 @@ sealed class OpenAIModelIdentifier( class OpenAIModel( override val authentication: Authentication.APIKey, override val modelIdentifier: OpenAIModelIdentifier, - private val openAI: OpenAI = defaultOpenAI(authentication) + private val openAI: OpenAI = defaultOpenAI(authentication), ) : Model { - - override suspend fun sendRequest(messages: List, tools: List): ModelResponse = openAI - .chatCompletion( - createChatCompletionsRequest( - messages = messages, - tools = tools, - openAIModelIdentifier = modelIdentifier + override suspend fun sendRequest( + messages: List, + tools: List, + ): ModelResponse = + openAI + .chatCompletion( + createChatCompletionsRequest( + messages = messages, + tools = tools, + openAIModelIdentifier = modelIdentifier, + ), ) - ) - .toModelResponse() + .toModelResponse() companion object { - fun defaultOpenAI(authentication: Authentication) = OpenAI( - token = (authentication as? Authentication.APIKey).let { - it?.key ?: error("OpenAI requires API Key authentication") - }, logging = LoggingConfig(LogLevel.None), timeout = Timeout(socket = 60.seconds) - ) + fun defaultOpenAI(authentication: Authentication) = + OpenAI( + token = + (authentication as? Authentication.APIKey).let { + it?.key ?: error("OpenAI requires API Key authentication") + }, + logging = LoggingConfig(LogLevel.None), + timeout = Timeout(socket = 60.seconds), + ) } } diff --git a/src/providers/openai/src/commonMain/kotlin/community/flock/aigentic/request/ChatCompletionsRequest.kt b/src/providers/openai/src/commonMain/kotlin/community/flock/aigentic/request/ChatCompletionsRequest.kt index dcc71c1..8ceb5f6 100644 --- a/src/providers/openai/src/commonMain/kotlin/community/flock/aigentic/request/ChatCompletionsRequest.kt +++ b/src/providers/openai/src/commonMain/kotlin/community/flock/aigentic/request/ChatCompletionsRequest.kt @@ -13,7 +13,7 @@ import community.flock.aigentic.model.OpenAIModelIdentifier internal fun createChatCompletionsRequest( messages: List, tools: List, - openAIModelIdentifier: OpenAIModelIdentifier + openAIModelIdentifier: OpenAIModelIdentifier, ): ChatCompletionRequest { return chatCompletionRequest { // temperature = 0.0 diff --git a/src/tools/http/src/commonMain/kotlin/community/flock/aigentic/tools/http/ArgumentsResolvers.kt b/src/tools/http/src/commonMain/kotlin/community/flock/aigentic/tools/http/ArgumentsResolvers.kt index 3dbc951..dfc336f 100644 --- a/src/tools/http/src/commonMain/kotlin/community/flock/aigentic/tools/http/ArgumentsResolvers.kt +++ b/src/tools/http/src/commonMain/kotlin/community/flock/aigentic/tools/http/ArgumentsResolvers.kt @@ -10,7 +10,6 @@ import kotlin.jvm.JvmInline @JvmInline value class ResolvedQueryParameters(val values: Map) { - companion object { fun empty() = ResolvedQueryParameters(emptyMap()) } @@ -22,41 +21,40 @@ value class ResolvedRequestBody(val stringBody: String) @JvmInline value class ResolvedUrl(val urlString: String) - interface QueryParametersArgumentsResolver { - fun resolveQueryParameters( - queryParameters: List, callArguments: JsonObject + queryParameters: List, + callArguments: JsonObject, ): ResolvedQueryParameters } class DefaultQueryParametersArgumentsResolver : QueryParametersArgumentsResolver { - override fun resolveQueryParameters( - queryParameters: List, callArguments: JsonObject - ): ResolvedQueryParameters = queryParameters.mapNotNull { queryParameter -> - - val parameterValue = queryParameter.getParameterValue(callArguments) - parameterValue?.let { - queryParameter.name to it + queryParameters: List, + callArguments: JsonObject, + ): ResolvedQueryParameters = + queryParameters.mapNotNull { queryParameter -> + + val parameterValue = queryParameter.getParameterValue(callArguments) + parameterValue?.let { + queryParameter.name to it + } + }.toMap().let { + ResolvedQueryParameters(it) } - - }.toMap().let { - ResolvedQueryParameters(it) - } } - interface RequestBodyArgumentsResolver { fun resolveRequestBody( - requestBodyParameter: Parameter.Complex.Object, callArguments: JsonObject + requestBodyParameter: Parameter.Complex.Object, + callArguments: JsonObject, ): ResolvedRequestBody? } class DefaultRequestBodyArgumentsResolver : RequestBodyArgumentsResolver { - override fun resolveRequestBody( - requestBodyParameter: Parameter.Complex.Object, callArguments: JsonObject + requestBodyParameter: Parameter.Complex.Object, + callArguments: JsonObject, ): ResolvedRequestBody? { val parameterValue = requestBodyParameter.getParameterValue(callArguments) return parameterValue?.let { ResolvedRequestBody(Json.encodeToString(it)) } @@ -65,15 +63,18 @@ class DefaultRequestBodyArgumentsResolver : RequestBodyArgumentsResolver { interface UrlArgumentsResolver { fun resolveUrl( - placeHolderUrl: String, pathParameters: List, callArguments: JsonObject + placeHolderUrl: String, + pathParameters: List, + callArguments: JsonObject, ): ResolvedUrl } class DefaultUrlArgumentsResolver : UrlArgumentsResolver { override fun resolveUrl( - placeHolderUrl: String, pathParameters: List, callArguments: JsonObject + placeHolderUrl: String, + pathParameters: List, + callArguments: JsonObject, ): ResolvedUrl { - // TODO Url encoding? return pathParameters.fold(placeHolderUrl) { url, pathParam -> val paramValue = pathParam.getParameterValue(callArguments) diff --git a/src/tools/http/src/commonMain/kotlin/community/flock/aigentic/tools/http/EndpointOperation.kt b/src/tools/http/src/commonMain/kotlin/community/flock/aigentic/tools/http/EndpointOperation.kt index 935ad8f..377c44f 100644 --- a/src/tools/http/src/commonMain/kotlin/community/flock/aigentic/tools/http/EndpointOperation.kt +++ b/src/tools/http/src/commonMain/kotlin/community/flock/aigentic/tools/http/EndpointOperation.kt @@ -17,8 +17,10 @@ data class EndpointOperation( enum class Method { GET, POST, PUT, DELETE, PATCH } } -fun EndpointOperation.toToolDefinition(restClientExecutor: RestClientExecutor, headers: List
): Tool { - +fun EndpointOperation.toToolDefinition( + restClientExecutor: RestClientExecutor, + headers: List
, +): Tool { val allParameterDefinitions = pathParams + queryParams + listOfNotNull(requestBody) return object : Tool { diff --git a/src/tools/http/src/commonMain/kotlin/community/flock/aigentic/tools/http/Authentication.kt b/src/tools/http/src/commonMain/kotlin/community/flock/aigentic/tools/http/Header.kt similarity index 99% rename from src/tools/http/src/commonMain/kotlin/community/flock/aigentic/tools/http/Authentication.kt rename to src/tools/http/src/commonMain/kotlin/community/flock/aigentic/tools/http/Header.kt index 7ac285a..9534401 100644 --- a/src/tools/http/src/commonMain/kotlin/community/flock/aigentic/tools/http/Authentication.kt +++ b/src/tools/http/src/commonMain/kotlin/community/flock/aigentic/tools/http/Header.kt @@ -1,7 +1,6 @@ package community.flock.aigentic.tools.http sealed interface Header { - val name: String val value: String diff --git a/src/tools/http/src/commonMain/kotlin/community/flock/aigentic/tools/http/HttpCientExecutor.kt b/src/tools/http/src/commonMain/kotlin/community/flock/aigentic/tools/http/HttpCientExecutor.kt deleted file mode 100644 index 5a6b3aa..0000000 --- a/src/tools/http/src/commonMain/kotlin/community/flock/aigentic/tools/http/HttpCientExecutor.kt +++ /dev/null @@ -1,37 +0,0 @@ -package community.flock.aigentic.tools.http - -import kotlinx.serialization.json.JsonObject - -class RestClientExecutor( - val restClient: RestClient , - val urlArgumentsResolver: UrlArgumentsResolver, - val queryParametersArgumentsResolver: QueryParametersArgumentsResolver, - val requestBodyArgumentsResolver: RequestBodyArgumentsResolver -) { - - suspend fun execute(operation: EndpointOperation, callArguments: JsonObject, headers: List
): String = - restClient.execute( - method = operation.method, - resolvedUrl = urlArgumentsResolver.resolveUrl(operation.url, operation.pathParams, callArguments), - resolvedQueryParameters = queryParametersArgumentsResolver.resolveQueryParameters( - operation.queryParams, - callArguments - ), - resolvedRequestBody = operation.requestBody?.let { - requestBodyArgumentsResolver.resolveRequestBody( - requestBodyParameter = it, - callArguments = callArguments - ) - }, - headers = headers - ) - - companion object { - val default = RestClientExecutor( - restClient = KtorRestClient(), - urlArgumentsResolver = DefaultUrlArgumentsResolver(), - queryParametersArgumentsResolver = DefaultQueryParametersArgumentsResolver(), - requestBodyArgumentsResolver = DefaultRequestBodyArgumentsResolver() - ) - } -} diff --git a/src/tools/http/src/commonMain/kotlin/community/flock/aigentic/tools/http/HttpClient.kt b/src/tools/http/src/commonMain/kotlin/community/flock/aigentic/tools/http/HttpClient.kt index ea6cf46..8f1e76d 100644 --- a/src/tools/http/src/commonMain/kotlin/community/flock/aigentic/tools/http/HttpClient.kt +++ b/src/tools/http/src/commonMain/kotlin/community/flock/aigentic/tools/http/HttpClient.kt @@ -1,13 +1,22 @@ package community.flock.aigentic.tools.http -import io.ktor.client.* -import io.ktor.client.call.* -import io.ktor.client.engine.* -import io.ktor.client.plugins.contentnegotiation.* -import io.ktor.client.plugins.logging.* -import io.ktor.client.request.* -import io.ktor.http.* -import io.ktor.serialization.kotlinx.json.* +import io.ktor.client.HttpClient +import io.ktor.client.HttpClientConfig +import io.ktor.client.call.body +import io.ktor.client.engine.HttpClientEngine +import io.ktor.client.plugins.contentnegotiation.ContentNegotiation +import io.ktor.client.plugins.logging.LogLevel +import io.ktor.client.plugins.logging.Logger +import io.ktor.client.plugins.logging.Logging +import io.ktor.client.plugins.logging.SIMPLE +import io.ktor.client.request.headers +import io.ktor.client.request.request +import io.ktor.client.request.setBody +import io.ktor.http.ContentType +import io.ktor.http.HttpMethod +import io.ktor.http.URLBuilder +import io.ktor.http.contentType +import io.ktor.serialization.kotlinx.json.json import kotlinx.serialization.json.JsonArray import kotlinx.serialization.json.jsonArray import kotlinx.serialization.json.jsonPrimitive @@ -18,12 +27,11 @@ interface RestClient { resolvedUrl: ResolvedUrl, resolvedQueryParameters: ResolvedQueryParameters, resolvedRequestBody: ResolvedRequestBody?, - headers: List
+ headers: List
, ): String } class KtorRestClient(engine: HttpClientEngine? = null) : RestClient { - private val configuration: HttpClientConfig<*>.() -> Unit = { install(ContentNegotiation) { json() @@ -41,22 +49,23 @@ class KtorRestClient(engine: HttpClientEngine? = null) : RestClient { resolvedUrl: ResolvedUrl, resolvedQueryParameters: ResolvedQueryParameters, resolvedRequestBody: ResolvedRequestBody?, - headers: List
- ): String = ktor.request(resolvedUrl.urlString) { - headers { - headers.forEach { header -> - append(header.name, header.value) + headers: List
, + ): String = + ktor.request(resolvedUrl.urlString) { + headers { + headers.forEach { header -> + append(header.name, header.value) + } } - } - contentType(ContentType.Application.Json) - this.method = method.toKtorMethod() - url { - addQueryParameters(resolvedQueryParameters) - } - resolvedRequestBody?.let { - setBody(it.stringBody) - } - }.body() + contentType(ContentType.Application.Json) + this.method = method.toKtorMethod() + url { + addQueryParameters(resolvedQueryParameters) + } + resolvedRequestBody?.let { + setBody(it.stringBody) + } + }.body() private fun URLBuilder.addQueryParameters(resolvedQueryParameters: ResolvedQueryParameters) = resolvedQueryParameters.values.forEach { (name, value) -> @@ -68,11 +77,12 @@ class KtorRestClient(engine: HttpClientEngine? = null) : RestClient { } } - private fun EndpointOperation.Method.toKtorMethod() = when (this) { - EndpointOperation.Method.GET -> HttpMethod.Get - EndpointOperation.Method.POST -> HttpMethod.Post - EndpointOperation.Method.PUT -> HttpMethod.Put - EndpointOperation.Method.DELETE -> HttpMethod.Delete - EndpointOperation.Method.PATCH -> HttpMethod.Patch - } + private fun EndpointOperation.Method.toKtorMethod() = + when (this) { + EndpointOperation.Method.GET -> HttpMethod.Get + EndpointOperation.Method.POST -> HttpMethod.Post + EndpointOperation.Method.PUT -> HttpMethod.Put + EndpointOperation.Method.DELETE -> HttpMethod.Delete + EndpointOperation.Method.PATCH -> HttpMethod.Patch + } } diff --git a/src/tools/http/src/commonMain/kotlin/community/flock/aigentic/tools/http/RestClientExecutor.kt b/src/tools/http/src/commonMain/kotlin/community/flock/aigentic/tools/http/RestClientExecutor.kt new file mode 100644 index 0000000..7031a72 --- /dev/null +++ b/src/tools/http/src/commonMain/kotlin/community/flock/aigentic/tools/http/RestClientExecutor.kt @@ -0,0 +1,43 @@ +package community.flock.aigentic.tools.http + +import kotlinx.serialization.json.JsonObject + +class RestClientExecutor( + val restClient: RestClient, + val urlArgumentsResolver: UrlArgumentsResolver, + val queryParametersArgumentsResolver: QueryParametersArgumentsResolver, + val requestBodyArgumentsResolver: RequestBodyArgumentsResolver, +) { + suspend fun execute( + operation: EndpointOperation, + callArguments: JsonObject, + headers: List
, + ): String = + restClient.execute( + method = operation.method, + resolvedUrl = urlArgumentsResolver.resolveUrl(operation.url, operation.pathParams, callArguments), + resolvedQueryParameters = + queryParametersArgumentsResolver.resolveQueryParameters( + operation.queryParams, + callArguments, + ), + resolvedRequestBody = + operation.requestBody?.let { + requestBodyArgumentsResolver.resolveRequestBody( + requestBodyParameter = it, + callArguments = callArguments, + ) + }, + headers = headers, + ) + + companion object { + val default = + RestClientExecutor( + restClient = KtorRestClient(), + urlArgumentsResolver = DefaultUrlArgumentsResolver(), + queryParametersArgumentsResolver = DefaultQueryParametersArgumentsResolver(), + requestBodyArgumentsResolver = DefaultRequestBodyArgumentsResolver(), + ) + } +} diff --git a/src/tools/http/src/jvmTest/kotlin/community/flock/aigentic/tools/http/ArgumentsResolverTest.kt b/src/tools/http/src/jvmTest/kotlin/community/flock/aigentic/tools/http/ArgumentsResolverTest.kt index e8ad824..3dea7a0 100644 --- a/src/tools/http/src/jvmTest/kotlin/community/flock/aigentic/tools/http/ArgumentsResolverTest.kt +++ b/src/tools/http/src/jvmTest/kotlin/community/flock/aigentic/tools/http/ArgumentsResolverTest.kt @@ -1,6 +1,7 @@ -package http +package community.flock.aigentic.tools.http -import community.flock.aigentic.tools.http.* +import http.createObjectParameter +import http.createPrimitiveParameter import io.kotest.assertions.throwables.shouldThrow import io.kotest.core.spec.style.DescribeSpec import io.kotest.matchers.shouldBe @@ -21,37 +22,41 @@ class ArgumentsResolverTest : DescribeSpec({ it("should replace single path parameters in url") { - val jsonArguments = buildJsonObject { - put(itemIdParameterName, "123") - } + val jsonArguments = + buildJsonObject { + put(itemIdParameterName, "123") + } val url = "https://localhost/items/{itemId}" - val pathParameters = listOf( - createPrimitiveParameter(itemIdParameterName) - ) + val pathParameters = + listOf( + createPrimitiveParameter(itemIdParameterName), + ) resolver.resolveUrl( - url, pathParameters, jsonArguments + url, pathParameters, jsonArguments, ) shouldBe ResolvedUrl("https://localhost/items/123") } it("should replace multiple path parameters in url") { - val jsonArguments = buildJsonObject { - put(itemIdParameterName, "123") - put(commentIdParameterName, "456") - } + val jsonArguments = + buildJsonObject { + put(itemIdParameterName, "123") + put(commentIdParameterName, "456") + } val url = "https://localhost/items/{itemId}/comments/{commentId}" - val pathParameters = listOf( - createPrimitiveParameter(itemIdParameterName), - createPrimitiveParameter(commentIdParameterName), - ) + val pathParameters = + listOf( + createPrimitiveParameter(itemIdParameterName), + createPrimitiveParameter(commentIdParameterName), + ) resolver.resolveUrl( - url, pathParameters, jsonArguments + url, pathParameters, jsonArguments, ) shouldBe ResolvedUrl("https://localhost/items/123/comments/456") } } @@ -60,17 +65,19 @@ class ArgumentsResolverTest : DescribeSpec({ it("should throw exception when path parameter is not in call arguments") { - val jsonArguments = buildJsonObject { - put(itemIdParameterName, "123") - put("someOtherParam", "456") - } + val jsonArguments = + buildJsonObject { + put(itemIdParameterName, "123") + put("someOtherParam", "456") + } val url = "https://localhost/items/{itemId}/comments/{commentId}" - val pathParameters = listOf( - createPrimitiveParameter(itemIdParameterName), - createPrimitiveParameter(commentIdParameterName), - ) + val pathParameters = + listOf( + createPrimitiveParameter(itemIdParameterName), + createPrimitiveParameter(commentIdParameterName), + ) shouldThrow { resolver.resolveUrl(url, pathParameters, jsonArguments) @@ -79,23 +86,24 @@ class ArgumentsResolverTest : DescribeSpec({ it("should throw exception if the number of placeholders in the url is greater than the number of pathParameters") { - val jsonArguments = buildJsonObject { - put(itemIdParameterName, "123") - put(commentIdParameterName, "456") - } + val jsonArguments = + buildJsonObject { + put(itemIdParameterName, "123") + put(commentIdParameterName, "456") + } val url = "https://localhost/items/{itemId}/comments/{commentId}" - val pathParameters = listOf( - createPrimitiveParameter(itemIdParameterName), - ) + val pathParameters = + listOf( + createPrimitiveParameter(itemIdParameterName), + ) shouldThrow { resolver.resolveUrl(url, pathParameters, jsonArguments) } } } - } describe("DefaultRequestBodyArgumentsResolver") { @@ -107,16 +115,17 @@ class ArgumentsResolverTest : DescribeSpec({ val bodyParameter = createObjectParameter( name = "body", - parameters = listOf(createPrimitiveParameter("someProperty")) + parameters = listOf(createPrimitiveParameter("someProperty")), ) - val jsonArguments = buildJsonObject { - put("body", """{ "someProperty" : "hello" }""") - } + val jsonArguments = + buildJsonObject { + put("body", """{ "someProperty" : "hello" }""") + } resolver.resolveRequestBody( bodyParameter, - jsonArguments + jsonArguments, ) shouldBe ResolvedRequestBody(""""{ \"someProperty\" : \"hello\" }"""") } } @@ -128,36 +137,41 @@ class ArgumentsResolverTest : DescribeSpec({ describe("happy path") { it("should resolve all query parameter arguments") { - val jsonArguments = buildJsonObject { - put(itemIdParameterName, "123") - put(commentIdParameterName, "456") - } + val jsonArguments = + buildJsonObject { + put(itemIdParameterName, "123") + put(commentIdParameterName, "456") + } - val queryParameters = listOf( - createPrimitiveParameter(itemIdParameterName) - ) + val queryParameters = + listOf( + createPrimitiveParameter(itemIdParameterName), + ) resolver.resolveQueryParameters( - queryParameters, jsonArguments - ) shouldBe ResolvedQueryParameters( - mapOf( - itemIdParameterName to JsonPrimitive("123") + queryParameters, jsonArguments, + ) shouldBe + ResolvedQueryParameters( + mapOf( + itemIdParameterName to JsonPrimitive("123"), + ), ) - ) } it("should resolve all non-required query parameter arguments when argument is not provided") { - val jsonArguments = buildJsonObject { - put(commentIdParameterName, "456") - } + val jsonArguments = + buildJsonObject { + put(commentIdParameterName, "456") + } - val queryParameters = listOf( - createPrimitiveParameter(itemIdParameterName, isRequired = false) - ) + val queryParameters = + listOf( + createPrimitiveParameter(itemIdParameterName, isRequired = false), + ) resolver.resolveQueryParameters( - queryParameters, jsonArguments + queryParameters, jsonArguments, ) shouldBe ResolvedQueryParameters(emptyMap()) } } @@ -166,23 +180,23 @@ class ArgumentsResolverTest : DescribeSpec({ it("should throw exception if parameter is required and argument is not provided") { - val jsonArguments = buildJsonObject { - put(commentIdParameterName, "456") - } + val jsonArguments = + buildJsonObject { + put(commentIdParameterName, "456") + } - val queryParameters = listOf( - createPrimitiveParameter(itemIdParameterName, isRequired = true) - ) + val queryParameters = + listOf( + createPrimitiveParameter(itemIdParameterName, isRequired = true), + ) shouldThrow { resolver.resolveQueryParameters( - queryParameters, jsonArguments + queryParameters, + jsonArguments, ) } } - - } } - }) diff --git a/src/tools/http/src/jvmTest/kotlin/community/flock/aigentic/tools/http/EndpointOperationsTest.kt b/src/tools/http/src/jvmTest/kotlin/community/flock/aigentic/tools/http/EndpointOperationsTest.kt index 36c5fb1..3ff1018 100644 --- a/src/tools/http/src/jvmTest/kotlin/community/flock/aigentic/tools/http/EndpointOperationsTest.kt +++ b/src/tools/http/src/jvmTest/kotlin/community/flock/aigentic/tools/http/EndpointOperationsTest.kt @@ -1,4 +1,4 @@ -package http +package community.flock.aigentic.tools.http import community.flock.aigentic.core.tool.PrimitiveValue import io.kotest.core.spec.style.DescribeSpec diff --git a/src/tools/http/src/jvmTest/kotlin/community/flock/aigentic/tools/http/RestClientExecutorTest.kt b/src/tools/http/src/jvmTest/kotlin/community/flock/aigentic/tools/http/RestClientExecutorTest.kt index 19cdcc2..344e263 100644 --- a/src/tools/http/src/jvmTest/kotlin/community/flock/aigentic/tools/http/RestClientExecutorTest.kt +++ b/src/tools/http/src/jvmTest/kotlin/community/flock/aigentic/tools/http/RestClientExecutorTest.kt @@ -1,8 +1,7 @@ -package http +package community.flock.aigentic.tools.http import community.flock.aigentic.core.tool.Parameter import community.flock.aigentic.core.tool.ParameterType -import community.flock.aigentic.tools.http.* import io.kotest.core.spec.style.DescribeSpec import io.kotest.matchers.shouldBe import io.mockk.coEvery @@ -20,84 +19,108 @@ class RestClientExecutorTest : DescribeSpec({ it("should execute operation with callArguments") { - val operation = EndpointOperation( - name = "getItemsByCategory", - description = null, - method = EndpointOperation.Method.GET, - pathParams = listOf( - Parameter.Primitive( - name = "id", description = "Item id", isRequired = true, type = ParameterType.Primitive.Integer - ) - ), - url = "https://example.com/api/items/{id}", - queryParams = listOf( - Parameter.Primitive( - name = "category", - description = "The category of the items to retrieve", - isRequired = true, - type = ParameterType.Primitive.String - ) - ), - requestBody = Parameter.Complex.Object( - name = "body", description = "Item to be created", parameters = listOf( - Parameter.Primitive( - name = "name", description = null, isRequired = true, type = ParameterType.Primitive.String + val operation = + EndpointOperation( + name = "getItemsByCategory", + description = null, + method = EndpointOperation.Method.GET, + pathParams = + listOf( + Parameter.Primitive( + name = "id", + description = "Item id", + isRequired = true, + type = ParameterType.Primitive.Integer, + ), ), - Parameter.Primitive( - name = "price", description = null, isRequired = true, type = ParameterType.Primitive.Number + url = "https://example.com/api/items/{id}", + queryParams = + listOf( + Parameter.Primitive( + name = "category", + description = "The category of the items to retrieve", + isRequired = true, + type = ParameterType.Primitive.String, + ), + ), + requestBody = + Parameter.Complex.Object( + name = "body", + description = "Item to be created", + parameters = + listOf( + Parameter.Primitive( + name = "name", + description = null, + isRequired = true, + type = ParameterType.Primitive.String, + ), + Parameter.Primitive( + name = "price", + description = null, + isRequired = true, + type = ParameterType.Primitive.Number, + ), + ), + isRequired = true, ), - ), isRequired = true ) - ) - val headers = listOf( - Header.CustomHeader("api-key", "secret-value") - ) + val headers = + listOf( + Header.CustomHeader("api-key", "secret-value"), + ) - val callArguments = buildJsonObject { - put("id", 2) - put("category", "furniture") - } + val callArguments = + buildJsonObject { + put("id", 2) + put("category", "furniture") + } val expectedUrl = ResolvedUrl("https://example.com/api/items/2") val expectedQueryParameters = ResolvedQueryParameters(mapOf("category" to JsonPrimitive("furniture"))) val expectedRequestBody = ResolvedRequestBody("""{ "name" : "someName", "price" : 12 }""") - val restClient = mockk().apply { - coEvery { - execute( - method = operation.method, - resolvedUrl = expectedUrl, - resolvedQueryParameters = expectedQueryParameters, - resolvedRequestBody = expectedRequestBody, - headers = headers - ) - } returns "someResult" - } - - val urlArgumentsResolver = mockk().apply { - every { resolveUrl(operation.url, operation.pathParams, callArguments) } returns expectedUrl - } - - val queryParametersArgumentsResolver = mockk().apply { - every { resolveQueryParameters(operation.queryParams, callArguments) } returns expectedQueryParameters - } - - val requestBodyArgumentsResolver = mockk().apply { - every { resolveRequestBody(operation.requestBody!!, callArguments) } returns expectedRequestBody - } - - val executor = RestClientExecutor( - restClient = restClient, - urlArgumentsResolver = urlArgumentsResolver, - queryParametersArgumentsResolver = queryParametersArgumentsResolver, - requestBodyArgumentsResolver = requestBodyArgumentsResolver - ) + val restClient = + mockk().apply { + coEvery { + execute( + method = operation.method, + resolvedUrl = expectedUrl, + resolvedQueryParameters = expectedQueryParameters, + resolvedRequestBody = expectedRequestBody, + headers = headers, + ) + } returns "someResult" + } + + val urlArgumentsResolver = + mockk().apply { + every { resolveUrl(operation.url, operation.pathParams, callArguments) } returns expectedUrl + } + + val queryParametersArgumentsResolver = + mockk().apply { + every { resolveQueryParameters(operation.queryParams, callArguments) } returns expectedQueryParameters + } + + val requestBodyArgumentsResolver = + mockk().apply { + every { resolveRequestBody(operation.requestBody!!, callArguments) } returns expectedRequestBody + } + + val executor = + RestClientExecutor( + restClient = restClient, + urlArgumentsResolver = urlArgumentsResolver, + queryParametersArgumentsResolver = queryParametersArgumentsResolver, + requestBodyArgumentsResolver = requestBodyArgumentsResolver, + ) executor.execute( operation = operation, callArguments = callArguments, - headers = headers + headers = headers, ) shouldBe "someResult" coVerify(exactly = 1) { restClient.execute(any(), any(), any(), any(), any()) } @@ -105,7 +128,5 @@ class RestClientExecutorTest : DescribeSpec({ verify(exactly = 1) { queryParametersArgumentsResolver.resolveQueryParameters(any(), any()) } verify(exactly = 1) { requestBodyArgumentsResolver.resolveRequestBody(any(), any()) } } - } - }) diff --git a/src/tools/http/src/jvmTest/kotlin/community/flock/aigentic/tools/http/RestClientTest.kt b/src/tools/http/src/jvmTest/kotlin/community/flock/aigentic/tools/http/RestClientTest.kt index a22f25c..38393ac 100644 --- a/src/tools/http/src/jvmTest/kotlin/community/flock/aigentic/tools/http/RestClientTest.kt +++ b/src/tools/http/src/jvmTest/kotlin/community/flock/aigentic/tools/http/RestClientTest.kt @@ -1,17 +1,18 @@ -package http +package community.flock.aigentic.tools.http -import community.flock.aigentic.tools.http.* import io.kotest.core.spec.style.DescribeSpec import io.kotest.datatest.withData import io.kotest.matchers.shouldBe -import io.ktor.client.engine.mock.* -import io.ktor.client.request.* -import io.ktor.client.utils.* -import io.ktor.http.* -import io.ktor.http.content.* +import io.ktor.client.engine.mock.MockEngine +import io.ktor.client.engine.mock.respond +import io.ktor.client.request.HttpRequestData +import io.ktor.client.utils.EmptyContent +import io.ktor.http.HttpMethod +import io.ktor.http.HttpStatusCode +import io.ktor.http.content.TextContent import kotlinx.serialization.json.JsonArray import kotlinx.serialization.json.JsonPrimitive -import java.util.* +import java.util.UUID class RestClientTest : DescribeSpec({ @@ -28,43 +29,46 @@ class RestClientTest : DescribeSpec({ EndpointOperation.Method.PATCH to HttpMethod.Patch, ) { (endpointOperationMethod, ktorMethod) -> - KtorRestClient(requestAssertions { - it.method shouldBe ktorMethod - }).execute( + KtorRestClient( + requestAssertions { + it.method shouldBe ktorMethod + }, + ).execute( method = endpointOperationMethod, resolvedUrl = testUrl, resolvedQueryParameters = ResolvedQueryParameters.empty(), resolvedRequestBody = null, - headers = emptyList() + headers = emptyList(), ) } } context("should add body if there is one") { withData( - ResolvedRequestBody("""{ "name" : "someValue" }"""), null + ResolvedRequestBody("""{ "name" : "someValue" }"""), + null, ) { resolvedRequestBody -> - KtorRestClient(requestAssertions { request: HttpRequestData -> - - if (resolvedRequestBody != null) { - (request.body as TextContent).let { - it.contentType.contentType shouldBe "application" - it.contentType.contentSubtype shouldBe "json" - it.text shouldBe resolvedRequestBody.stringBody + KtorRestClient( + requestAssertions { request: HttpRequestData -> + + if (resolvedRequestBody != null) { + (request.body as TextContent).let { + it.contentType.contentType shouldBe "application" + it.contentType.contentSubtype shouldBe "json" + it.text shouldBe resolvedRequestBody.stringBody + } + } else { + request.body shouldBe EmptyContent } - } else { - request.body shouldBe EmptyContent - } - - }).execute( + }, + ).execute( method = EndpointOperation.Method.GET, resolvedUrl = testUrl, resolvedQueryParameters = ResolvedQueryParameters.empty(), resolvedRequestBody = resolvedRequestBody, - headers = emptyList() + headers = emptyList(), ) - } } @@ -74,59 +78,63 @@ class RestClientTest : DescribeSpec({ nameFn = { it.values.toString() }, ResolvedQueryParameters( mapOf( - "itemId" to JsonPrimitive(123) - ) + "itemId" to JsonPrimitive(123), + ), ), ResolvedQueryParameters( mapOf( - "name" to JsonPrimitive("someName") - ) + "name" to JsonPrimitive("someName"), + ), ), ResolvedQueryParameters( mapOf( "itemId" to JsonPrimitive(123), "name" to JsonPrimitive("someName"), - ) + ), ), ResolvedQueryParameters( mapOf( - "isAvailable" to JsonPrimitive(true) - ) + "isAvailable" to JsonPrimitive(true), + ), ), ResolvedQueryParameters( mapOf( - "ids" to JsonArray(listOf(JsonPrimitive(1), JsonPrimitive(2), JsonPrimitive(3))) - ) + "ids" to JsonArray(listOf(JsonPrimitive(1), JsonPrimitive(2), JsonPrimitive(3))), + ), ), ) { resolvedQueryParameters -> - KtorRestClient(requestAssertions { request: HttpRequestData -> - request.url.parameters.entries().size shouldBe resolvedQueryParameters.values.size - }).execute( + KtorRestClient( + requestAssertions { request: HttpRequestData -> + request.url.parameters.entries().size shouldBe resolvedQueryParameters.values.size + }, + ).execute( method = EndpointOperation.Method.GET, resolvedUrl = testUrl, resolvedQueryParameters = resolvedQueryParameters, resolvedRequestBody = null, - headers = emptyList() + headers = emptyList(), ) - } } it("should add multiple url query components for array query parameters") { - val resolvedQueryParameters = ResolvedQueryParameters( - mapOf( - "ids" to JsonArray(listOf(JsonPrimitive(1), JsonPrimitive(2), JsonPrimitive(3))) + val resolvedQueryParameters = + ResolvedQueryParameters( + mapOf( + "ids" to JsonArray(listOf(JsonPrimitive(1), JsonPrimitive(2), JsonPrimitive(3))), + ), ) - ) - KtorRestClient(requestAssertions { request: HttpRequestData -> - request.url.toString() shouldBe "https://some-url.com?ids=1&ids=2&ids=3" - }).execute( + KtorRestClient( + requestAssertions { request: HttpRequestData -> + request.url.toString() shouldBe "https://some-url.com?ids=1&ids=2&ids=3" + }, + ).execute( method = EndpointOperation.Method.GET, resolvedUrl = testUrl, resolvedQueryParameters = resolvedQueryParameters, resolvedRequestBody = null, - headers = emptyList() + headers = emptyList(), ) } @@ -134,27 +142,31 @@ class RestClientTest : DescribeSpec({ val expectedUrl = "http://localhost/some-url" - KtorRestClient(requestAssertions { request -> - request.url.toString() shouldBe expectedUrl - }).execute( + KtorRestClient( + requestAssertions { request -> + request.url.toString() shouldBe expectedUrl + }, + ).execute( method = EndpointOperation.Method.GET, resolvedUrl = ResolvedUrl(expectedUrl), resolvedQueryParameters = ResolvedQueryParameters.empty(), resolvedRequestBody = null, - headers = emptyList() + headers = emptyList(), ) } it("should use json header") { - KtorRestClient(requestAssertions { request -> - request.headers["Content-Type"] shouldBe "application/json" - }).execute( + KtorRestClient( + requestAssertions { request -> + request.headers["Content-Type"] shouldBe "application/json" + }, + ).execute( method = EndpointOperation.Method.GET, resolvedUrl = testUrl, resolvedQueryParameters = ResolvedQueryParameters.empty(), resolvedRequestBody = null, - headers = emptyList() + headers = emptyList(), ) } @@ -165,20 +177,21 @@ class RestClientTest : DescribeSpec({ resolvedUrl = testUrl, resolvedQueryParameters = ResolvedQueryParameters.empty(), resolvedRequestBody = null, - headers = emptyList() + headers = emptyList(), ) shouldBe expectedResult } } - }) private val expectedResult = UUID.randomUUID().toString() -private fun requestAssertions(result: String = expectedResult, assertBlock: (request: HttpRequestData) -> Unit) = - MockEngine { request -> - assertBlock(request) - respond( - content = result, - status = HttpStatusCode.OK, - ) - } +private fun requestAssertions( + result: String = expectedResult, + assertBlock: (request: HttpRequestData) -> Unit, +) = MockEngine { request -> + assertBlock(request) + respond( + content = result, + status = HttpStatusCode.OK, + ) +} diff --git a/src/tools/http/src/jvmTest/kotlin/community/flock/aigentic/tools/http/Util.kt b/src/tools/http/src/jvmTest/kotlin/community/flock/aigentic/tools/http/Util.kt index 7a44441..41f54a2 100644 --- a/src/tools/http/src/jvmTest/kotlin/community/flock/aigentic/tools/http/Util.kt +++ b/src/tools/http/src/jvmTest/kotlin/community/flock/aigentic/tools/http/Util.kt @@ -4,15 +4,23 @@ import community.flock.aigentic.core.tool.Parameter import community.flock.aigentic.core.tool.ParameterType fun createPrimitiveParameter( - name: String, type: ParameterType.Primitive = ParameterType.Primitive.String, isRequired: Boolean = true + name: String, + type: ParameterType.Primitive = ParameterType.Primitive.String, + isRequired: Boolean = true, ) = Parameter.Primitive( - name = name, description = null, isRequired = isRequired, type = type + name = name, + description = null, + isRequired = isRequired, + type = type, ) fun createObjectParameter( name: String, isRequired: Boolean = true, - parameters: List + parameters: List, ) = Parameter.Complex.Object( - name = name, description = null, isRequired = isRequired, parameters = parameters + name = name, + description = null, + isRequired = isRequired, + parameters = parameters, ) diff --git a/src/tools/openapi/src/commonMain/kotlin/community/flock/aigentic/tools/openapi/OASParser.kt b/src/tools/openapi/src/commonMain/kotlin/community/flock/aigentic/tools/openapi/OASParser.kt index 840022b..34cda08 100644 --- a/src/tools/openapi/src/commonMain/kotlin/community/flock/aigentic/tools/openapi/OASParser.kt +++ b/src/tools/openapi/src/commonMain/kotlin/community/flock/aigentic/tools/openapi/OASParser.kt @@ -1,11 +1,17 @@ package community.flock.aigentic.tools.openapi +import community.flock.aigentic.core.logging.Logger +import community.flock.aigentic.core.logging.SimpleLogger import community.flock.aigentic.core.tool.Parameter import community.flock.aigentic.core.tool.ParameterType import community.flock.aigentic.core.tool.ParameterType.Primitive import community.flock.aigentic.core.tool.PrimitiveValue import community.flock.aigentic.tools.http.EndpointOperation import community.flock.aigentic.tools.http.EndpointOperation.Method.DELETE +import community.flock.aigentic.tools.http.EndpointOperation.Method.GET +import community.flock.aigentic.tools.http.EndpointOperation.Method.PATCH +import community.flock.aigentic.tools.http.EndpointOperation.Method.POST +import community.flock.aigentic.tools.http.EndpointOperation.Method.PUT import community.flock.kotlinx.openapi.bindings.v3.MediaType import community.flock.kotlinx.openapi.bindings.v3.OpenAPI import community.flock.kotlinx.openapi.bindings.v3.OpenAPIObject @@ -20,37 +26,38 @@ import community.flock.kotlinx.openapi.bindings.v3.RequestBodyOrReferenceObject import community.flock.kotlinx.openapi.bindings.v3.SchemaObject import community.flock.kotlinx.openapi.bindings.v3.SchemaOrReferenceObject import community.flock.kotlinx.openapi.bindings.v3.Type -import community.flock.aigentic.core.logging.Logger -import community.flock.aigentic.core.logging.SimpleLogger -import community.flock.aigentic.tools.http.EndpointOperation.Method.GET -import community.flock.aigentic.tools.http.EndpointOperation.Method.PATCH -import community.flock.aigentic.tools.http.EndpointOperation.Method.POST -import community.flock.aigentic.tools.http.EndpointOperation.Method.PUT import kotlinx.serialization.json.Json import kotlinx.serialization.json.jsonPrimitive class OpenAPIv3Parser( - private val openApi: OpenAPIObject, private val logger: Logger + private val openApi: OpenAPIObject, + private val logger: Logger, ) { - companion object { - fun parseOperations(json: String, logger: Logger = SimpleLogger): List = + fun parseOperations( + json: String, + logger: Logger = SimpleLogger, + ): List = OpenAPI(json = Json { ignoreUnknownKeys = true }).decodeFromString(json).let { OpenAPIv3Parser(it, logger).getEndpointOperations() } } - fun getEndpointOperations(): List = openApi.paths.flatMap { (path, pathItemObject) -> - listOfNotNull( - pathItemObject.delete?.toEndpointOperation(DELETE, path), - pathItemObject.get?.toEndpointOperation(GET, path), - pathItemObject.post?.toEndpointOperation(POST, path), - pathItemObject.put?.toEndpointOperation(PUT, path), - pathItemObject.patch?.toEndpointOperation(PATCH, path), - ) - } + fun getEndpointOperations(): List = + openApi.paths.flatMap { (path, pathItemObject) -> + listOfNotNull( + pathItemObject.delete?.toEndpointOperation(DELETE, path), + pathItemObject.get?.toEndpointOperation(GET, path), + pathItemObject.post?.toEndpointOperation(POST, path), + pathItemObject.put?.toEndpointOperation(PUT, path), + pathItemObject.patch?.toEndpointOperation(PATCH, path), + ) + } - private fun OperationObject.toEndpointOperation(method: EndpointOperation.Method, path: Path): EndpointOperation { + private fun OperationObject.toEndpointOperation( + method: EndpointOperation.Method, + path: Path, + ): EndpointOperation { val parameters = getParameters() return EndpointOperation( name = operationId ?: "$method ${path.value}", @@ -59,7 +66,7 @@ class OpenAPIv3Parser( url = getEndpointUrl(path), pathParams = parameters.path, queryParams = parameters.query, - requestBody = getRequestBody() + requestBody = getRequestBody(), ) } @@ -77,16 +84,17 @@ class OpenAPIv3Parser( parameterOrReferenceObject.resolve().let { parameterObject: ParameterObject -> val schemaObject = parameterObject.schema?.resolve() ?: error("Schema cannot be null") - val parameter = schemaObject.toParameter( - name = parameterObject.name, - description = parameterObject.description, - isRequired = parameterObject.required ?: false - ) + val parameter = + schemaObject.toParameter( + name = parameterObject.name, + description = parameterObject.description, + isRequired = parameterObject.required ?: false, + ) when (parameterObject.`in`) { ParameterLocation.QUERY -> parameters.copy(query = parameters.query + listOfNotNull(parameter)) ParameterLocation.PATH -> parameters.copy(path = parameters.path + listOfNotNull(parameter)) - ParameterLocation.COOKIE, ParameterLocation.HEADER -> parameters// Ignored + ParameterLocation.COOKIE, ParameterLocation.HEADER -> parameters // Ignored } } } ?: Parameters() @@ -97,7 +105,7 @@ class OpenAPIv3Parser( it.createObjectParameter( name = it.xml?.name ?: "body", description = requestBodyObject.description, - isRequired = requestBodyObject.required == true + isRequired = requestBodyObject.required == true, ) } } @@ -109,19 +117,26 @@ class OpenAPIv3Parser( } private fun SchemaObject.toParameter( - name: String, description: String?, isRequired: Boolean + name: String, + description: String?, + isRequired: Boolean, ): Parameter? { - return when (val type = determineType()) { null -> null - ParameterType.Complex.Array -> createArrayParameter( - name, description, isRequired - ) + ParameterType.Complex.Array -> + createArrayParameter( + name, + description, + isRequired, + ) ParameterType.Complex.Enum -> createEnumParameter(name, description, isRequired) - ParameterType.Complex.Object -> createObjectParameter( - name, description, isRequired - ) + ParameterType.Complex.Object -> + createObjectParameter( + name, + description, + isRequired, + ) is Primitive -> createPrimitiveParameter(name, description, isRequired, type) } @@ -132,7 +147,6 @@ class OpenAPIv3Parser( description: String?, isRequired: Boolean, ): Parameter.Complex.Enum { - val enumValueType = determineEnumValueType() val enumValues = createEnumValues(enumValueType) @@ -142,44 +156,51 @@ class OpenAPIv3Parser( isRequired = isRequired, default = enumValues.firstOrNull { it.value.toString() == default?.jsonPrimitive?.content }, values = enumValues, - valueType = enumValueType + valueType = enumValueType, ) } - private fun SchemaObject.determineEnumValueType(): Primitive = when (type) { - null -> error("Enum value type cannot be null") - Type.STRING -> Primitive.String - Type.NUMBER -> Primitive.Number - Type.INTEGER -> Primitive.Integer - Type.BOOLEAN -> Primitive.Boolean // Not sure why you want to use a boolean in an enum.... but it's possible - Type.OBJECT, Type.ARRAY -> error("Only primitive values are supported for enum, got: $type") - } + private fun SchemaObject.determineEnumValueType(): Primitive = + when (type) { + null -> error("Enum value type cannot be null") + Type.STRING -> Primitive.String + Type.NUMBER -> Primitive.Number + Type.INTEGER -> Primitive.Integer + Type.BOOLEAN -> Primitive.Boolean // Not sure why you want to use a boolean in an enum.... but it's possible + Type.OBJECT, Type.ARRAY -> error("Only primitive values are supported for enum, got: $type") + } - private fun SchemaObject.createEnumValues(parameterType: Primitive) = when (parameterType) { - Primitive.Boolean -> PrimitiveValue.Boolean::fromString - Primitive.Integer -> PrimitiveValue.Integer::fromString - Primitive.Number -> PrimitiveValue.Number::fromString - Primitive.String -> PrimitiveValue.String::fromString - }.let { constructor -> - enum?.map { constructor(it.content) } ?: error("Enum values cannot be null") - } + private fun SchemaObject.createEnumValues(parameterType: Primitive) = + when (parameterType) { + Primitive.Boolean -> PrimitiveValue.Boolean::fromString + Primitive.Integer -> PrimitiveValue.Integer::fromString + Primitive.Number -> PrimitiveValue.Number::fromString + Primitive.String -> PrimitiveValue.String::fromString + }.let { constructor -> + enum?.map { constructor(it.content) } ?: error("Enum values cannot be null") + } private fun SchemaObject.createArrayParameter( name: String, description: String?, isRequired: Boolean, - ): Parameter.Complex.Array? = createArrayItemParameterDefinition( - arrayItemSchemaObject = items?.resolve() ?: error("Array items cannot be null"), isRequired = isRequired - )?.let { - Parameter.Complex.Array( - name = name, description = description, isRequired = isRequired, itemDefinition = it - ) - } + ): Parameter.Complex.Array? = + createArrayItemParameterDefinition( + arrayItemSchemaObject = items?.resolve() ?: error("Array items cannot be null"), + isRequired = isRequired, + )?.let { + Parameter.Complex.Array( + name = name, + description = description, + isRequired = isRequired, + itemDefinition = it, + ) + } private fun createArrayItemParameterDefinition( - arrayItemSchemaObject: SchemaObject, isRequired: Boolean + arrayItemSchemaObject: SchemaObject, + isRequired: Boolean, ): Parameter? { - val arrayItemSchemaParameterType = arrayItemSchemaObject.determineType() val name = arrayItemSchemaObject.xml?.name ?: "item" val description = arrayItemSchemaObject.description @@ -189,54 +210,72 @@ class OpenAPIv3Parser( null -> null ParameterType.Complex.Array -> arrayItemSchemaObject.createArrayParameter("item", null, false) - ParameterType.Complex.Object -> Parameter.Complex.Object( - name = name, - description = description, - isRequired = isRequired, - parameters = arrayItemSchemaObject.properties?.getParameters( - arrayItemSchemaObject.required - ) ?: error("Object properties cannot be empty for parameter: $name ($description)") - ) + ParameterType.Complex.Object -> + Parameter.Complex.Object( + name = name, + description = description, + isRequired = isRequired, + parameters = + arrayItemSchemaObject.properties?.getParameters( + arrayItemSchemaObject.required, + ) ?: error("Object properties cannot be empty for parameter: $name ($description)"), + ) - is Primitive -> createPrimitiveParameter( - name = name, description = description, isRequired = isRequired, type = arrayItemSchemaParameterType - ) + is Primitive -> + createPrimitiveParameter( + name = name, + description = description, + isRequired = isRequired, + type = arrayItemSchemaParameterType, + ) - ParameterType.Complex.Enum -> arrayItemSchemaObject.createEnumParameter( - name = name, - description = description, - isRequired = isRequired, - ) + ParameterType.Complex.Enum -> + arrayItemSchemaObject.createEnumParameter( + name = name, + description = description, + isRequired = isRequired, + ) } } private fun createPrimitiveParameter( - name: String, description: String?, isRequired: Boolean, type: Primitive + name: String, + description: String?, + isRequired: Boolean, + type: Primitive, ) = Parameter.Primitive( - name = name, description = description, isRequired = isRequired, type = type - ) - - private fun SchemaObject.createObjectParameter( - name: String, description: String?, isRequired: Boolean - ): Parameter.Complex.Object = Parameter.Complex.Object( name = name, description = description, isRequired = isRequired, - parameters = properties?.getParameters(this.required) ?: emptyList() + type = type, ) + private fun SchemaObject.createObjectParameter( + name: String, + description: String?, + isRequired: Boolean, + ): Parameter.Complex.Object = + Parameter.Complex.Object( + name = name, + description = description, + isRequired = isRequired, + parameters = properties?.getParameters(this.required) ?: emptyList(), + ) + private fun SchemaObject.determineType(): ParameterType? { val isUnion = oneOf != null || anyOf != null || allOf != null return when (val schemaObjectType = this.type) { null -> { when { - isUnion -> null.also { - logger.warning("No type found and union types (oneOf, anyOf, allOf) not yet supported") - } - - else -> null.also { - logger.warning("Cannot determine type for schema: $this") - } + isUnion -> + null.also { + logger.warning("No type found and union types (oneOf, anyOf, allOf) not yet supported") + } + + else -> + null.also { + logger.warning("Cannot determine type for schema: $this") + } } } @@ -249,29 +288,31 @@ class OpenAPIv3Parser( type is Primitive && isEnum -> ParameterType.Complex.Enum type is ParameterType.Complex.Array -> ParameterType.Complex.Array type is ParameterType.Complex.Object && !isUnion -> ParameterType.Complex.Object - type is ParameterType.Complex.Object && isUnion -> null.also { - logger.warning("Type found: '$type' but union types (oneOf, anyOf, allOf) not yet supported") - } - - else -> null.also { - logger.warning("Got type: '$type' but unable to determine parameter type for schema: $this") - } + type is ParameterType.Complex.Object && isUnion -> + null.also { + logger.warning("Type found: '$type' but union types (oneOf, anyOf, allOf) not yet supported") + } + + else -> + null.also { + logger.warning("Got type: '$type' but unable to determine parameter type for schema: $this") + } } } } } - private fun Type.toParameterType(): ParameterType = when (this) { - Type.STRING -> Primitive.String - Type.NUMBER -> Primitive.Number - Type.INTEGER -> Primitive.Integer - Type.BOOLEAN -> Primitive.Boolean - Type.ARRAY -> ParameterType.Complex.Array - Type.OBJECT -> ParameterType.Complex.Object - } + private fun Type.toParameterType(): ParameterType = + when (this) { + Type.STRING -> Primitive.String + Type.NUMBER -> Primitive.Number + Type.INTEGER -> Primitive.Integer + Type.BOOLEAN -> Primitive.Boolean + Type.ARRAY -> ParameterType.Complex.Array + Type.OBJECT -> ParameterType.Complex.Object + } - private fun ReferenceObject.getReference() = - this.ref.value.split("/").getOrNull(3) ?: error("Wrong reference: ${this.ref.value}") + private fun ReferenceObject.getReference() = this.ref.value.split("/").getOrNull(3) ?: error("Wrong reference: ${this.ref.value}") private fun ParameterOrReferenceObject.resolve(): ParameterObject { fun ReferenceObject.resolveParameterObject(): ParameterObject = @@ -289,7 +330,6 @@ class OpenAPIv3Parser( } private fun SchemaOrReferenceObject.resolve(): SchemaObject { - fun ReferenceObject.resolveSchemaObject(): SchemaObject = openApi.components?.schemas?.get(getReference())?.let { when (it) { @@ -305,7 +345,6 @@ class OpenAPIv3Parser( } private fun RequestBodyOrReferenceObject.resolve(): RequestBodyObject { - fun ReferenceObject.resolveRequestBody(): RequestBodyObject = openApi.components?.requestBodies?.get(getReference())?.let { when (it) { @@ -322,5 +361,6 @@ class OpenAPIv3Parser( } data class Parameters( - val query: List = listOf(), val path: List = listOf() + val query: List = listOf(), + val path: List = listOf(), ) diff --git a/src/tools/openapi/src/commonMain/kotlin/community/flock/aigentic/tools/openapi/dsl/AgentOASTools.kt b/src/tools/openapi/src/commonMain/kotlin/community/flock/aigentic/tools/openapi/dsl/AgentOASTools.kt index 5b2f6b6..ca051b5 100644 --- a/src/tools/openapi/src/commonMain/kotlin/community/flock/aigentic/tools/openapi/dsl/AgentOASTools.kt +++ b/src/tools/openapi/src/commonMain/kotlin/community/flock/aigentic/tools/openapi/dsl/AgentOASTools.kt @@ -1,33 +1,33 @@ package community.flock.aigentic.tools.openapi.dsl -import community.flock.aigentic.tools.http.Header -import community.flock.aigentic.tools.http.RestClientExecutor -import community.flock.aigentic.tools.http.toToolDefinition import community.flock.aigentic.core.dsl.AgentConfig import community.flock.aigentic.core.dsl.AgentDSL import community.flock.aigentic.core.dsl.Config +import community.flock.aigentic.tools.http.Header +import community.flock.aigentic.tools.http.RestClientExecutor +import community.flock.aigentic.tools.http.toToolDefinition import community.flock.aigentic.tools.openapi.OpenAPIv3Parser fun AgentConfig.openApiTools( oasJson: String, restClientExecutor: RestClientExecutor = RestClientExecutor.default, - oasHeaderConfig: (OASHeaderConfig.() -> Unit)? = null + oasHeaderConfig: (OASHeaderConfig.() -> Unit)? = null, ) { val headerConfig = oasHeaderConfig?.let { OASHeaderConfig().apply(it) } val operations = OpenAPIv3Parser.parseOperations(oasJson) operations.forEach { operation -> - val toolDefinition = operation.toToolDefinition( - restClientExecutor = restClientExecutor, - headers = headerConfig?.build() ?: emptyList() - ) + val toolDefinition = + operation.toToolDefinition( + restClientExecutor = restClientExecutor, + headers = headerConfig?.build() ?: emptyList(), + ) addTool(toolDefinition) } } @AgentDSL class OASHeaderConfig : Config> { - private val headers = mutableListOf
() fun OASHeaderConfig.addHeader(header: Header) { diff --git a/src/tools/openapi/src/jvmTest/kotlin/community/flock/aigentic/tools/openapi/OASParserTest.kt b/src/tools/openapi/src/jvmTest/kotlin/community/flock/aigentic/tools/openapi/OASParserTest.kt index a8fa1f0..1726910 100644 --- a/src/tools/openapi/src/jvmTest/kotlin/community/flock/aigentic/tools/openapi/OASParserTest.kt +++ b/src/tools/openapi/src/jvmTest/kotlin/community/flock/aigentic/tools/openapi/OASParserTest.kt @@ -1,10 +1,14 @@ +@file:Suppress("ktlint:standard:max-line-length") + package community.flock.aigentic.tools.openapi import community.flock.aigentic.core.tool.Parameter import community.flock.aigentic.core.tool.ParameterType import community.flock.aigentic.core.tool.PrimitiveValue import community.flock.aigentic.tools.http.EndpointOperation -import community.flock.aigentic.tools.http.EndpointOperation.Method.* +import community.flock.aigentic.tools.http.EndpointOperation.Method.GET +import community.flock.aigentic.tools.http.EndpointOperation.Method.POST +import community.flock.aigentic.tools.http.EndpointOperation.Method.PUT import io.kotest.core.spec.style.DescribeSpec import io.kotest.datatest.withData import io.kotest.matchers.shouldBe @@ -17,276 +21,310 @@ class OASParserTest : DescribeSpec({ val operation = "/path-param-integer.json".parseOperations().first { it.name == "getItemById" } - operation shouldBe EndpointOperation( - name = "getItemById", description = "Get an item by its ID", method = GET, pathParams = listOf( - Parameter.Primitive( - name = "itemId", - description = "The ID of the item to retrieve", - isRequired = true, - type = ParameterType.Primitive.Integer - ) - ), url = "https://example.com/api/item/{itemId}", queryParams = emptyList(), requestBody = null - ) + operation shouldBe + EndpointOperation( + name = "getItemById", description = "Get an item by its ID", method = GET, + pathParams = + listOf( + Parameter.Primitive( + name = "itemId", + description = "The ID of the item to retrieve", + isRequired = true, + type = ParameterType.Primitive.Integer, + ), + ), + url = "https://example.com/api/item/{itemId}", queryParams = emptyList(), requestBody = null, + ) } it("should parse string query param") { val operation = "/query-param-string.json".parseOperations().first { it.name == "getItemsByName" } - operation shouldBe EndpointOperation( - name = "getItemsByName", - description = "Get items by name", - method = GET, - pathParams = emptyList(), - queryParams = listOf( - Parameter.Primitive( - name = "itemName", - description = "The name of the item to search for", - isRequired = false, - type = ParameterType.Primitive.String - ) - ), - url = "https://example.com/api/items", - requestBody = null - ) + operation shouldBe + EndpointOperation( + name = "getItemsByName", + description = "Get items by name", + method = GET, + pathParams = emptyList(), + queryParams = + listOf( + Parameter.Primitive( + name = "itemName", + description = "The name of the item to search for", + isRequired = false, + type = ParameterType.Primitive.String, + ), + ), + url = "https://example.com/api/items", + requestBody = null, + ) } } - describe("ArrayParameter") { it("should parse array query params") { val operation = "/query-param-string-array.json".parseOperations().first { it.name == "getItemsByIds" } - operation shouldBe EndpointOperation( - name = "getItemsByIds", - description = null, - method = GET, - pathParams = emptyList(), - url = "https://example.com/api/items", - queryParams = listOf( - Parameter.Complex.Array( - name = "itemIds", - description = "The IDs of the items to retrieve", - isRequired = false, - itemDefinition = Parameter.Primitive( - name = "item", - description = null, - isRequired = false, - type = ParameterType.Primitive.Integer + operation shouldBe + EndpointOperation( + name = "getItemsByIds", + description = null, + method = GET, + pathParams = emptyList(), + url = "https://example.com/api/items", + queryParams = + listOf( + Parameter.Complex.Array( + name = "itemIds", + description = "The IDs of the items to retrieve", + isRequired = false, + itemDefinition = + Parameter.Primitive( + name = "item", + description = null, + isRequired = false, + type = ParameterType.Primitive.Integer, + ), + ), ), - ) - ), - requestBody = null - ) + requestBody = null, + ) } describe("should parse request body which has a array property which is of type object") { val operations = "/request-body-object-array.json".parseOperations() operations.size shouldBe 1 - operations.first() shouldBe EndpointOperation( - name = "POST /users", - description = "Creates a new user with the provided information, including multiple addresses.", - method = POST, - pathParams = emptyList(), - url = "https://example.com/api/users", - queryParams = emptyList(), - requestBody = Parameter.Complex.Object( - name = "body", description = "User to be created", isRequired = true, parameters = listOf( - Parameter.Primitive( - name = "username", - description = null, - isRequired = true, - type = ParameterType.Primitive.String - ), Parameter.Complex.Array( - "addresses", - description = null, - isRequired = true, - itemDefinition = Parameter.Complex.Object( - "item", description = null, true, parameters = listOf( + operations.first() shouldBe + EndpointOperation( + name = "POST /users", + description = "Creates a new user with the provided information, including multiple addresses.", + method = POST, + pathParams = emptyList(), + url = "https://example.com/api/users", + queryParams = emptyList(), + requestBody = + Parameter.Complex.Object( + name = "body", description = "User to be created", isRequired = true, + parameters = + listOf( Parameter.Primitive( - name = "street", + name = "username", description = null, isRequired = true, - type = ParameterType.Primitive.String + type = ParameterType.Primitive.String, ), - Parameter.Primitive( - name = "city", + Parameter.Complex.Array( + "addresses", description = null, isRequired = true, - type = ParameterType.Primitive.String + itemDefinition = + Parameter.Complex.Object( + "item", description = null, true, + parameters = + listOf( + Parameter.Primitive( + name = "street", + description = null, + isRequired = true, + type = ParameterType.Primitive.String, + ), + Parameter.Primitive( + name = "city", + description = null, + isRequired = true, + type = ParameterType.Primitive.String, + ), + Parameter.Primitive( + name = "zipcode", + description = null, + isRequired = true, + type = ParameterType.Primitive.String, + ), + ), + ), ), - Parameter.Primitive( - name = "zipcode", - description = null, - isRequired = true, - type = ParameterType.Primitive.String - ), - ) - ) - ) - ) + ), + ), ) - ) - } it("should parse request body which has a array property which is of type array") { val operations = "/request-body-array-array.json".parseOperations() operations.size shouldBe 1 - operations.first() shouldBe EndpointOperation( - name = "POST /projects", - description = "Creates a new project with the provided information, including team members.", - method = POST, - pathParams = emptyList(), - url = "https://example.com/api/projects", - queryParams = emptyList(), - requestBody = Parameter.Complex.Object( - name = "body", description = "Project to be created", isRequired = true, parameters = listOf( - Parameter.Primitive( - name = "projectName", - description = null, - isRequired = true, - type = ParameterType.Primitive.String - ), Parameter.Complex.Array( - "teamMembers", - description = null, - isRequired = true, - itemDefinition = Parameter.Complex.Array( - "item", description = null, false, itemDefinition = Parameter.Complex.Object( - name = "item", description = null, isRequired = false, parameters = listOf( - Parameter.Primitive( - name = "name", - description = null, - isRequired = false, - type = ParameterType.Primitive.String - ), - Parameter.Primitive( - name = "role", - description = null, - isRequired = false, - type = ParameterType.Primitive.String - ), - ) - ) - ) - - ) - ) + operations.first() shouldBe + EndpointOperation( + name = "POST /projects", + description = "Creates a new project with the provided information, including team members.", + method = POST, + pathParams = emptyList(), + url = "https://example.com/api/projects", + queryParams = emptyList(), + requestBody = + Parameter.Complex.Object( + name = "body", description = "Project to be created", isRequired = true, + parameters = + listOf( + Parameter.Primitive( + name = "projectName", + description = null, + isRequired = true, + type = ParameterType.Primitive.String, + ), + Parameter.Complex.Array( + "teamMembers", + description = null, + isRequired = true, + itemDefinition = + Parameter.Complex.Array( + "item", description = null, false, + itemDefinition = + Parameter.Complex.Object( + name = "item", description = null, isRequired = false, + parameters = + listOf( + Parameter.Primitive( + name = "name", + description = null, + isRequired = false, + type = ParameterType.Primitive.String, + ), + Parameter.Primitive( + name = "role", + description = null, + isRequired = false, + type = ParameterType.Primitive.String, + ), + ), + ), + ), + ), + ), + ), ) - ) } it("should parse request body which has a array property which is of type primitive enum") { val operations = "/request-body-primitive-enum-array.json".parseOperations() operations.size shouldBe 1 - operations.first() shouldBe EndpointOperation( - name = "POST /tasks", - description = "Updates the statuses of tasks based on the provided information.", - method = POST, - pathParams = emptyList(), - url = "https://example.com/api/tasks", - queryParams = emptyList(), - requestBody = Parameter.Complex.Object( - name = "body", - description = "List of task statuses to be updated", - isRequired = true, - parameters = listOf( - Parameter.Primitive( - name = "taskId", - description = null, + operations.first() shouldBe + EndpointOperation( + name = "POST /tasks", + description = "Updates the statuses of tasks based on the provided information.", + method = POST, + pathParams = emptyList(), + url = "https://example.com/api/tasks", + queryParams = emptyList(), + requestBody = + Parameter.Complex.Object( + name = "body", + description = "List of task statuses to be updated", isRequired = true, - type = ParameterType.Primitive.String - ), Parameter.Complex.Array( - "statuses", - description = null, - isRequired = true, - itemDefinition = Parameter.Complex.Enum( - name = "item", description = null, isRequired = true, default = null, values = listOf( - PrimitiveValue.String("Not Started"), - PrimitiveValue.String("In Progress"), - PrimitiveValue.String("Completed"), - PrimitiveValue.String("Blocked") - ), valueType = ParameterType.Primitive.String - ) - ) - ) + parameters = + listOf( + Parameter.Primitive( + name = "taskId", + description = null, + isRequired = true, + type = ParameterType.Primitive.String, + ), + Parameter.Complex.Array( + "statuses", + description = null, + isRequired = true, + itemDefinition = + Parameter.Complex.Enum( + name = "item", description = null, isRequired = true, default = null, + values = + listOf( + PrimitiveValue.String("Not Started"), + PrimitiveValue.String("In Progress"), + PrimitiveValue.String("Completed"), + PrimitiveValue.String("Blocked"), + ), + valueType = ParameterType.Primitive.String, + ), + ), + ), + ), ) - - ) } } - describe("EnumParameter") { it("should parse string enum query params") { val operation = "/query-param-string-enum.json".parseOperations().first { it.name == "getItemsByCategory" } - - operation shouldBe EndpointOperation( - name = "getItemsByCategory", - description = null, - method = GET, - pathParams = emptyList(), - url = "https://example.com/api/items", - queryParams = listOf( - Parameter.Complex.Enum( - name = "category", - description = "The category of the items to retrieve", - isRequired = true, - default = PrimitiveValue.String("clothing"), - values = listOf( - PrimitiveValue.String("electronics"), - PrimitiveValue.String("clothing"), - PrimitiveValue.String("furniture") + operation shouldBe + EndpointOperation( + name = "getItemsByCategory", + description = null, + method = GET, + pathParams = emptyList(), + url = "https://example.com/api/items", + queryParams = + listOf( + Parameter.Complex.Enum( + name = "category", + description = "The category of the items to retrieve", + isRequired = true, + default = PrimitiveValue.String("clothing"), + values = + listOf( + PrimitiveValue.String("electronics"), + PrimitiveValue.String("clothing"), + PrimitiveValue.String("furniture"), + ), + valueType = ParameterType.Primitive.String, + ), ), - valueType = ParameterType.Primitive.String - ) - ), - requestBody = null - ) + requestBody = null, + ) } it("should parse request body which has an integer enum property") { val operations = "/request-body-integer-enum.json".parseOperations() operations.size shouldBe 1 - operations.first() shouldBe EndpointOperation( - name = "POST /submitStatus", - description = "Submits a status code represented as an integer.", - method = POST, - pathParams = emptyList(), - url = "https://example.com/api/submitStatus", - queryParams = emptyList(), - requestBody = Parameter.Complex.Object( - name = "body", - description = "Payload containing a status code", - isRequired = true, - parameters = listOf( - Parameter.Complex.Enum( - name = "statusCode", - description = "Status code representing the state of an item. 0: New, 1: In Progress, 2: Completed, 3: Archived.", + operations.first() shouldBe + EndpointOperation( + name = "POST /submitStatus", + description = "Submits a status code represented as an integer.", + method = POST, + pathParams = emptyList(), + url = "https://example.com/api/submitStatus", + queryParams = emptyList(), + requestBody = + Parameter.Complex.Object( + name = "body", + description = "Payload containing a status code", isRequired = true, - default = null, - values = listOf( - PrimitiveValue.Integer(0), - PrimitiveValue.Integer(1), - PrimitiveValue.Integer(2), - PrimitiveValue.Integer(3), - ), - valueType = ParameterType.Primitive.Integer - - ) - ) + parameters = + listOf( + Parameter.Complex.Enum( + name = "statusCode", + description = "Status code representing the state of an item. 0: New, 1: In Progress, 2: Completed, 3: Archived.", + isRequired = true, + default = null, + values = + listOf( + PrimitiveValue.Integer(0), + PrimitiveValue.Integer(1), + PrimitiveValue.Integer(2), + PrimitiveValue.Integer(3), + ), + valueType = ParameterType.Primitive.Integer, + ), + ), + ), ) - ) } - } describe("ObjectParameter") { @@ -295,41 +333,46 @@ class OASParserTest : DescribeSpec({ val operation = "/request-body-object.json".parseOperations().first { it.name == "createItem" } - operation shouldBe EndpointOperation( - name = "createItem", - description = "Create an item", - method = PUT, - pathParams = emptyList(), - url = "https://example.com/api/item", - queryParams = emptyList(), - requestBody = Parameter.Complex.Object( - name = "body", description = "Item to be created", parameters = listOf( - Parameter.Primitive( - name = "name", description = null, isRequired = true, type = ParameterType.Primitive.String - ), - Parameter.Primitive( - name = "description", - description = null, - isRequired = false, - type = ParameterType.Primitive.String - ), - Parameter.Primitive( - name = "price", description = null, isRequired = true, type = ParameterType.Primitive.Number + operation shouldBe + EndpointOperation( + name = "createItem", + description = "Create an item", + method = PUT, + pathParams = emptyList(), + url = "https://example.com/api/item", + queryParams = emptyList(), + requestBody = + Parameter.Complex.Object( + name = "body", description = "Item to be created", + parameters = + listOf( + Parameter.Primitive( + name = "name", description = null, isRequired = true, type = ParameterType.Primitive.String, + ), + Parameter.Primitive( + name = "description", + description = null, + isRequired = false, + type = ParameterType.Primitive.String, + ), + Parameter.Primitive( + name = "price", description = null, isRequired = true, type = ParameterType.Primitive.Number, + ), + ), + isRequired = true, ), - ), isRequired = true ) - ) } - } context("should parse all operations") { withData( - "/petstore.json" to 19, "/github.json" to 930, "/hackernews.json" to 7 + "/petstore.json" to 19, + "/github.json" to 930, + "/hackernews.json" to 7, ) { (specFile, expectedNumberOfOperations) -> specFile.parseOperations().size shouldBe expectedNumberOfOperations } - } })