├── .gitignore ├── .idea ├── .name ├── gradle.xml ├── kotlinc.xml ├── misc.xml └── vcs.xml ├── LICENSE ├── README.md ├── ask-api ├── build.gradle.kts ├── gradle.properties └── src │ ├── main │ └── kotlin │ │ └── com │ │ └── fluxtah │ │ └── ask │ │ └── api │ │ ├── AssistantRunManager.kt │ │ ├── AssistantRunner.kt │ │ ├── InputHandler.kt │ │ ├── PollRunStatus.kt │ │ ├── RecoverRunDetails.kt │ │ ├── RunDetails.kt │ │ ├── RunManagerStatus.kt │ │ ├── RunResult.kt │ │ ├── ansi │ │ └── green.kt │ │ ├── assistants │ │ ├── AssistantInstallRecord.kt │ │ ├── AssistantInstallRepository.kt │ │ └── AssistantRegistry.kt │ │ ├── audio │ │ ├── AudioPlayer.kt │ │ ├── AudioRecorder.kt │ │ └── TextToSpeechPlayer.kt │ │ ├── clients │ │ ├── HttpClient.kt │ │ └── openai │ │ │ ├── assistants │ │ │ ├── AssistantsApi.kt │ │ │ └── model │ │ │ │ ├── Assistant.kt │ │ │ │ ├── AssistantDeletionStatus.kt │ │ │ │ ├── AssistantMessageContent.kt │ │ │ │ ├── AssistantMessageList.kt │ │ │ │ ├── AssistantMessageText.kt │ │ │ │ ├── AssistantRun.kt │ │ │ │ ├── AssistantRunError.kt │ │ │ │ ├── AssistantRunList.kt │ │ │ │ ├── AssistantRunStep.kt │ │ │ │ ├── AssistantRunStepDetails.kt │ │ │ │ ├── AssistantRunStepList.kt │ │ │ │ ├── AssistantThread.kt │ │ │ │ ├── AssistantThreadDeletionStatus.kt │ │ │ │ ├── AssistantTool.kt │ │ │ │ ├── CreateAssistantRequest.kt │ │ │ │ ├── Message.kt │ │ │ │ ├── RunRequest.kt │ │ │ │ ├── RunStatus.kt │ │ │ │ ├── SubmitToolOutputsRequest.kt │ │ │ │ ├── ToolOutput.kt │ │ │ │ ├── ToolResources.kt │ │ │ │ ├── ToolResourcesCodeInterpreter.kt │ │ │ │ └── ToolResourcesFileSearch.kt │ │ │ └── audio │ │ │ ├── AudioApi.kt │ │ │ └── model │ │ │ ├── CreateTranscriptionRequest.kt │ │ │ └── CreateTranscriptionResponse.kt │ │ ├── commanding │ │ ├── CommandFactory.kt │ │ └── commands │ │ │ ├── Clear.kt │ │ │ ├── ClearModel.kt │ │ │ ├── Command.kt │ │ │ ├── DeleteThread.kt │ │ │ ├── EnableTalkCommand.kt │ │ │ ├── Exit.kt │ │ │ ├── GetAssistant.kt │ │ │ ├── GetThread.kt │ │ │ ├── Help.kt │ │ │ ├── InstallAssistant.kt │ │ │ ├── JSON.kt │ │ │ ├── ListAssistants.kt │ │ │ ├── ListMessages.kt │ │ │ ├── ListRunSteps.kt │ │ │ ├── ListRuns.kt │ │ │ ├── ListThreads.kt │ │ │ ├── MaxCompletionTokens.kt │ │ │ ├── MaxPromptTokens.kt │ │ │ ├── PlayTts.kt │ │ │ ├── RecordVoice.kt │ │ │ ├── RecoverRun.kt │ │ │ ├── ReinstallAssistant.kt │ │ │ ├── SetLogLevel.kt │ │ │ ├── SetModel.kt │ │ │ ├── SetOpenAiApiKey.kt │ │ │ ├── ShellExec.kt │ │ │ ├── ShowHttpLog.kt │ │ │ ├── SkipTts.kt │ │ │ ├── SwitchThread.kt │ │ │ ├── ThreadNew.kt │ │ │ ├── ThreadRecall.kt │ │ │ ├── ThreadRename.kt │ │ │ ├── ToShortDateTimeString.kt │ │ │ ├── TruncateLastMessages.kt │ │ │ ├── UnInstallAssistant.kt │ │ │ ├── VoiceAutoSendCommand.kt │ │ │ ├── WhichAssistant.kt │ │ │ ├── WhichModel.kt │ │ │ └── WhichThread.kt │ │ ├── di │ │ ├── AskApiModule.kt │ │ ├── CommandFactoryModule.kt │ │ └── CommandsModule.kt │ │ ├── kotlin │ │ └── KotlinFileRepository.kt │ │ ├── markdown │ │ ├── AnsiMarkdownRenderer.kt │ │ ├── MarkdownParser.kt │ │ └── Token.kt │ │ ├── plugins │ │ └── AskPluginLoader.kt │ │ ├── printers │ │ ├── AskConsoleResponsePrinter.kt │ │ └── AskResponsePrinter.kt │ │ ├── repository │ │ └── ThreadRepository.kt │ │ ├── store │ │ ├── PropertyStore.kt │ │ └── user │ │ │ └── UserProperties.kt │ │ ├── tools │ │ └── fn │ │ │ ├── FunctionInvoker.kt │ │ │ └── FunctionToolGenerator.kt │ │ └── version │ │ └── VersionUtils.kt │ └── test │ └── kotlin │ └── com │ └── fluxtah │ └── ask │ └── api │ ├── KotlinFileRepositoryTest.kt │ ├── audio │ └── TextToSpeechPlayerTest.kt │ ├── markdown │ └── MarkdownParserTest.kt │ └── tools │ └── fn │ └── FunctionInvokerTest.kt ├── build.gradle.kts ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── scripts └── ask.sh ├── settings.gradle.kts ├── src ├── main │ └── kotlin │ │ ├── Main.kt │ │ └── com │ │ └── fluxtah │ │ └── ask │ │ ├── Version.kt │ │ └── app │ │ ├── AskCommandCompleter.kt │ │ ├── ConsoleApplication.kt │ │ ├── ConsoleOutputRenderer.kt │ │ ├── WorkingSpinner.kt │ │ └── di │ │ └── AppModule.kt └── test │ └── kotlin │ └── com │ └── fluxtah │ └── ask │ └── VersionUtilsTest.kt └── version.properties /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | build/ 3 | !gradle/wrapper/gradle-wrapper.jar 4 | !**/src/main/**/build/ 5 | !**/src/test/**/build/ 6 | 7 | ### IntelliJ IDEA ### 8 | .idea 9 | .idea/modules.xml 10 | .idea/jarRepositories.xml 11 | .idea/compiler.xml 12 | .idea/libraries/ 13 | *.iws 14 | *.iml 15 | *.ipr 16 | out/ 17 | !**/src/main/**/out/ 18 | !**/src/test/**/out/ 19 | 20 | ### Eclipse ### 21 | .apt_generated 22 | .classpath 23 | .factorypath 24 | .project 25 | .settings 26 | .springBeans 27 | .sts4-cache 28 | bin/ 29 | !**/src/main/**/bin/ 30 | !**/src/test/**/bin/ 31 | 32 | ### NetBeans ### 33 | /nbproject/private/ 34 | /nbbuild/ 35 | /dist/ 36 | /nbdist/ 37 | /.nb-gradle/ 38 | 39 | ### VS Code ### 40 | .vscode/ 41 | 42 | ### Mac OS ### 43 | .DS_Store 44 | 45 | user.properties -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | ask -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 34 | 35 | -------------------------------------------------------------------------------- /.idea/kotlinc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Ian Warwick 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /ask-api/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile 2 | 3 | val ktor_version: String by project 4 | 5 | plugins { 6 | kotlin("jvm") version "1.9.10" 7 | id("org.jetbrains.kotlin.plugin.serialization") version "1.8.21" 8 | } 9 | 10 | group = "com.fluxtah.ask" 11 | version = "0.5.0" 12 | 13 | repositories { 14 | mavenCentral() 15 | maven { url = uri("https://repo.gradle.org/gradle/libs-releases") } 16 | maven { url = uri("https://jitpack.io") } 17 | } 18 | 19 | dependencies { 20 | api("com.github.fluxtah:ask-plugin-sdk:0.7.2") 21 | 22 | // SLF4J API 23 | implementation("org.slf4j:slf4j-api:1.7.32") 24 | // Logback (which includes the SLF4J binding) 25 | implementation("ch.qos.logback:logback-classic:1.4.12") 26 | 27 | implementation("io.ktor:ktor-client-core:$ktor_version") 28 | implementation("io.ktor:ktor-client-cio:$ktor_version") 29 | implementation("io.ktor:ktor-client-okhttp:$ktor_version") 30 | implementation("io.ktor:ktor-client-content-negotiation:$ktor_version") 31 | implementation("io.ktor:ktor-serialization-kotlinx-json-jvm:$ktor_version") 32 | implementation("io.ktor:ktor-client-logging-jvm:$ktor_version") 33 | 34 | implementation("org.jetbrains.kotlin:kotlin-reflect") 35 | implementation("org.gradle:gradle-tooling-api:8.4") 36 | implementation("org.jetbrains.kotlin:kotlin-compiler-embeddable:1.7.0") 37 | 38 | implementation("org.xerial:sqlite-jdbc:3.41.2.2") 39 | implementation("org.jetbrains.exposed:exposed-core:0.37.3") 40 | implementation("org.jetbrains.exposed:exposed-dao:0.37.3") 41 | implementation("org.jetbrains.exposed:exposed-jdbc:0.37.3") 42 | implementation("org.jetbrains.exposed:exposed-java-time:0.37.3") 43 | 44 | implementation("io.insert-koin:koin-core:3.5.6") 45 | 46 | testImplementation(kotlin("test")) 47 | 48 | // MockK for mocking 49 | testImplementation("io.mockk:mockk:1.12.0") 50 | 51 | // Coroutine Testing 52 | testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.5.2") 53 | 54 | // JUnit 55 | testImplementation("junit:junit:4.13.2") 56 | } 57 | 58 | 59 | tasks.withType { 60 | kotlinOptions.jvmTarget = "17" 61 | } 62 | 63 | tasks.test { 64 | useJUnitPlatform() 65 | } 66 | -------------------------------------------------------------------------------- /ask-api/gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.code.style=official 2 | 3 | ktor_version=2.3.1 4 | -------------------------------------------------------------------------------- /ask-api/src/main/kotlin/com/fluxtah/ask/api/AssistantRunManager.kt: -------------------------------------------------------------------------------- 1 | package com.fluxtah.ask.api 2 | 3 | import com.fluxtah.ask.api.clients.openai.assistants.model.AssistantRunStepDetails 4 | import com.fluxtah.ask.api.clients.openai.assistants.model.Message 5 | import com.fluxtah.ask.api.clients.openai.assistants.model.RunStatus 6 | import com.fluxtah.ask.api.store.user.UserProperties 7 | import kotlinx.coroutines.runBlocking 8 | 9 | class AssistantRunManager( 10 | private val assistantRunner: AssistantRunner, 11 | private val userProperties: UserProperties 12 | ) { 13 | var onStatusChanged: ((RunManagerStatus) -> Unit)? = null 14 | 15 | fun runAssistant(input: String) { 16 | val currentThreadId = userProperties.getThreadId() 17 | 18 | if (currentThreadId.isEmpty()) { 19 | onStatusChanged?.invoke( 20 | RunManagerStatus.Error( 21 | "You need to create a thread first. Use /thread-new", 22 | RunManagerStatus.ErrorType.ThreadNotSet 23 | ) 24 | ) 25 | return 26 | } 27 | 28 | if (!input.startsWith("@") && userProperties.getAssistantId().isEmpty()) { 29 | onStatusChanged?.invoke( 30 | RunManagerStatus.Error( 31 | "You need to address an assistant with @assistant-id , to see available assistants use /assistant-list", 32 | RunManagerStatus.ErrorType.TargetAssistantNotSet 33 | ) 34 | ) 35 | return 36 | } 37 | 38 | val assistantId = getNamedAssistantIdOrLast(input) 39 | 40 | onStatusChanged?.invoke(RunManagerStatus.BeforeBeginRun) 41 | 42 | runBlocking { 43 | val result = assistantRunner.run( 44 | details = RunDetails( 45 | assistantId = assistantId, 46 | threadId = currentThreadId, 47 | model = userProperties.getModel(), 48 | prompt = input, 49 | maxPromptTokens = userProperties.getMaxPromptTokensOrNull(), 50 | maxCompletionTokens = userProperties.getMaxCompletionTokensOrNull(), 51 | truncationStrategy = userProperties.getTruncationStrategyOrNull() 52 | ), 53 | onRunStatusChanged = ::onRunStatusChanged, 54 | onMessageCreation = ::onMessageCreation, 55 | onExecuteTool = { toolCallDetails -> 56 | onExecuteTool(toolCallDetails) 57 | } 58 | ) 59 | 60 | handleRunResult(result, assistantId) 61 | } 62 | } 63 | 64 | private fun handleRunResult(result: RunResult, assistantId: String) { 65 | when (result) { 66 | is RunResult.Complete -> { 67 | userProperties.setRunId(result.runId) 68 | userProperties.setAssistantId(assistantId) 69 | userProperties.save() 70 | 71 | onStatusChanged?.invoke(RunManagerStatus.Response(result.responseText)) 72 | } 73 | 74 | is RunResult.Error -> { 75 | onStatusChanged?.invoke(RunManagerStatus.Error(result.message, RunManagerStatus.ErrorType.Unknown)) 76 | } 77 | } 78 | } 79 | 80 | fun recoverRun() { 81 | val runId = userProperties.getRunId() 82 | if (runId.isEmpty()) { 83 | onStatusChanged?.invoke( 84 | RunManagerStatus.Error( 85 | "No run to recover", 86 | RunManagerStatus.ErrorType.NoRunToRecover 87 | ) 88 | ) 89 | return 90 | } 91 | 92 | val threadId: String = userProperties.getThreadId() 93 | if (threadId.isEmpty()) { 94 | onStatusChanged?.invoke( 95 | RunManagerStatus.Error( 96 | "No thread to recover in", 97 | RunManagerStatus.ErrorType.NoThreadToRecoverIn 98 | ) 99 | ) 100 | return 101 | } 102 | 103 | val result = runBlocking { 104 | assistantRunner.recoverRun( 105 | details = RecoverRunDetails( 106 | threadId = threadId, 107 | runId = runId, 108 | ), 109 | onRunStatusChanged = ::onRunStatusChanged, 110 | onMessageCreation = ::onMessageCreation, 111 | onExecuteTool = { toolCallDetails -> 112 | onExecuteTool(toolCallDetails) 113 | } 114 | ) 115 | } 116 | handleRunResult(result, userProperties.getAssistantId()) 117 | } 118 | 119 | private fun onExecuteTool(toolCallDetails: AssistantRunStepDetails.ToolCalls.ToolCallDetails.FunctionToolCallDetails) { 120 | onStatusChanged?.invoke(RunManagerStatus.ToolCall(toolCallDetails)) 121 | } 122 | 123 | private fun onMessageCreation(message: Message) { 124 | onStatusChanged?.invoke(RunManagerStatus.MessageCreated(message)) 125 | } 126 | 127 | private fun onRunStatusChanged(status: RunStatus) { 128 | onStatusChanged?.invoke(RunManagerStatus.RunStatusChanged(status)) 129 | } 130 | 131 | private fun getNamedAssistantIdOrLast(input: String) = if (input.startsWith("@")) { 132 | val parts = input.split(" ") 133 | val assistantId = parts[0].substring(1) 134 | parts.drop(1).joinToString(" ") 135 | assistantId 136 | } else { 137 | userProperties.getAssistantId() 138 | } 139 | } -------------------------------------------------------------------------------- /ask-api/src/main/kotlin/com/fluxtah/ask/api/InputHandler.kt: -------------------------------------------------------------------------------- 1 | package com.fluxtah.ask.api 2 | 3 | import com.fluxtah.ask.api.printers.AskResponsePrinter 4 | import com.fluxtah.ask.api.commanding.CommandFactory 5 | import com.fluxtah.askpluginsdk.logging.AskLogger 6 | import com.fluxtah.askpluginsdk.logging.LogLevel 7 | 8 | class InputHandler( 9 | private val commandFactory: CommandFactory, 10 | private val responsePrinter: AskResponsePrinter, 11 | private val logger: AskLogger, 12 | private val assistantRunManager: AssistantRunManager, 13 | ) { 14 | suspend fun handleInput(input: String) { 15 | if (input.isEmpty()) { 16 | return 17 | } 18 | 19 | try { 20 | when { 21 | input.startsWith("/") -> { 22 | commandFactory.executeCommand(input) 23 | } 24 | 25 | input.startsWith(":") -> { // Alias for /exec 26 | commandFactory.executeCommand("/exec ${input.drop(1)}") 27 | } 28 | 29 | else -> { 30 | assistantRunManager.runAssistant(input) 31 | } 32 | } 33 | } catch (e: Exception) { 34 | responsePrinter.begin().println("Error: ${e.message}, run with /log-level ERROR for more info").end() 35 | logger.log(LogLevel.ERROR, "Error: ${e.stackTraceToString()}") 36 | } 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /ask-api/src/main/kotlin/com/fluxtah/ask/api/PollRunStatus.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024 Ian Warwick 3 | * Released under the MIT license 4 | * https://opensource.org/licenses/MIT 5 | */ 6 | 7 | package com.fluxtah.ask.api 8 | 9 | import com.fluxtah.ask.api.clients.openai.assistants.AssistantsApi 10 | import com.fluxtah.ask.api.clients.openai.assistants.model.AssistantRun 11 | import com.fluxtah.ask.api.clients.openai.assistants.model.RunStatus 12 | import kotlinx.coroutines.delay 13 | 14 | /** 15 | * Poll until the run status is no longer QUEUED or IN_PROGRESS 16 | */ 17 | suspend fun pollRunStatus( 18 | assistantsApi: AssistantsApi, 19 | currentThreadId: String, 20 | initialRunStatus: AssistantRun, 21 | onStatusChanged: (RunStatus) -> Unit 22 | ): AssistantRun { 23 | var run = initialRunStatus 24 | while (true) { 25 | run = assistantsApi.runs.getRun(currentThreadId, run.id) 26 | when (run.status) { 27 | RunStatus.QUEUED, RunStatus.IN_PROGRESS, RunStatus.CANCELLING -> { 28 | // These statuses imply waiting is needed. You can log or handle these differently if needed. 29 | onStatusChanged(run.status) 30 | delay(1000) 31 | } 32 | 33 | RunStatus.REQUIRES_ACTION, RunStatus.CANCELLED, RunStatus.FAILED, RunStatus.COMPLETED, RunStatus.EXPIRED, RunStatus.INCOMPLETE -> { 34 | onStatusChanged(run.status) 35 | return run 36 | } 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /ask-api/src/main/kotlin/com/fluxtah/ask/api/RecoverRunDetails.kt: -------------------------------------------------------------------------------- 1 | package com.fluxtah.ask.api 2 | 3 | data class RecoverRunDetails( 4 | val threadId: String, 5 | val runId: String, 6 | ) -------------------------------------------------------------------------------- /ask-api/src/main/kotlin/com/fluxtah/ask/api/RunDetails.kt: -------------------------------------------------------------------------------- 1 | package com.fluxtah.ask.api 2 | 3 | import com.fluxtah.ask.api.clients.openai.assistants.model.TruncationStrategy 4 | 5 | data class RunDetails( 6 | val assistantId: String, 7 | val model: String? = null, 8 | val threadId: String, 9 | val prompt: String, 10 | val maxPromptTokens: Int? = null, 11 | val maxCompletionTokens: Int? = null, 12 | val truncationStrategy: TruncationStrategy? = null 13 | ) -------------------------------------------------------------------------------- /ask-api/src/main/kotlin/com/fluxtah/ask/api/RunManagerStatus.kt: -------------------------------------------------------------------------------- 1 | package com.fluxtah.ask.api 2 | 3 | import com.fluxtah.ask.api.clients.openai.assistants.model.AssistantRunStepDetails 4 | import com.fluxtah.ask.api.clients.openai.assistants.model.Message 5 | import com.fluxtah.ask.api.clients.openai.assistants.model.RunStatus 6 | 7 | sealed class RunManagerStatus { 8 | data object BeforeBeginRun : RunManagerStatus() 9 | data class MessageCreated(val message: Message) : RunManagerStatus() 10 | data class ToolCall(val details: AssistantRunStepDetails.ToolCalls.ToolCallDetails.FunctionToolCallDetails) : 11 | RunManagerStatus() 12 | 13 | data class Response(val response: String) : RunManagerStatus() 14 | data class Error(val message: String, val type: ErrorType) : RunManagerStatus() 15 | data class RunStatusChanged(val runStatus: RunStatus) : RunManagerStatus() 16 | 17 | enum class ErrorType { 18 | Unknown, 19 | ThreadNotSet, 20 | TargetAssistantNotSet, 21 | NoRunToRecover, 22 | NoThreadToRecoverIn 23 | } 24 | } -------------------------------------------------------------------------------- /ask-api/src/main/kotlin/com/fluxtah/ask/api/RunResult.kt: -------------------------------------------------------------------------------- 1 | package com.fluxtah.ask.api 2 | 3 | sealed class RunResult { 4 | data class Complete( 5 | val runId: String, 6 | val responseText: String 7 | ) : RunResult() 8 | 9 | data class Error(val message: String) : RunResult() 10 | } -------------------------------------------------------------------------------- /ask-api/src/main/kotlin/com/fluxtah/ask/api/ansi/green.kt: -------------------------------------------------------------------------------- 1 | package com.fluxtah.ask.api.ansi 2 | 3 | fun green(text: String = "") : String { 4 | return "\u001b[32m$text\u001B[0m" 5 | } 6 | 7 | fun red(text: String = "") : String { 8 | return "\u001b[31m$text\u001B[0m" 9 | } 10 | 11 | fun blue(text: String = "") : String { 12 | return "\u001b[34m$text\u001B[0m" 13 | } 14 | 15 | fun cyan(text: String = "") : String { 16 | return "\u001b[36m$text\u001B[0m" 17 | } 18 | -------------------------------------------------------------------------------- /ask-api/src/main/kotlin/com/fluxtah/ask/api/assistants/AssistantInstallRecord.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024 Ian Warwick 3 | * Released under the MIT license 4 | * https://opensource.org/licenses/MIT 5 | */ 6 | 7 | package com.fluxtah.ask.api.assistants 8 | 9 | import kotlinx.serialization.Serializable 10 | 11 | @Serializable 12 | data class AssistantInstallRecord( 13 | val id: String, 14 | val version: String, 15 | val installId: String 16 | ) -------------------------------------------------------------------------------- /ask-api/src/main/kotlin/com/fluxtah/ask/api/assistants/AssistantInstallRepository.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024 Ian Warwick 3 | * Released under the MIT license 4 | * https://opensource.org/licenses/MIT 5 | */ 6 | 7 | package com.fluxtah.ask.api.assistants 8 | 9 | import com.fluxtah.ask.api.clients.openai.assistants.AssistantsApi 10 | import com.fluxtah.ask.api.clients.openai.assistants.model.CreateAssistantRequest 11 | import com.fluxtah.ask.api.clients.openai.assistants.model.ModifyAssistantRequest 12 | import com.fluxtah.ask.api.tools.fn.FunctionToolGenerator 13 | import com.fluxtah.askpluginsdk.AssistantDefinition 14 | import com.fluxtah.askpluginsdk.io.getUserConfigDirectory 15 | import kotlinx.serialization.encodeToString 16 | import kotlinx.serialization.json.Json 17 | import java.io.File 18 | 19 | class AssistantInstallRepository(private val assistantsApi: AssistantsApi) { 20 | suspend fun install(assistantDef: AssistantDefinition): AssistantInstallRecord { 21 | val assistantInstallRecord = getAssistantInstallRecord(assistantDef.id) 22 | 23 | val newRecord = if (assistantInstallRecord == null) { 24 | createAssistantFromDef(assistantDef) 25 | } else { 26 | modifyAssistantFromDef(assistantDef, assistantInstallRecord) 27 | } 28 | 29 | saveOrReplaceAssistantInstallRecord(newRecord) 30 | 31 | return newRecord 32 | } 33 | 34 | suspend fun uninstall(assistantRecord: AssistantInstallRecord): Boolean { 35 | val status = assistantsApi.assistants.deleteAssistant(assistantRecord.installId) 36 | 37 | if (status.deleted) { 38 | removeAssistantInstallRecord(assistantRecord) 39 | return true 40 | } 41 | 42 | return false 43 | } 44 | 45 | 46 | private suspend fun modifyAssistantFromDef( 47 | assistantDef: AssistantDefinition, 48 | assistantInstallRecord: AssistantInstallRecord 49 | ): AssistantInstallRecord { 50 | val modifyAssistantRequest = ModifyAssistantRequest( 51 | model = assistantDef.model, 52 | name = assistantDef.name, 53 | description = assistantDef.description, 54 | instructions = assistantDef.instructions, 55 | tools = FunctionToolGenerator().generateToolsForInstance(assistantDef.functions), 56 | metadata = mapOf( 57 | "version" to assistantDef.version, 58 | "assistantId" to assistantDef.id 59 | ), 60 | temperature = assistantDef.temperature 61 | ) 62 | 63 | val assistant = 64 | assistantsApi.assistants.modifyAssistant(assistantInstallRecord.installId, modifyAssistantRequest) 65 | 66 | return AssistantInstallRecord( 67 | id = assistantDef.id, 68 | version = assistantDef.version, 69 | installId = assistant.id 70 | ) 71 | } 72 | 73 | private suspend fun createAssistantFromDef(assistantDef: AssistantDefinition): AssistantInstallRecord { 74 | val createAssistantRequest = CreateAssistantRequest( 75 | model = assistantDef.model, 76 | temperature = assistantDef.temperature, 77 | name = assistantDef.name, 78 | description = assistantDef.description, 79 | instructions = assistantDef.instructions, 80 | tools = FunctionToolGenerator().generateToolsForInstance(assistantDef.functions), 81 | metadata = mapOf( 82 | "version" to assistantDef.version, 83 | "assistantId" to assistantDef.id 84 | ) 85 | ) 86 | 87 | val assistant = assistantsApi.assistants.createAssistant(createAssistantRequest) 88 | 89 | return AssistantInstallRecord( 90 | id = assistantDef.id, 91 | version = assistantDef.version, 92 | installId = assistant.id 93 | ) 94 | } 95 | 96 | fun getAssistantInstallRecord(assistantId: String): AssistantInstallRecord? { 97 | return getAssistantInstallRecords().find { it.id == assistantId } 98 | } 99 | 100 | fun getAssistantInstallRecords(): List { 101 | // Load from file JSONL 102 | val records = mutableListOf() 103 | val file = File(getUserConfigDirectory(), "assistants.jsonl") 104 | if (!file.exists()) { 105 | return emptyList() 106 | } 107 | file.forEachLine { line -> 108 | val record = Json.decodeFromString(line) 109 | records.add(record) 110 | } 111 | 112 | return records 113 | } 114 | 115 | private fun saveOrReplaceAssistantInstallRecord(record: AssistantInstallRecord) { 116 | val records = getAssistantInstallRecords().toMutableList() 117 | val existingRecord = records.find { it.id == record.id } 118 | if (existingRecord != null) { 119 | records.remove(existingRecord) 120 | } 121 | records.add(record) 122 | 123 | val file = File(getUserConfigDirectory(), "assistants.jsonl") 124 | file.writeText(records.joinToString("\n") { Json.encodeToString(it) }) 125 | } 126 | 127 | private fun removeAssistantInstallRecord(record: AssistantInstallRecord) { 128 | val records = getAssistantInstallRecords().toMutableList() 129 | val existingRecord = records.find { it.id == record.id } 130 | if (existingRecord != null) { 131 | records.remove(existingRecord) 132 | } 133 | 134 | val file = File(getUserConfigDirectory(), "assistants.jsonl") 135 | file.writeText(records.joinToString("\n") { Json.encodeToString(it) }) 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /ask-api/src/main/kotlin/com/fluxtah/ask/api/assistants/AssistantRegistry.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024 Ian Warwick 3 | * Released under the MIT license 4 | * https://opensource.org/licenses/MIT 5 | */ 6 | 7 | package com.fluxtah.ask.api.assistants 8 | 9 | import com.fluxtah.askpluginsdk.AssistantDefinition 10 | 11 | class AssistantRegistry { 12 | private val assistants = mutableListOf() 13 | 14 | fun register(assistant: AssistantDefinition) { 15 | assistants.add(assistant) 16 | } 17 | 18 | fun getAssistantById(id: String): AssistantDefinition? { 19 | return assistants.find { it.id == id } 20 | } 21 | 22 | fun getAssistants(): List { 23 | return assistants 24 | } 25 | } -------------------------------------------------------------------------------- /ask-api/src/main/kotlin/com/fluxtah/ask/api/audio/AudioPlayer.kt: -------------------------------------------------------------------------------- 1 | import kotlinx.coroutines.Dispatchers 2 | import kotlinx.coroutines.withContext 3 | import java.io.ByteArrayInputStream 4 | import javax.sound.sampled.AudioInputStream 5 | import javax.sound.sampled.AudioSystem 6 | import javax.sound.sampled.DataLine 7 | import javax.sound.sampled.SourceDataLine 8 | 9 | class AudioPlayer { 10 | 11 | private var line: SourceDataLine? = null 12 | 13 | suspend fun play(audioData: ByteArray, onComplete: () -> Unit = {}) = withContext(Dispatchers.IO) { 14 | if (line != null) { 15 | stop() 16 | } 17 | try { 18 | val bais = ByteArrayInputStream(audioData) 19 | val audioStream: AudioInputStream = AudioSystem.getAudioInputStream(bais) 20 | val format = audioStream.format 21 | val info = DataLine.Info(SourceDataLine::class.java, format) 22 | 23 | if (!AudioSystem.isLineSupported(info)) { 24 | println("Line not supported") 25 | return@withContext 26 | } 27 | 28 | line = AudioSystem.getLine(info) as SourceDataLine 29 | line?.open(format) 30 | line?.start() 31 | 32 | // Gradually ramp up the volume at the start 33 | val buffer = ByteArray(4096) 34 | var bytesRead = audioStream.read(buffer, 0, buffer.size) 35 | while (bytesRead != -1) { 36 | line?.write(buffer, 0, bytesRead) 37 | if (line == null) { 38 | break 39 | } 40 | bytesRead = audioStream.read(buffer, 0, buffer.size) 41 | } 42 | 43 | line?.drain() 44 | line?.close() 45 | line = null 46 | onComplete() 47 | } catch (ex: Exception) { 48 | ex.printStackTrace() 49 | } 50 | } 51 | 52 | fun stop() { 53 | line?.stop() 54 | line?.close() 55 | line = null 56 | } 57 | 58 | fun isPlaying(): Boolean { 59 | return line != null 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /ask-api/src/main/kotlin/com/fluxtah/ask/api/audio/AudioRecorder.kt: -------------------------------------------------------------------------------- 1 | package com.fluxtah.ask.api.audio 2 | 3 | import com.fluxtah.askpluginsdk.io.getUserConfigDirectory 4 | import kotlinx.coroutines.Dispatchers 5 | import kotlinx.coroutines.withContext 6 | import java.io.File 7 | import java.io.IOException 8 | import javax.sound.sampled.AudioFileFormat 9 | import javax.sound.sampled.AudioFormat 10 | import javax.sound.sampled.AudioInputStream 11 | import javax.sound.sampled.AudioSystem 12 | import javax.sound.sampled.DataLine 13 | import javax.sound.sampled.LineUnavailableException 14 | import javax.sound.sampled.TargetDataLine 15 | 16 | class AudioRecorder { 17 | private var line: TargetDataLine? = null 18 | private val fileType = AudioFileFormat.Type.WAVE 19 | private val wavFile = File(getUserConfigDirectory(),"input.wav") 20 | 21 | fun getAudioFile(): File = wavFile 22 | suspend fun start() = withContext(Dispatchers.IO) { 23 | try { 24 | val format = getAudioFormat() 25 | val info = DataLine.Info(TargetDataLine::class.java, format) 26 | 27 | if (!AudioSystem.isLineSupported(info)) { 28 | println("Line not supported") 29 | return@withContext 30 | } 31 | 32 | line = AudioSystem.getLine(info) as TargetDataLine 33 | line!!.open(format) 34 | line!!.start() 35 | 36 | val ais = AudioInputStream(line) 37 | AudioSystem.write(ais, fileType, wavFile) 38 | } catch (ex: LineUnavailableException) { 39 | ex.printStackTrace() 40 | } catch (ex: IOException) { 41 | ex.printStackTrace() 42 | } 43 | } 44 | 45 | fun stop() { 46 | line?.stop() 47 | line?.close() 48 | line = null 49 | } 50 | 51 | fun isRecording(): Boolean { 52 | return line != null 53 | } 54 | 55 | private fun getAudioFormat(): AudioFormat { 56 | val sampleRate = 16000f 57 | val sampleSizeInBits = 16 58 | val channels = 1 59 | val signed = true 60 | val bigEndian = true 61 | return AudioFormat(sampleRate, sampleSizeInBits, channels, signed, bigEndian) 62 | } 63 | } -------------------------------------------------------------------------------- /ask-api/src/main/kotlin/com/fluxtah/ask/api/audio/TextToSpeechPlayer.kt: -------------------------------------------------------------------------------- 1 | package com.fluxtah.ask.api.audio 2 | 3 | import AudioPlayer 4 | import com.fluxtah.ask.api.clients.openai.audio.AudioApi 5 | import com.fluxtah.ask.api.clients.openai.audio.CreateSpeechRequest 6 | import com.fluxtah.ask.api.clients.openai.audio.ResponseFormat 7 | import com.fluxtah.ask.api.clients.openai.audio.SpeechModel 8 | import com.fluxtah.ask.api.clients.openai.audio.SpeechVoice 9 | import com.fluxtah.ask.api.markdown.MarkdownParser 10 | import com.fluxtah.ask.api.markdown.Token 11 | import kotlinx.coroutines.CoroutineScope 12 | import kotlinx.coroutines.launch 13 | 14 | const val ADVICE_PLAY_OR_SKIP_CODE = "Type SLASH P to play the code block or SLASH S to skip it." 15 | 16 | class TextToSpeechPlayer( 17 | private val audioApi: AudioApi, 18 | private val audioPlayer: AudioPlayer, 19 | private val coroutineScope: CoroutineScope, 20 | ) { 21 | // Backing property, mutable and private 22 | private val _ttsSegments = mutableListOf() 23 | 24 | // Public property, immutable view 25 | val ttsSegments: List 26 | get() = _ttsSegments 27 | 28 | var enabled: Boolean = true 29 | 30 | fun queue(text: String) { 31 | if (!enabled) { 32 | return 33 | } 34 | val markdownParser = MarkdownParser(text) 35 | val tokens = markdownParser.parse() 36 | val builder = StringBuilder() 37 | val newSegments = mutableListOf() 38 | 39 | tokens.forEach { token -> 40 | when (token) { 41 | is Token.CodeBlock -> { 42 | appendBlock(builder, newSegments, ADVICE_PLAY_OR_SKIP_CODE) 43 | if (builder.isNotEmpty()) { 44 | newSegments.add(TtsSegment.Text(builder.toString())) 45 | builder.clear() 46 | } 47 | newSegments.add(TtsSegment.CodeBlock(token.language, token.content)) 48 | } 49 | 50 | is Token.Bold -> { 51 | appendBlock(builder, newSegments, token.content) 52 | } 53 | 54 | is Token.Code -> { 55 | appendBlock(builder, newSegments, token.content) 56 | } 57 | 58 | is Token.Text -> { 59 | appendBlock(builder, newSegments, token.content) 60 | } 61 | } 62 | } 63 | 64 | if (builder.isNotEmpty()) { 65 | newSegments.add(TtsSegment.Text(builder.toString())) 66 | builder.clear() 67 | } 68 | 69 | // Autoplay the first text segment 70 | if (newSegments.first() is TtsSegment.Text) { 71 | newSegments[0] = (newSegments.first() as TtsSegment.Text).copy(autoPlay = true) 72 | } 73 | 74 | // Autoplay text blocks after code blocks 75 | newSegments.forEachIndexed { index, ttsSegment -> 76 | if (ttsSegment is TtsSegment.CodeBlock) { 77 | val nextIndex = index + 1 78 | if (nextIndex < newSegments.size && newSegments[nextIndex] is TtsSegment.Text) { 79 | newSegments[nextIndex] = (newSegments[nextIndex] as TtsSegment.Text).copy(autoPlay = true) 80 | } 81 | } 82 | } 83 | 84 | _ttsSegments.addAll(newSegments) 85 | } 86 | 87 | private fun appendBlock(builder: StringBuilder, segments: MutableList, content: String) { 88 | if ((builder.count() + content.count()) > 4096) { 89 | val breakSymbols = listOf('.', '!', '?') 90 | val nearestSymbol = builder.indexOfLast { it in breakSymbols } 91 | if (nearestSymbol != -1) { 92 | segments.add(TtsSegment.Text(builder.substring(0, nearestSymbol + 1))) 93 | val remaining = builder.substring(nearestSymbol + 1) 94 | builder.clear() 95 | builder.append(remaining) 96 | } else { 97 | segments.add(TtsSegment.Text(builder.toString())) 98 | builder.clear() 99 | } 100 | } else { 101 | builder.append(content) 102 | } 103 | } 104 | 105 | fun skipNext() { 106 | ttsSegments.firstOrNull()?.let { 107 | _ttsSegments.removeAt(0) 108 | } 109 | } 110 | 111 | fun playNext() { 112 | if (!enabled) { 113 | return 114 | } 115 | 116 | // Don't play if already playing 117 | if(audioPlayer.isPlaying()) { 118 | return 119 | } 120 | 121 | ttsSegments.firstOrNull()?.let { segment -> 122 | when (segment) { 123 | is TtsSegment.Text -> playText(segment.content, onComplete = { 124 | if (ttsSegments.firstOrNull()?.autoPlay == true) { 125 | playNext() 126 | } 127 | }) 128 | 129 | is TtsSegment.CodeBlock -> playText(segment.content, onComplete = { 130 | if (ttsSegments.firstOrNull()?.autoPlay == true) { 131 | playNext() 132 | } 133 | }) 134 | } 135 | if(ttsSegments.isNotEmpty()) { 136 | _ttsSegments.removeAt(0) 137 | } 138 | } 139 | } 140 | 141 | private fun playText(text: String, onComplete: () -> Unit = {}) { 142 | coroutineScope.launch { 143 | val audio = audioApi.createSpeech( 144 | CreateSpeechRequest( 145 | model = SpeechModel.TTS_1, 146 | voice = SpeechVoice.ECHO, 147 | responseFormat = ResponseFormat.WAV, 148 | input = text, 149 | speed = 1.0 150 | ) 151 | ) 152 | audioPlayer.play(audio, onComplete) 153 | } 154 | } 155 | 156 | fun stop() { 157 | audioPlayer.stop() 158 | } 159 | 160 | fun clear() { 161 | _ttsSegments.clear() 162 | } 163 | 164 | sealed class TtsSegment { 165 | abstract val autoPlay: Boolean 166 | 167 | data class Text(val content: String, override val autoPlay: Boolean = false) : TtsSegment() 168 | data class CodeBlock(val language: String?, val content: String, override val autoPlay: Boolean = false) : 169 | TtsSegment() 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /ask-api/src/main/kotlin/com/fluxtah/ask/api/clients/HttpClient.kt: -------------------------------------------------------------------------------- 1 | package com.fluxtah.ask.api.clients 2 | 3 | import com.fluxtah.ask.api.clients.openai.assistants.addHttpLog 4 | import io.ktor.client.HttpClient 5 | import io.ktor.client.engine.okhttp.OkHttp 6 | import io.ktor.client.plugins.HttpRequestRetry 7 | import io.ktor.client.plugins.contentnegotiation.ContentNegotiation 8 | import io.ktor.client.plugins.logging.LogLevel 9 | import io.ktor.client.plugins.logging.Logger 10 | import io.ktor.client.plugins.logging.Logging 11 | import io.ktor.serialization.kotlinx.json.json 12 | import kotlinx.serialization.json.Json 13 | import java.util.concurrent.TimeUnit 14 | 15 | val httpClient = HttpClient(OkHttp) { 16 | engine { 17 | clientCacheSize = 0 18 | config { 19 | retryOnConnectionFailure(true) 20 | connectTimeout(60, TimeUnit.SECONDS) 21 | readTimeout(60, TimeUnit.SECONDS) 22 | writeTimeout(10, TimeUnit.SECONDS) 23 | } 24 | } 25 | install(HttpRequestRetry) { 26 | retryIf(5) { _, response -> 27 | response.status.value.let { it == 429 || it in 500..599 } 28 | } 29 | exponentialDelay() 30 | } 31 | install(ContentNegotiation) { 32 | json(json = Json { 33 | ignoreUnknownKeys = true 34 | encodeDefaults = false 35 | }) 36 | } 37 | install(Logging) { 38 | logger = object : Logger { 39 | override fun log(message: String) { 40 | addHttpLog(message) 41 | } 42 | } 43 | level = LogLevel.ALL 44 | } 45 | } -------------------------------------------------------------------------------- /ask-api/src/main/kotlin/com/fluxtah/ask/api/clients/openai/assistants/model/Assistant.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024 Ian Warwick 3 | * Released under the MIT license 4 | * https://opensource.org/licenses/MIT 5 | */ 6 | 7 | package com.fluxtah.ask.api.clients.openai.assistants.model 8 | 9 | import kotlinx.serialization.SerialName 10 | import kotlinx.serialization.Serializable 11 | 12 | @Serializable 13 | data class Assistant( 14 | @SerialName("id") val id: String, 15 | @SerialName("object") val objectName: String, 16 | @SerialName("created_at") val createdAt: Long, 17 | @SerialName("name") val name: String? = null, 18 | @SerialName("description") val description: String? = null, 19 | @SerialName("model") val model: String? = null, 20 | @SerialName("instructions") val instructions: String? = null, 21 | @SerialName("tools") val tools: List = emptyList(), 22 | @SerialName("tool_resources") val toolResource: ToolResources? = null, 23 | @SerialName("metadata") val metadata: Map = emptyMap(), 24 | @SerialName("temperature") val temperature: Float? = null, 25 | @SerialName("top_p") val topP: Float? = null, 26 | // @SerialName("response_format") val responseFormat: ResponseFormat? = null 27 | ) 28 | -------------------------------------------------------------------------------- /ask-api/src/main/kotlin/com/fluxtah/ask/api/clients/openai/assistants/model/AssistantDeletionStatus.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024 Ian Warwick 3 | * Released under the MIT license 4 | * https://opensource.org/licenses/MIT 5 | */ 6 | 7 | package com.fluxtah.ask.api.clients.openai.assistants.model 8 | 9 | import kotlinx.serialization.SerialName 10 | import kotlinx.serialization.Serializable 11 | 12 | @Serializable 13 | data class AssistantDeletionStatus( 14 | val id: String, 15 | @SerialName("object") val objectName: String, 16 | val deleted: Boolean = false 17 | ) -------------------------------------------------------------------------------- /ask-api/src/main/kotlin/com/fluxtah/ask/api/clients/openai/assistants/model/AssistantMessageContent.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024 Ian Warwick 3 | * Released under the MIT license 4 | * https://opensource.org/licenses/MIT 5 | */ 6 | 7 | package com.fluxtah.ask.api.clients.openai.assistants.model 8 | 9 | import kotlinx.serialization.SerialName 10 | import kotlinx.serialization.Serializable 11 | 12 | @Serializable 13 | data class AssistantMessageContent( 14 | @SerialName("type") val type: String, 15 | @SerialName("text") val text: AssistantMessageText 16 | ) -------------------------------------------------------------------------------- /ask-api/src/main/kotlin/com/fluxtah/ask/api/clients/openai/assistants/model/AssistantMessageList.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024 Ian Warwick 3 | * Released under the MIT license 4 | * https://opensource.org/licenses/MIT 5 | */ 6 | 7 | package com.fluxtah.ask.api.clients.openai.assistants.model 8 | 9 | import kotlinx.serialization.SerialName 10 | import kotlinx.serialization.Serializable 11 | 12 | @Serializable 13 | data class AssistantMessageList( 14 | @SerialName("object") val objectName: String, 15 | @SerialName("data") val data: List = emptyList(), 16 | @SerialName("first_id") val firstId: String? = null, 17 | @SerialName("last_id") val lastId: String? = null, 18 | @SerialName("has_more") val hasMore: Boolean = false 19 | ) -------------------------------------------------------------------------------- /ask-api/src/main/kotlin/com/fluxtah/ask/api/clients/openai/assistants/model/AssistantMessageText.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024 Ian Warwick 3 | * Released under the MIT license 4 | * https://opensource.org/licenses/MIT 5 | */ 6 | 7 | package com.fluxtah.ask.api.clients.openai.assistants.model 8 | 9 | import kotlinx.serialization.SerialName 10 | import kotlinx.serialization.Serializable 11 | 12 | @Serializable 13 | data class AssistantMessageText( 14 | @SerialName("value") val value: String, 15 | @SerialName("annotations") val annotations: List 16 | ) -------------------------------------------------------------------------------- /ask-api/src/main/kotlin/com/fluxtah/ask/api/clients/openai/assistants/model/AssistantRun.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024 Ian Warwick 3 | * Released under the MIT license 4 | * https://opensource.org/licenses/MIT 5 | */ 6 | 7 | package com.fluxtah.ask.api.clients.openai.assistants.model 8 | 9 | import kotlinx.serialization.SerialName 10 | import kotlinx.serialization.Serializable 11 | 12 | @Serializable 13 | data class AssistantRun( 14 | @SerialName("id") val id: String, 15 | @SerialName("object") val objectName: String, 16 | @SerialName("created_at") val createdAt: Long, 17 | @SerialName("assistant_id") val assistantId: String, 18 | @SerialName("thread_id") val threadId: String, 19 | @SerialName("status") val status: RunStatus, 20 | @SerialName("started_at") val startedAt: Long? = null, 21 | @SerialName("expires_at") val expiresAt: Long? = null, 22 | @SerialName("cancelled_at") val cancelledAt: Long? = null, 23 | @SerialName("failed_at") val failedAt: Long? = null, 24 | @SerialName("completed_at") val completedAt: Long? = null, 25 | @SerialName("last_error") val lastError: AssistantRunError? = null, 26 | @SerialName("model") val model: String, 27 | @SerialName("instructions") val instructions: String? = null, 28 | @SerialName("tools") val tools: List = emptyList(), 29 | @SerialName("file_ids") val fileIds: List = emptyList(), 30 | @SerialName("metadata") val metadata: Map = emptyMap(), 31 | @SerialName("usage") val usage: AssistantRunUsage? = null 32 | ) 33 | 34 | @Serializable 35 | data class AssistantRunUsage( 36 | @SerialName("prompt_tokens") val promptTokens: Long, 37 | @SerialName("completion_tokens") val completionTokens: Long, 38 | @SerialName("total_tokens") val totalTokens: Long, 39 | ) 40 | 41 | -------------------------------------------------------------------------------- /ask-api/src/main/kotlin/com/fluxtah/ask/api/clients/openai/assistants/model/AssistantRunError.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024 Ian Warwick 3 | * Released under the MIT license 4 | * https://opensource.org/licenses/MIT 5 | */ 6 | 7 | package com.fluxtah.ask.api.clients.openai.assistants.model 8 | 9 | import kotlinx.serialization.SerialName 10 | import kotlinx.serialization.Serializable 11 | 12 | @Serializable 13 | data class AssistantRunError( 14 | @SerialName("code") val code: String, 15 | @SerialName("message") val message: String 16 | ) -------------------------------------------------------------------------------- /ask-api/src/main/kotlin/com/fluxtah/ask/api/clients/openai/assistants/model/AssistantRunList.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024 Ian Warwick 3 | * Released under the MIT license 4 | * https://opensource.org/licenses/MIT 5 | */ 6 | 7 | package com.fluxtah.ask.api.clients.openai.assistants.model 8 | 9 | import kotlinx.serialization.SerialName 10 | import kotlinx.serialization.Serializable 11 | 12 | @Serializable 13 | data class AssistantRunList( 14 | @SerialName("object") val objectName: String, 15 | @SerialName("data") val data: List = emptyList(), 16 | @SerialName("first_id") val firstId: String? = null, 17 | @SerialName("last_id") val lastId: String? = null, 18 | @SerialName("has_more") val hasMore: Boolean = false 19 | ) -------------------------------------------------------------------------------- /ask-api/src/main/kotlin/com/fluxtah/ask/api/clients/openai/assistants/model/AssistantRunStep.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024 Ian Warwick 3 | * Released under the MIT license 4 | * https://opensource.org/licenses/MIT 5 | */ 6 | 7 | package com.fluxtah.ask.api.clients.openai.assistants.model 8 | 9 | import kotlinx.serialization.SerialName 10 | import kotlinx.serialization.Serializable 11 | 12 | @Serializable 13 | data class AssistantRunStep( 14 | @SerialName("id") val id: String, 15 | @SerialName("object") val objectName: String, 16 | @SerialName("created_at") val createdAt: Long, 17 | @SerialName("run_id") val runId: String, 18 | @SerialName("assistant_id") val assistantId: String, 19 | @SerialName("thread_id") val threadId: String, 20 | @SerialName("type") val type: String, 21 | @SerialName("status") val status: String, 22 | @SerialName("cancelled_at") val cancelledAt: Long? = null, 23 | @SerialName("completed_at") val completedAt: Long? = null, 24 | @SerialName("expired_at") val expiredAt: Long? = null, 25 | @SerialName("failed_at") val failedAt: Long? = null, 26 | @SerialName("last_error") val lastError: String? = null, 27 | @SerialName("step_details") val stepDetails: AssistantRunStepDetails 28 | ) -------------------------------------------------------------------------------- /ask-api/src/main/kotlin/com/fluxtah/ask/api/clients/openai/assistants/model/AssistantRunStepDetails.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024 Ian Warwick 3 | * Released under the MIT license 4 | * https://opensource.org/licenses/MIT 5 | */ 6 | 7 | package com.fluxtah.ask.api.clients.openai.assistants.model 8 | 9 | import kotlinx.serialization.SerialName 10 | import kotlinx.serialization.Serializable 11 | 12 | @Serializable 13 | sealed class AssistantRunStepDetails { 14 | @Serializable 15 | @SerialName("message_creation") 16 | data class MessageCreation( 17 | @SerialName("message_creation") val messageCreation: MessageCreationDetails 18 | ) : AssistantRunStepDetails() { 19 | @Serializable 20 | data class MessageCreationDetails( 21 | @SerialName("message_id") val messageId: String 22 | ) 23 | } 24 | 25 | @Serializable 26 | @SerialName("tool_calls") 27 | data class ToolCalls( 28 | @SerialName("tool_calls") val toolCalls: List 29 | ) : AssistantRunStepDetails() { 30 | 31 | @Serializable 32 | sealed class ToolCallDetails { 33 | @Serializable 34 | @SerialName("function") 35 | data class FunctionToolCallDetails( 36 | @SerialName("id") val id: String, 37 | @SerialName("function") val function: FunctionSpec 38 | ) : ToolCallDetails() { 39 | @Serializable 40 | data class FunctionSpec( 41 | @SerialName("name") val name: String, 42 | @SerialName("arguments") val arguments: String 43 | ) 44 | } 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /ask-api/src/main/kotlin/com/fluxtah/ask/api/clients/openai/assistants/model/AssistantRunStepList.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024 Ian Warwick 3 | * Released under the MIT license 4 | * https://opensource.org/licenses/MIT 5 | */ 6 | 7 | package com.fluxtah.ask.api.clients.openai.assistants.model 8 | 9 | import kotlinx.serialization.SerialName 10 | import kotlinx.serialization.Serializable 11 | 12 | @Serializable 13 | data class AssistantRunStepList( 14 | @SerialName("object") val objectName: String, 15 | @SerialName("data") val data: List, 16 | @SerialName("first_id") val firstId: String, 17 | @SerialName("last_id") val lastId: String, 18 | @SerialName("has_more") val hasMore: Boolean 19 | ) -------------------------------------------------------------------------------- /ask-api/src/main/kotlin/com/fluxtah/ask/api/clients/openai/assistants/model/AssistantThread.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024 Ian Warwick 3 | * Released under the MIT license 4 | * https://opensource.org/licenses/MIT 5 | */ 6 | 7 | package com.fluxtah.ask.api.clients.openai.assistants.model 8 | 9 | import kotlinx.serialization.SerialName 10 | import kotlinx.serialization.Serializable 11 | 12 | @Serializable 13 | data class AssistantThread( 14 | @SerialName("id") val id: String, 15 | @SerialName("object") val objectName: String, 16 | @SerialName("created_at") val createdAt: Long, 17 | @SerialName("metadata") val metadata: Map 18 | ) -------------------------------------------------------------------------------- /ask-api/src/main/kotlin/com/fluxtah/ask/api/clients/openai/assistants/model/AssistantThreadDeletionStatus.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024 Ian Warwick 3 | * Released under the MIT license 4 | * https://opensource.org/licenses/MIT 5 | */ 6 | 7 | package com.fluxtah.ask.api.clients.openai.assistants.model 8 | 9 | import kotlinx.serialization.SerialName 10 | import kotlinx.serialization.Serializable 11 | 12 | @Serializable 13 | data class AssistantThreadDeletionStatus( 14 | @SerialName("id") val id: String, 15 | @SerialName("object") val objectName: String, 16 | @SerialName("deleted") val deleted: Boolean 17 | ) -------------------------------------------------------------------------------- /ask-api/src/main/kotlin/com/fluxtah/ask/api/clients/openai/assistants/model/AssistantTool.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024 Ian Warwick 3 | * Released under the MIT license 4 | * https://opensource.org/licenses/MIT 5 | */ 6 | 7 | package com.fluxtah.ask.api.clients.openai.assistants.model 8 | 9 | import kotlinx.serialization.EncodeDefault 10 | import kotlinx.serialization.ExperimentalSerializationApi 11 | import kotlinx.serialization.SerialName 12 | import kotlinx.serialization.Serializable 13 | 14 | @Serializable 15 | sealed class AssistantTool { 16 | 17 | @Serializable 18 | @SerialName("function") 19 | data class FunctionTool( 20 | @SerialName("function") val function: FunctionSpec 21 | ) : AssistantTool() { 22 | @Serializable 23 | data class FunctionSpec( 24 | val name: String, 25 | @EncodeDefault(EncodeDefault.Mode.NEVER) 26 | val description: String = "", 27 | val parameters: ParametersSpec = ParametersSpec() 28 | ) 29 | 30 | @Serializable 31 | data class ParametersSpec( 32 | val type: String? = null, 33 | val properties: Map = emptyMap(), 34 | val required: List = emptyList() 35 | ) 36 | 37 | @Serializable 38 | data class PropertySpec @OptIn(ExperimentalSerializationApi::class) constructor( 39 | val type: String, 40 | @EncodeDefault(EncodeDefault.Mode.NEVER) 41 | val description: String = "", 42 | @EncodeDefault(EncodeDefault.Mode.NEVER) 43 | val properties: Map = emptyMap() 44 | ) 45 | } 46 | 47 | @Serializable 48 | @SerialName("code_interpreter") 49 | data object CodeInterpreter : AssistantTool() 50 | 51 | @Serializable 52 | @SerialName("retrieval") 53 | data object Retrieval : AssistantTool() 54 | } 55 | -------------------------------------------------------------------------------- /ask-api/src/main/kotlin/com/fluxtah/ask/api/clients/openai/assistants/model/CreateAssistantRequest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024 Ian Warwick 3 | * Released under the MIT license 4 | * https://opensource.org/licenses/MIT 5 | */ 6 | 7 | package com.fluxtah.ask.api.clients.openai.assistants.model 8 | 9 | import kotlinx.serialization.SerialName 10 | import kotlinx.serialization.Serializable 11 | 12 | @Serializable 13 | data class CreateAssistantRequest( 14 | @SerialName("model") val model: String? = null, 15 | @SerialName("name") val name: String? = null, 16 | @SerialName("description") val description: String? = null, 17 | @SerialName("instructions") val instructions: String? = null, 18 | @SerialName("tools") val tools: List = emptyList(), 19 | @SerialName("tool_resources") val toolResource: ToolResources? = null, 20 | @SerialName("metadata") val metadata: Map = emptyMap(), 21 | @SerialName("temperature") val temperature: Float? = null, 22 | @SerialName("top_p") val topP: Float? = null, 23 | 24 | /** 25 | * Specifies the format that the model must output. Compatible with GPT-4o, GPT-4 Turbo, and 26 | * all GPT-3.5 Turbo models since gpt-3.5-turbo-1106. 27 | * 28 | * Setting to { "type": "json_object" } enables JSON mode, which guarantees the message the 29 | * model generates is valid JSON. 30 | * 31 | * Important: when using JSON mode, you must also instruct the model to produce JSON 32 | * yourself via a system or user message. Without this, the model may generate an unending 33 | * stream of whitespace until the generation reaches the token limit, resulting in a 34 | * long-running and seemingly "stuck" request. Also note that the message content may be 35 | * partially cut off if finish_reason="length", which indicates the generation 36 | * exceeded max_tokens or the conversation exceeded the max context length. 37 | */ 38 | @SerialName("response_format") val responseFormat: ResponseFormat? = null 39 | ) 40 | 41 | @Serializable 42 | data class ResponseFormat( 43 | @SerialName("type") val type: String, 44 | ) { 45 | companion object { 46 | val JSON = ResponseFormat("json_object") 47 | } 48 | } 49 | 50 | @Serializable 51 | data class ModifyAssistantRequest( 52 | @SerialName("model") val model: String? = null, 53 | @SerialName("name") val name: String? = null, 54 | @SerialName("description") val description: String? = null, 55 | @SerialName("instructions") val instructions: String? = null, 56 | @SerialName("tools") val tools: List = emptyList(), 57 | @SerialName("tool_resources") val toolResource: ToolResources? = null, 58 | @SerialName("metadata") val metadata: Map = emptyMap(), 59 | @SerialName("temperature") val temperature: Float? = null 60 | ) -------------------------------------------------------------------------------- /ask-api/src/main/kotlin/com/fluxtah/ask/api/clients/openai/assistants/model/Message.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024 Ian Warwick 3 | * Released under the MIT license 4 | * https://opensource.org/licenses/MIT 5 | */ 6 | 7 | package com.fluxtah.ask.api.clients.openai.assistants.model 8 | 9 | import kotlinx.serialization.SerialName 10 | import kotlinx.serialization.Serializable 11 | 12 | @Serializable 13 | data class Message( 14 | @SerialName("id") val id: String, 15 | @SerialName("object") val objectName: String, 16 | @SerialName("created_at") val createdAt: Long, 17 | @SerialName("thread_id") val threadId: String, 18 | @SerialName("role") val role: String, 19 | @SerialName("content") val content: List, 20 | @SerialName("file_ids") val fileIds: List = emptyList(), 21 | @SerialName("assistant_id") val assistantId: String? = null, 22 | @SerialName("run_id") val runId: String? = null, 23 | @SerialName("metadata") val metadata: Map = emptyMap() 24 | ) -------------------------------------------------------------------------------- /ask-api/src/main/kotlin/com/fluxtah/ask/api/clients/openai/assistants/model/RunRequest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024 Ian Warwick 3 | * Released under the MIT license 4 | * https://opensource.org/licenses/MIT 5 | */ 6 | 7 | package com.fluxtah.ask.api.clients.openai.assistants.model 8 | 9 | import kotlinx.serialization.SerialName 10 | import kotlinx.serialization.Serializable 11 | 12 | @Serializable 13 | data class RunRequest( 14 | /** 15 | * The ID of the assistant to use to execute this run. 16 | */ 17 | @SerialName("assistant_id") val assistantId: String, 18 | 19 | /** 20 | * The ID of the Model to be used to execute this run. 21 | * If a value is provided here, it will override the model associated with the assistant. 22 | * If not, the model associated with the assistant will be used. 23 | */ 24 | @SerialName("model") val model: String? = null, 25 | 26 | /** 27 | * Overrides the instructions of the assistant. This is useful for modifying the behavior on a per-run basis. 28 | */ 29 | @SerialName("instructions") val instructions: String? = null, 30 | 31 | /** 32 | * Appends additional instructions at the end of the instructions for the run. 33 | * This is useful for modifying the behavior on a per-run basis without overriding other instructions. 34 | */ 35 | @SerialName("additional_instructions") val additionalInstructions: String? = null, 36 | 37 | /** 38 | * Adds additional messages to the thread before creating the run. 39 | */ 40 | @SerialName("additional_messages") val additionalMessages: List? = null, 41 | 42 | /** 43 | * Override the tools the assistant can use for this run. 44 | * This is useful for modifying the behavior on a per-run basis. 45 | */ 46 | @SerialName("tools") val tools: List? = null, 47 | 48 | /** 49 | * Set of 16 key-value pairs that can be attached to an object. 50 | * This can be useful for storing additional information about the object in a structured format. 51 | * Keys can be a maximum of 64 characters long and values can be a maximum of 512 characters long. 52 | */ 53 | @SerialName("metadata") val metadata: Map? = null, 54 | 55 | /** 56 | * What sampling temperature to use, between 0 and 2. Higher values like 0.8 will make the output more random, 57 | * while lower values like 0.2 will make it more focused and deterministic. 58 | */ 59 | @SerialName("temperature") val temperature: Float? = null, 60 | 61 | /** 62 | * An alternative to sampling with temperature, called nucleus sampling, where the model considers 63 | * the results of the tokens with top_p probability mass. So 0.1 means only the tokens comprising 64 | * the top 10% probability mass are considered. 65 | * 66 | * We generally recommend altering this or temperature but not both. 67 | */ 68 | @SerialName("top_p") val topP: Float? = null, 69 | 70 | /** 71 | * The maximum number of prompt tokens that may be used over the course of the run. 72 | * The run will make a best effort to use only the number of prompt tokens specified, 73 | * across multiple turns of the run. If the run exceeds the number of prompt tokens specified, 74 | * the run will end with status incomplete. See incomplete_details for more info. 75 | */ 76 | @SerialName("max_prompt_tokens") val maxPromptTokens: Int? = null, 77 | 78 | /** 79 | * The maximum number of completion tokens that may be used over the course of the run. 80 | * The run will make a best effort to use only the number of completion tokens specified, 81 | * across multiple turns of the run. If the run exceeds the number of completion tokens specified, 82 | * the run will end with status incomplete. See incomplete_details for more info. 83 | */ 84 | @SerialName("max_completion_tokens") val maxCompletionTokens: Int? = null, 85 | 86 | /** 87 | * Controls for how a thread will be truncated prior to the run. 88 | * Use this to control the initial context window of the run. 89 | */ 90 | @SerialName("truncation_strategy") val truncationStrategy: TruncationStrategy? = null, 91 | 92 | /** 93 | * Specifies the format that the model must output. Compatible with GPT-4o, GPT-4 Turbo, and 94 | * all GPT-3.5 Turbo models since gpt-3.5-turbo-1106. 95 | * 96 | * Setting to { "type": "json_object" } enables JSON mode, which guarantees the message the 97 | * model generates is valid JSON. 98 | * 99 | * Important: when using JSON mode, you must also instruct the model to produce JSON 100 | * yourself via a system or user message. Without this, the model may generate an unending 101 | * stream of whitespace until the generation reaches the token limit, resulting in a 102 | * long-running and seemingly "stuck" request. Also note that the message content may be 103 | * partially cut off if finish_reason="length", which indicates the generation 104 | * exceeded max_tokens or the conversation exceeded the max context length. 105 | */ 106 | @SerialName("response_format") val responseFormat: ResponseFormat? = null 107 | ) 108 | 109 | /** 110 | * The truncation strategy to use for the thread. The default is auto. 111 | * If set to last_messages, the thread will be truncated to the n most recent messages in the thread. 112 | * When set to auto, messages in the middle of the thread will be dropped to fit 113 | * the context length of the model, max_prompt_tokens. 114 | */ 115 | @Serializable 116 | sealed class TruncationStrategy { 117 | @Serializable 118 | @SerialName("auto") 119 | data object Auto : TruncationStrategy() 120 | 121 | @Serializable 122 | @SerialName("last_messages") 123 | data class LastMessages( 124 | /** 125 | * The number of most recent messages from the thread when constructing the context for the run. 126 | */ 127 | @SerialName("last_messages") val numMessages: Int? = null, 128 | ) : TruncationStrategy() 129 | } -------------------------------------------------------------------------------- /ask-api/src/main/kotlin/com/fluxtah/ask/api/clients/openai/assistants/model/RunStatus.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024 Ian Warwick 3 | * Released under the MIT license 4 | * https://opensource.org/licenses/MIT 5 | */ 6 | 7 | package com.fluxtah.ask.api.clients.openai.assistants.model 8 | 9 | import kotlinx.serialization.SerialName 10 | import kotlinx.serialization.Serializable 11 | 12 | @Serializable 13 | enum class RunStatus { 14 | @SerialName("queued") 15 | QUEUED, 16 | 17 | @SerialName("in_progress") 18 | IN_PROGRESS, 19 | 20 | @SerialName("requires_action") 21 | REQUIRES_ACTION, 22 | 23 | @SerialName("cancelling") 24 | CANCELLING, 25 | 26 | @SerialName("cancelled") 27 | CANCELLED, 28 | 29 | @SerialName("failed") 30 | FAILED, 31 | 32 | @SerialName("completed") 33 | COMPLETED, 34 | 35 | @SerialName("expired") 36 | EXPIRED, 37 | 38 | @SerialName("incomplete") 39 | INCOMPLETE 40 | } -------------------------------------------------------------------------------- /ask-api/src/main/kotlin/com/fluxtah/ask/api/clients/openai/assistants/model/SubmitToolOutputsRequest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024 Ian Warwick 3 | * Released under the MIT license 4 | * https://opensource.org/licenses/MIT 5 | */ 6 | 7 | package com.fluxtah.ask.api.clients.openai.assistants.model 8 | 9 | import kotlinx.serialization.SerialName 10 | import kotlinx.serialization.Serializable 11 | 12 | @Serializable 13 | data class SubmitToolOutputsRequest( 14 | @SerialName("tool_outputs") val toolOutputs: List 15 | ) -------------------------------------------------------------------------------- /ask-api/src/main/kotlin/com/fluxtah/ask/api/clients/openai/assistants/model/ToolOutput.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024 Ian Warwick 3 | * Released under the MIT license 4 | * https://opensource.org/licenses/MIT 5 | */ 6 | 7 | package com.fluxtah.ask.api.clients.openai.assistants.model 8 | 9 | import kotlinx.serialization.SerialName 10 | import kotlinx.serialization.Serializable 11 | 12 | @Serializable 13 | data class ToolOutput( 14 | @SerialName("tool_call_id") val toolCallId: String, 15 | @SerialName("output") val output: String? = null 16 | ) -------------------------------------------------------------------------------- /ask-api/src/main/kotlin/com/fluxtah/ask/api/clients/openai/assistants/model/ToolResources.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024 Ian Warwick 3 | * Released under the MIT license 4 | * https://opensource.org/licenses/MIT 5 | */ 6 | 7 | package com.fluxtah.ask.api.clients.openai.assistants.model 8 | 9 | import kotlinx.serialization.SerialName 10 | import kotlinx.serialization.Serializable 11 | 12 | @Serializable 13 | data class ToolResources( 14 | @SerialName("code_interpreter") val codeInterpreter: ToolResourcesCodeInterpreter? = null, 15 | @SerialName("file_search") val fileSearch: ToolResourcesFileSearch? = null, 16 | ) -------------------------------------------------------------------------------- /ask-api/src/main/kotlin/com/fluxtah/ask/api/clients/openai/assistants/model/ToolResourcesCodeInterpreter.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024 Ian Warwick 3 | * Released under the MIT license 4 | * https://opensource.org/licenses/MIT 5 | */ 6 | 7 | package com.fluxtah.ask.api.clients.openai.assistants.model 8 | 9 | import kotlinx.serialization.SerialName 10 | import kotlinx.serialization.Serializable 11 | 12 | @Serializable 13 | data class ToolResourcesCodeInterpreter( 14 | @SerialName("file_ids") val fileIds: List = emptyList(), 15 | ) -------------------------------------------------------------------------------- /ask-api/src/main/kotlin/com/fluxtah/ask/api/clients/openai/assistants/model/ToolResourcesFileSearch.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024 Ian Warwick 3 | * Released under the MIT license 4 | * https://opensource.org/licenses/MIT 5 | */ 6 | 7 | package com.fluxtah.ask.api.clients.openai.assistants.model 8 | 9 | import kotlinx.serialization.SerialName 10 | import kotlinx.serialization.Serializable 11 | 12 | @Serializable 13 | data class ToolResourcesFileSearch( 14 | @SerialName("vector_store_ids") val vectorStoreIds: List = emptyList(), 15 | ) -------------------------------------------------------------------------------- /ask-api/src/main/kotlin/com/fluxtah/ask/api/clients/openai/audio/AudioApi.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024 Ian Warwick 3 | * Released under the MIT license 4 | * https://opensource.org/licenses/MIT 5 | */ 6 | package com.fluxtah.ask.api.clients.openai.audio 7 | 8 | import com.fluxtah.ask.api.clients.httpClient 9 | import com.fluxtah.ask.api.clients.openai.audio.model.CreateTranscriptionRequest 10 | import com.fluxtah.ask.api.clients.openai.audio.model.CreateTranscriptionResponse 11 | import io.ktor.client.* 12 | import io.ktor.client.call.* 13 | import io.ktor.client.request.* 14 | import io.ktor.client.request.forms.* 15 | import io.ktor.client.statement.* 16 | import io.ktor.http.* 17 | import kotlinx.serialization.SerialName 18 | import kotlinx.serialization.Serializable 19 | 20 | class AudioApi( 21 | private val client: HttpClient = httpClient, 22 | private val baseUri: String = "https://api.openai.com", 23 | private val version: String = "v1", 24 | private val apiKeyProvider: () -> String 25 | ) { 26 | suspend fun createTranscription(request: CreateTranscriptionRequest): CreateTranscriptionResponse { 27 | val response = client.post("$baseUri/$version/audio/transcriptions") { 28 | header("Authorization", "Bearer ${apiKeyProvider.invoke()}") 29 | setBody( 30 | MultiPartFormDataContent( 31 | formData { 32 | append("model", request.model) 33 | if (request.language != null) { 34 | append("language", request.language) 35 | } 36 | if (request.prompt != null) { 37 | append("prompt", request.prompt) 38 | } 39 | if (request.responseFormat != null) { 40 | append("response_format", request.responseFormat) 41 | } 42 | if (request.temperature != null) { 43 | append("temperature", request.temperature.toString()) 44 | } 45 | append("file", request.audioFile.readBytes(), Headers.build { 46 | append( 47 | HttpHeaders.ContentDisposition, 48 | "form-data; name=\"file\"; filename=\"${request.audioFile.name}\"" 49 | ) 50 | append(HttpHeaders.ContentType, "audio/wav") 51 | }) 52 | } 53 | ) 54 | ) 55 | } 56 | 57 | when (response.status) { 58 | HttpStatusCode.OK -> { 59 | return response.body() 60 | } 61 | 62 | else -> throw IllegalStateException(response.bodyAsText()) 63 | } 64 | } 65 | 66 | /** 67 | * Generates audio from the input text. 68 | */ 69 | suspend fun createSpeech(request: CreateSpeechRequest): ByteArray { 70 | val response = client.post("$baseUri/$version/audio/speech") { 71 | header("Authorization", "Bearer ${apiKeyProvider.invoke()}") 72 | contentType(ContentType.Application.Json) 73 | setBody(request) 74 | } 75 | 76 | when (response.status) { 77 | HttpStatusCode.OK -> { 78 | return response.body() 79 | } 80 | 81 | else -> throw IllegalStateException(response.bodyAsText()) 82 | } 83 | } 84 | } 85 | 86 | @Serializable 87 | data class CreateSpeechRequest( 88 | /** 89 | * One of the available TTS models: tts-1 or tts-1-hd 90 | */ 91 | @SerialName("model") 92 | val model: SpeechModel, 93 | /** 94 | * The text to generate audio for. The maximum length is 4096 characters. 95 | */ 96 | val input: String, 97 | /** 98 | * The voice to use when generating the audio. Supported voices are alloy, echo, fable, onyx, nova, and shimmer. 99 | * Previews of the voices are available in the Text to speech guide https://docs.openai.com/text-to-speech/overview/ 100 | */ 101 | val voice: SpeechVoice, 102 | 103 | /** 104 | * The format to audio in. Supported formats are mp3, opus, aac, flac, wav, and pcm. 105 | */ 106 | @SerialName("response_format") 107 | val responseFormat: ResponseFormat? = null, 108 | /** 109 | * The speed of the generated audio. Select a value from 0.25 to 4.0. 1.0 is the default. 110 | */ 111 | val speed: Double? = null 112 | ) 113 | 114 | @Serializable 115 | enum class SpeechModel { 116 | @SerialName("tts-1") 117 | TTS_1, 118 | @SerialName("tts-1-hd") 119 | TTS_1_HD 120 | } 121 | 122 | @Serializable 123 | enum class SpeechVoice { 124 | @SerialName("alloy") 125 | ALLOY, 126 | @SerialName("echo") 127 | ECHO, 128 | @SerialName("fable") 129 | FABLE, 130 | @SerialName("onyx") 131 | ONYX, 132 | @SerialName("nova") 133 | NOVA, 134 | @SerialName("shimmer") 135 | SHIMMER 136 | } 137 | 138 | @Serializable 139 | enum class ResponseFormat { 140 | @SerialName("mp3") 141 | MP3, 142 | @SerialName("opus") 143 | OPUS, 144 | @SerialName("aac") 145 | AAC, 146 | @SerialName("flac") 147 | FLAC, 148 | @SerialName("wav") 149 | WAV, 150 | @SerialName("pcm") 151 | PCM 152 | } -------------------------------------------------------------------------------- /ask-api/src/main/kotlin/com/fluxtah/ask/api/clients/openai/audio/model/CreateTranscriptionRequest.kt: -------------------------------------------------------------------------------- 1 | package com.fluxtah.ask.api.clients.openai.audio.model 2 | 3 | import java.io.File 4 | 5 | data class CreateTranscriptionRequest( 6 | val audioFile: File, 7 | val model: String = "whisper-1", 8 | /** 9 | * The language of the input audio. Supplying the input language in ISO-639-1 format will improve accuracy and latency. 10 | */ 11 | val language: String? = null, 12 | /** 13 | * An optional text to guide the model's style or continue a previous audio segment. The prompt should match the audio language. 14 | */ 15 | val prompt: String? = null, 16 | 17 | /** 18 | * Defaults to json. The format of the transcript output, in one of these options: json, text, srt, verbose_json, or vtt. 19 | */ 20 | val responseFormat: String? = null, 21 | 22 | /** 23 | * The sampling temperature, between 0 and 1. Higher values like 0.8 will make the output more random, while lower values like 0.2 24 | * will make it more focused and deterministic. If set to 0, the model will use log probability to 25 | * automatically increase the temperature until certain thresholds are hit. 26 | */ 27 | val temperature: Double? = null, 28 | ) -------------------------------------------------------------------------------- /ask-api/src/main/kotlin/com/fluxtah/ask/api/clients/openai/audio/model/CreateTranscriptionResponse.kt: -------------------------------------------------------------------------------- 1 | package com.fluxtah.ask.api.clients.openai.audio.model 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class CreateTranscriptionResponse(val text: String) -------------------------------------------------------------------------------- /ask-api/src/main/kotlin/com/fluxtah/ask/api/commanding/CommandFactory.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024 Ian Warwick 3 | * Released under the MIT license 4 | * https://opensource.org/licenses/MIT 5 | */ 6 | 7 | package com.fluxtah.ask.api.commanding 8 | 9 | import com.fluxtah.ask.api.commanding.commands.Command 10 | import com.fluxtah.ask.api.printers.AskResponsePrinter 11 | import com.fluxtah.ask.api.store.user.UserProperties 12 | import kotlinx.coroutines.runBlocking 13 | import org.koin.core.component.KoinComponent 14 | import org.koin.core.component.get 15 | 16 | data class CommandEntry(val name: String, val description: String, val command: () -> Command) 17 | 18 | class CommandFactory( 19 | private val responsePrinter: AskResponsePrinter, 20 | private val userProperties: UserProperties, 21 | ) : KoinComponent { 22 | val commands = mutableMapOf() 23 | 24 | inline fun registerCommand(name: String, description: String) { 25 | commands[name] = CommandEntry(name, description) { get() } 26 | } 27 | 28 | suspend fun executeCommand(input: String) { 29 | val parts = input.drop(1).split(" ") 30 | val command = commands[parts[0]]?.command?.invoke() 31 | 32 | if (command == null) { 33 | responsePrinter.begin().println("Command not found: ${parts[0]}").end() 34 | return 35 | } 36 | 37 | if (command.requiresApiKey) { 38 | if (userProperties.getOpenaiApiKey().isEmpty()) { 39 | responsePrinter 40 | .begin().println("You need to set an OpenAI API key first! with /set-key ").end() 41 | return 42 | } 43 | } 44 | command.execute(parts.drop(1)) 45 | } 46 | 47 | fun getCommandsSortedByName(): List { 48 | return commands.values.sortedBy { it.name } 49 | } 50 | } -------------------------------------------------------------------------------- /ask-api/src/main/kotlin/com/fluxtah/ask/api/commanding/commands/Clear.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024 Ian Warwick 3 | * Released under the MIT license 4 | * https://opensource.org/licenses/MIT 5 | */ 6 | 7 | package com.fluxtah.ask.api.commanding.commands 8 | 9 | import com.fluxtah.ask.api.printers.AskResponsePrinter 10 | 11 | class Clear(private val printer: AskResponsePrinter) : Command() { 12 | override suspend fun execute(args: List) { 13 | printer.printMessage("\u001b[H\u001b[2J") 14 | } 15 | 16 | override val requiresApiKey: Boolean = false 17 | } -------------------------------------------------------------------------------- /ask-api/src/main/kotlin/com/fluxtah/ask/api/commanding/commands/ClearModel.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024 Ian Warwick 3 | * Released under the MIT license 4 | * https://opensource.org/licenses/MIT 5 | */ 6 | 7 | package com.fluxtah.ask.api.commanding.commands 8 | 9 | import com.fluxtah.ask.api.printers.AskResponsePrinter 10 | import com.fluxtah.ask.api.store.user.UserProperties 11 | 12 | class ClearModel( 13 | private val userProperties: UserProperties, 14 | private val printer: AskResponsePrinter 15 | ) : Command() { 16 | override val requiresApiKey: Boolean = false 17 | override suspend fun execute(args: List) { 18 | userProperties.setModel("") 19 | userProperties.save() 20 | printer.printMessage("Model cleared, all targeted assistants will use their default model") 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /ask-api/src/main/kotlin/com/fluxtah/ask/api/commanding/commands/Command.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024 Ian Warwick 3 | * Released under the MIT license 4 | * https://opensource.org/licenses/MIT 5 | */ 6 | 7 | package com.fluxtah.ask.api.commanding.commands 8 | 9 | abstract class Command { 10 | abstract val requiresApiKey: Boolean 11 | abstract suspend fun execute(args: List) 12 | } 13 | -------------------------------------------------------------------------------- /ask-api/src/main/kotlin/com/fluxtah/ask/api/commanding/commands/DeleteThread.kt: -------------------------------------------------------------------------------- 1 | package com.fluxtah.ask.api.commanding.commands 2 | 3 | import com.fluxtah.ask.api.clients.openai.assistants.AssistantsApi 4 | import com.fluxtah.ask.api.printers.AskResponsePrinter 5 | import com.fluxtah.ask.api.repository.ThreadRepository 6 | import com.fluxtah.ask.api.store.user.UserProperties 7 | 8 | class DeleteThread( 9 | private val assistantsApi: AssistantsApi, 10 | private val threadRepository: ThreadRepository, 11 | private val userProperties: UserProperties, 12 | private val printer: AskResponsePrinter, 13 | ) : Command() { 14 | override val requiresApiKey: Boolean = true 15 | override suspend fun execute(args: List) { 16 | if (args.isEmpty() || args.joinToString("").trim().isEmpty()) { 17 | printer.printMessage("Invalid number of arguments for /thread-delete, expected a thread ID following the command") 18 | return 19 | } 20 | 21 | val threadId = args.first() 22 | 23 | assistantsApi.threads.deleteThread(threadId) 24 | threadRepository.deleteThread(threadId) 25 | if (userProperties.getThreadId() == threadId) { 26 | userProperties.setThreadId("") 27 | userProperties.save() 28 | } 29 | printer.printMessage("Thread deleted") 30 | } 31 | } -------------------------------------------------------------------------------- /ask-api/src/main/kotlin/com/fluxtah/ask/api/commanding/commands/EnableTalkCommand.kt: -------------------------------------------------------------------------------- 1 | package com.fluxtah.ask.api.commanding.commands 2 | 3 | import com.fluxtah.ask.api.printers.AskResponsePrinter 4 | import com.fluxtah.ask.api.store.user.UserProperties 5 | import com.fluxtah.ask.api.audio.TextToSpeechPlayer 6 | 7 | class EnableTalkCommand( 8 | private val userProperties: UserProperties, 9 | private val responsePrinter: AskResponsePrinter, 10 | private val textToSpeechPlayer: TextToSpeechPlayer 11 | ) : Command() { 12 | override val requiresApiKey: Boolean = false 13 | 14 | override suspend fun execute(args: List) { 15 | val enable = !userProperties.getTalkEnabled() 16 | userProperties.setTalkEnabled(enable) 17 | textToSpeechPlayer.enabled = enable 18 | responsePrinter.printMessage("Talk mode is now ${if (enable) "enabled" else "disabled"}.") 19 | if (!enable) { 20 | textToSpeechPlayer.stop() 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /ask-api/src/main/kotlin/com/fluxtah/ask/api/commanding/commands/Exit.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024 Ian Warwick 3 | * Released under the MIT license 4 | * https://opensource.org/licenses/MIT 5 | */ 6 | 7 | package com.fluxtah.ask.api.commanding.commands 8 | 9 | import com.fluxtah.ask.api.printers.AskResponsePrinter 10 | import kotlin.system.exitProcess 11 | 12 | class Exit(private val printer: AskResponsePrinter) : Command() { 13 | override val requiresApiKey: Boolean = false 14 | override suspend fun execute(args: List) { 15 | printer.printMessage("Exiting the application...") 16 | exitProcess(0) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /ask-api/src/main/kotlin/com/fluxtah/ask/api/commanding/commands/GetAssistant.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024 Ian Warwick 3 | * Released under the MIT license 4 | * https://opensource.org/licenses/MIT 5 | */ 6 | 7 | package com.fluxtah.ask.api.commanding.commands 8 | 9 | import com.fluxtah.ask.api.assistants.AssistantInstallRepository 10 | import com.fluxtah.ask.api.assistants.AssistantRegistry 11 | import com.fluxtah.ask.api.printers.AskResponsePrinter 12 | 13 | class GetAssistant( 14 | private val assistantRegistry: AssistantRegistry, 15 | private val assistantInstallRepository: AssistantInstallRepository, 16 | private val responsePrinter: AskResponsePrinter, 17 | ) : 18 | Command() { 19 | override val requiresApiKey: Boolean = true 20 | override suspend fun execute(args: List) { 21 | if (args.size != 1) { 22 | responsePrinter 23 | .printMessage("Invalid number of arguments for /assistant-info, expected a assistant ID following the command") 24 | return 25 | } 26 | 27 | val assistantId = args.first() 28 | 29 | val assistantDef = assistantRegistry.getAssistantById(assistantId) 30 | if (assistantDef == null) { 31 | responsePrinter.printMessage("Assistant not found") 32 | return 33 | } 34 | 35 | val assistantInstallRecord = assistantInstallRepository.getAssistantInstallRecord(assistantId) 36 | 37 | val installed = assistantInstallRecord != null 38 | 39 | responsePrinter 40 | .printMessage("@${assistantDef.id} - ${assistantDef.name} ${assistantDef.version}, ${assistantDef.model}, installed: $installed") 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /ask-api/src/main/kotlin/com/fluxtah/ask/api/commanding/commands/GetThread.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024 Ian Warwick 3 | * Released under the MIT license 4 | * https://opensource.org/licenses/MIT 5 | */ 6 | 7 | package com.fluxtah.ask.api.commanding.commands 8 | 9 | import com.fluxtah.ask.api.clients.openai.assistants.AssistantsApi 10 | import com.fluxtah.ask.api.clients.openai.assistants.model.AssistantThread 11 | import com.fluxtah.ask.api.printers.AskResponsePrinter 12 | import com.fluxtah.ask.api.store.user.UserProperties 13 | import kotlinx.serialization.encodeToString 14 | 15 | class GetThread( 16 | private val assistantsApi: AssistantsApi, 17 | private val userProperties: UserProperties, 18 | private val printer: AskResponsePrinter 19 | ) : Command() { 20 | override val requiresApiKey: Boolean = true 21 | override suspend fun execute(args: List) { 22 | val threadId = if (args.isEmpty()) null else args.first() 23 | 24 | val actualThread = threadId ?: userProperties.getThreadId().ifEmpty { null } 25 | 26 | if (actualThread == null) { 27 | printer.printMessage("You need to create a thread first. Use /thread-new or pass a thread id as the first argument") 28 | return 29 | } 30 | println(JSON.encodeToString(assistantsApi.threads.getThread(actualThread))) 31 | } 32 | } 33 | 34 | -------------------------------------------------------------------------------- /ask-api/src/main/kotlin/com/fluxtah/ask/api/commanding/commands/Help.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024 Ian Warwick 3 | * Released under the MIT license 4 | * https://opensource.org/licenses/MIT 5 | */ 6 | 7 | package com.fluxtah.ask.api.commanding.commands 8 | 9 | import com.fluxtah.ask.api.printers.AskResponsePrinter 10 | import com.fluxtah.ask.api.commanding.CommandFactory 11 | import kotlinx.coroutines.delay 12 | 13 | class Help( 14 | private val commandFactory: CommandFactory, 15 | private val printer: AskResponsePrinter 16 | ) : Command() { 17 | override val requiresApiKey: Boolean = false 18 | override suspend fun execute(args: List) { 19 | printer 20 | .begin() 21 | .println(String.format("%-20s %-30s", "Command", "Description")) 22 | .println("--------------------------------------------------------------------------------") 23 | .apply { 24 | commandFactory.getCommandsSortedByName().forEach { 25 | println(String.format("%-20s %-30s", it.name, it.description)) 26 | } 27 | 28 | } 29 | .end() 30 | } 31 | } 32 | 33 | -------------------------------------------------------------------------------- /ask-api/src/main/kotlin/com/fluxtah/ask/api/commanding/commands/InstallAssistant.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024 Ian Warwick 3 | * Released under the MIT license 4 | * https://opensource.org/licenses/MIT 5 | */ 6 | 7 | package com.fluxtah.ask.api.commanding.commands 8 | 9 | import com.fluxtah.ask.api.assistants.AssistantInstallRepository 10 | import com.fluxtah.ask.api.assistants.AssistantRegistry 11 | import com.fluxtah.ask.api.printers.AskResponsePrinter 12 | 13 | class InstallAssistant( 14 | private val assistantRegistry: AssistantRegistry, 15 | private val assistantInstallRepository: AssistantInstallRepository, 16 | private val printer: AskResponsePrinter, 17 | ) : Command() { 18 | override val requiresApiKey: Boolean = true 19 | override suspend fun execute(args: List) { 20 | if (args.size != 1) { 21 | printer 22 | .printMessage("Invalid number of arguments for /assistant-install, expected an assistant ID following the command") 23 | return 24 | } 25 | 26 | val assistantId = args.first() 27 | 28 | val def = assistantRegistry.getAssistantById(assistantId) 29 | 30 | if (def == null) { 31 | printer.printMessage("Assistant not found: $assistantId") 32 | return 33 | } 34 | 35 | val assistantInstallRecord = assistantInstallRepository.install(def) 36 | 37 | printer 38 | .printMessage("Installed assistant: @${def.id} ${def.version} as ${assistantInstallRecord.installId}") 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /ask-api/src/main/kotlin/com/fluxtah/ask/api/commanding/commands/JSON.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024 Ian Warwick 3 | * Released under the MIT license 4 | * https://opensource.org/licenses/MIT 5 | */ 6 | 7 | package com.fluxtah.ask.api.commanding.commands 8 | 9 | import kotlinx.serialization.json.Json 10 | 11 | internal val JSON = Json { 12 | isLenient = true 13 | prettyPrint = true 14 | } 15 | -------------------------------------------------------------------------------- /ask-api/src/main/kotlin/com/fluxtah/ask/api/commanding/commands/ListAssistants.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024 Ian Warwick 3 | * Released under the MIT license 4 | * https://opensource.org/licenses/MIT 5 | */ 6 | 7 | package com.fluxtah.ask.api.commanding.commands 8 | 9 | import com.fluxtah.ask.api.ansi.cyan 10 | import com.fluxtah.ask.api.assistants.AssistantInstallRepository 11 | import com.fluxtah.ask.api.assistants.AssistantRegistry 12 | import com.fluxtah.ask.api.printers.AskResponsePrinter 13 | import com.fluxtah.ask.api.version.VersionUtils 14 | 15 | class ListAssistants( 16 | private val assistantRegistry: AssistantRegistry, 17 | private val assistantInstallRepository: AssistantInstallRepository, 18 | private val printer: AskResponsePrinter 19 | ) : Command() { 20 | override val requiresApiKey: Boolean = true 21 | override suspend fun execute(args: List) { 22 | val installedAssistants = assistantInstallRepository.getAssistantInstallRecords() 23 | printer 24 | .begin() 25 | .println() 26 | .println(String.format("%-10s %-10s %-16s %-12s %-8s", "ID", "Version", "Name", "Installed", "Update")) 27 | .println("-----------------------------------------------------------------") 28 | .apply { 29 | assistantRegistry.getAssistants().forEach { 30 | val installedAssistant = installedAssistants.find { record -> record.id == it.id } 31 | val currentVersion = installedAssistant?.version ?: it.version 32 | val upgradeAvailable = 33 | if (installedAssistant != null && 34 | VersionUtils.isVersionGreater(it.version, installedAssistant.version) 35 | ) { 36 | cyan(it.version) 37 | } else { 38 | "x" 39 | } 40 | println( 41 | String.format( 42 | "%-10s %-10s %-16s %-12s %-8s", 43 | it.id, 44 | currentVersion, 45 | it.name, 46 | if (installedAssistant != null) "✔" else "x", 47 | upgradeAvailable 48 | ) 49 | ) 50 | } 51 | } 52 | .end() 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /ask-api/src/main/kotlin/com/fluxtah/ask/api/commanding/commands/ListMessages.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024 Ian Warwick 3 | * Released under the MIT license 4 | * https://opensource.org/licenses/MIT 5 | */ 6 | 7 | package com.fluxtah.ask.api.commanding.commands 8 | 9 | import com.fluxtah.ask.api.clients.openai.assistants.AssistantsApi 10 | import com.fluxtah.ask.api.printers.AskResponsePrinter 11 | import com.fluxtah.ask.api.store.user.UserProperties 12 | 13 | class ListMessages( 14 | private val assistantsApi: AssistantsApi, 15 | private val userProperties: UserProperties, 16 | private val printer: AskResponsePrinter 17 | ) : 18 | Command() { 19 | override val requiresApiKey: Boolean = true 20 | override suspend fun execute(args: List) { 21 | val threadId = userProperties.getThreadId() 22 | if (threadId.isEmpty()) { 23 | printer.printMessage("You need to create a thread first. Use /thread-new") 24 | return 25 | } 26 | printer 27 | .begin() 28 | .println() 29 | .println(String.format("%-19s %-28s %-10s %-28s", "Date", "ID", "Role", "Content")) 30 | .println("-----------------------------------------------------------------------------------------------") 31 | .apply { 32 | assistantsApi.messages.listMessages(threadId).data.forEach { 33 | val contentShortened = it.content.joinToString { it.text.value }.lines().first().take(32) 34 | val contentElipsised = 35 | if (contentShortened.length < 32) contentShortened else "$contentShortened..." 36 | println( 37 | String.format( 38 | "%-19s %-28s %-10s %-28s", 39 | it.createdAt.toShortDateTimeString(), 40 | it.id, 41 | it.role, 42 | contentElipsised 43 | ) 44 | ) 45 | } 46 | } 47 | .end() 48 | } 49 | } 50 | 51 | -------------------------------------------------------------------------------- /ask-api/src/main/kotlin/com/fluxtah/ask/api/commanding/commands/ListRunSteps.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024 Ian Warwick 3 | * Released under the MIT license 4 | * https://opensource.org/licenses/MIT 5 | */ 6 | 7 | package com.fluxtah.ask.api.commanding.commands 8 | 9 | import com.fluxtah.ask.api.clients.openai.assistants.AssistantsApi 10 | import com.fluxtah.ask.api.clients.openai.assistants.model.AssistantRunStepList 11 | import com.fluxtah.ask.api.printers.AskResponsePrinter 12 | import com.fluxtah.ask.api.store.user.UserProperties 13 | import kotlinx.serialization.encodeToString 14 | 15 | class ListRunSteps( 16 | private val assistantsApi: AssistantsApi, 17 | private val userProperties: UserProperties, 18 | private val printer: AskResponsePrinter 19 | ) : 20 | Command() { 21 | override val requiresApiKey: Boolean = true 22 | override suspend fun execute(args: List) { 23 | val threadId = userProperties.getThreadId() 24 | if (threadId.isEmpty()) { 25 | printer.printMessage("You need to create a thread first. Use /thread-new") 26 | return 27 | } 28 | val runId = userProperties.getRunId() 29 | if (runId.isEmpty()) { 30 | printer.printMessage("No last run") 31 | return 32 | } 33 | 34 | printer 35 | .begin() 36 | .println(JSON.encodeToString(assistantsApi.runs.listRunSteps(threadId, runId))) 37 | .end() 38 | } 39 | } -------------------------------------------------------------------------------- /ask-api/src/main/kotlin/com/fluxtah/ask/api/commanding/commands/ListRuns.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024 Ian Warwick 3 | * Released under the MIT license 4 | * https://opensource.org/licenses/MIT 5 | */ 6 | 7 | package com.fluxtah.ask.api.commanding.commands 8 | 9 | import com.fluxtah.ask.api.clients.openai.assistants.AssistantsApi 10 | import com.fluxtah.ask.api.printers.AskResponsePrinter 11 | import com.fluxtah.ask.api.store.user.UserProperties 12 | 13 | class ListRuns( 14 | private val assistantsApi: AssistantsApi, 15 | private val userProperties: UserProperties, 16 | private val printer: AskResponsePrinter 17 | ) : Command() { 18 | override val requiresApiKey: Boolean = true 19 | override suspend fun execute(args: List) { 20 | val threadId = userProperties.getThreadId() 21 | if (threadId.isEmpty()) { 22 | printer.printMessage("You need to create a thread first. Use /thread-new") 23 | return 24 | } 25 | printer 26 | .begin() 27 | .println() 28 | .println(String.format("%-19s %-28s %-12s %-10s %-10s", "Created", "ID", "Status", "In", "Out")) 29 | .println("------------------------------------------------------------------------------------") 30 | .apply { 31 | assistantsApi.runs.listRuns(threadId).data.forEach { 32 | println( 33 | String.format( 34 | "%-19s %-28s %-12s %-10s %-10s", 35 | it.createdAt.toShortDateTimeString(), 36 | it.id, 37 | it.status, 38 | it.usage?.promptTokens, 39 | it.usage?.completionTokens 40 | ) 41 | ) 42 | } 43 | } 44 | .end() 45 | } 46 | } 47 | 48 | -------------------------------------------------------------------------------- /ask-api/src/main/kotlin/com/fluxtah/ask/api/commanding/commands/ListThreads.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024 Ian Warwick 3 | * Released under the MIT license 4 | * https://opensource.org/licenses/MIT 5 | */ 6 | 7 | package com.fluxtah.ask.api.commanding.commands 8 | 9 | import com.fluxtah.ask.api.printers.AskResponsePrinter 10 | import com.fluxtah.ask.api.repository.ThreadRepository 11 | import com.fluxtah.ask.api.store.user.UserProperties 12 | 13 | class ListThreads( 14 | private val userProperties: UserProperties, 15 | private val threadRepository: ThreadRepository, 16 | private val printer: AskResponsePrinter 17 | ) : Command() { 18 | override val requiresApiKey: Boolean = true 19 | override suspend fun execute(args: List) { 20 | val threads = threadRepository.listThreads() 21 | printer 22 | .begin() 23 | .println() 24 | .println(String.format("%-36s %-30s", "Thread", "Title")) 25 | .println("--------------------------------------------------------------------------------") 26 | .apply { 27 | if (threads.isEmpty()) { 28 | println("No threads found, type /thread-new to create a new thread") 29 | } else { 30 | threads.forEach { 31 | val title = it.title.ifEmpty { "" } 32 | if (userProperties.getThreadId() == it.threadId) { 33 | println(String.format("%-36s %-30s", it.threadId, "$title (Active)")) 34 | } else { 35 | println(String.format("%-36s %-30s", it.threadId, title)) 36 | } 37 | } 38 | } 39 | } 40 | .end() 41 | } 42 | } -------------------------------------------------------------------------------- /ask-api/src/main/kotlin/com/fluxtah/ask/api/commanding/commands/MaxCompletionTokens.kt: -------------------------------------------------------------------------------- 1 | package com.fluxtah.ask.api.commanding.commands 2 | 3 | import com.fluxtah.ask.api.printers.AskResponsePrinter 4 | import com.fluxtah.ask.api.store.user.UserProperties 5 | 6 | class MaxCompletionTokens( 7 | private val userProperties: UserProperties, 8 | private val responsePrinter: AskResponsePrinter 9 | ) : Command() { 10 | override val requiresApiKey: Boolean = false 11 | 12 | override suspend fun execute(args: List) { 13 | if (args.size != 1 || args.first().toIntOrNull() == null) { 14 | responsePrinter 15 | .printMessage("Current max completion tokens: ${userProperties.getMaxCompletionTokens()}, to set a new value use /max-completion-tokens ") 16 | } else { 17 | val maxCompletionTokens = args.first().toInt() 18 | userProperties.setMaxCompletionTokens(maxCompletionTokens) 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /ask-api/src/main/kotlin/com/fluxtah/ask/api/commanding/commands/MaxPromptTokens.kt: -------------------------------------------------------------------------------- 1 | package com.fluxtah.ask.api.commanding.commands 2 | 3 | import com.fluxtah.ask.api.printers.AskResponsePrinter 4 | import com.fluxtah.ask.api.store.user.UserProperties 5 | 6 | class MaxPromptTokens( 7 | private val userProperties: UserProperties, 8 | private val responsePrinter: AskResponsePrinter 9 | ) : Command() { 10 | override val requiresApiKey: Boolean = false 11 | 12 | override suspend fun execute(args: List) { 13 | if (args.size != 1 || args.first().toIntOrNull() == null) { 14 | responsePrinter 15 | .printMessage("Current max prompt tokens: ${userProperties.getMaxPromptTokens()}, to set a new value use /max-prompt-tokens ") 16 | } else { 17 | val maxPromptTokens = args.first().toInt() 18 | userProperties.setMaxPromptTokens(maxPromptTokens) 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /ask-api/src/main/kotlin/com/fluxtah/ask/api/commanding/commands/PlayTts.kt: -------------------------------------------------------------------------------- 1 | package com.fluxtah.ask.api.commanding.commands 2 | 3 | import com.fluxtah.ask.api.audio.TextToSpeechPlayer 4 | 5 | class PlayTts(private val player: TextToSpeechPlayer) : Command() { 6 | override suspend fun execute(args: List) { 7 | player.stop() 8 | player.playNext() 9 | } 10 | 11 | override val requiresApiKey: Boolean = false 12 | } -------------------------------------------------------------------------------- /ask-api/src/main/kotlin/com/fluxtah/ask/api/commanding/commands/RecordVoice.kt: -------------------------------------------------------------------------------- 1 | package com.fluxtah.ask.api.commanding.commands 2 | 3 | import com.fluxtah.ask.api.audio.AudioRecorder 4 | import com.fluxtah.ask.api.audio.TextToSpeechPlayer 5 | import com.fluxtah.ask.api.printers.AskResponsePrinter 6 | import kotlinx.coroutines.CoroutineScope 7 | import kotlinx.coroutines.launch 8 | 9 | class RecordVoice( 10 | private val coroutineScope: CoroutineScope, 11 | private val audioRecorder: AudioRecorder, 12 | private val responsePrinter: AskResponsePrinter, 13 | private val player: TextToSpeechPlayer 14 | ) : Command() { 15 | override suspend fun execute(args: List) { 16 | coroutineScope.launch { 17 | player.stop() 18 | audioRecorder.start() 19 | } 20 | responsePrinter.begin().print("\u001b[1A\u001b[2K").end() 21 | // Unfortunate hack to allow the audio recorder to start/stop prevents a race condition 22 | Thread.sleep(250) 23 | } 24 | 25 | override val requiresApiKey: Boolean = false 26 | } -------------------------------------------------------------------------------- /ask-api/src/main/kotlin/com/fluxtah/ask/api/commanding/commands/RecoverRun.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024 Ian Warwick 3 | * Released under the MIT license 4 | * https://opensource.org/licenses/MIT 5 | */ 6 | 7 | package com.fluxtah.ask.api.commanding.commands 8 | 9 | import com.fluxtah.ask.api.AssistantRunManager 10 | 11 | class RecoverRun(private val assistantRunManager: AssistantRunManager) : Command() { 12 | override val requiresApiKey: Boolean = true 13 | override suspend fun execute(args: List) { 14 | assistantRunManager.recoverRun() 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /ask-api/src/main/kotlin/com/fluxtah/ask/api/commanding/commands/ReinstallAssistant.kt: -------------------------------------------------------------------------------- 1 | package com.fluxtah.ask.api.commanding.commands 2 | 3 | import com.fluxtah.ask.api.assistants.AssistantInstallRepository 4 | import com.fluxtah.ask.api.assistants.AssistantRegistry 5 | import com.fluxtah.ask.api.printers.AskResponsePrinter 6 | 7 | class ReinstallAssistant( 8 | private val assistantRegistry: AssistantRegistry, 9 | private val assistantInstallRepository: AssistantInstallRepository, 10 | private val printer: AskResponsePrinter, 11 | ) : Command() { 12 | override val requiresApiKey: Boolean = true 13 | override suspend fun execute(args: List) { 14 | if (args.size != 1) { 15 | printer.printMessage("Invalid number of arguments for /assistant-reinstall, expected an assistant ID following the command") 16 | return 17 | } 18 | 19 | val assistantId = args[0] 20 | 21 | val def = assistantRegistry.getAssistantById(assistantId) 22 | 23 | if (def == null) { 24 | printer.printMessage("Assistant not found: $assistantId") 25 | return 26 | } 27 | 28 | val assistantInstallRecord = assistantInstallRepository.getAssistantInstallRecord(assistantId) 29 | 30 | if (assistantInstallRecord != null) { 31 | if (assistantInstallRepository.uninstall(assistantInstallRecord)) { 32 | printer.printMessage("Uninstalled assistant: @${def.id} ${assistantInstallRecord.version} ${assistantInstallRecord.installId}") 33 | } else { 34 | printer.printMessage("Failed to uninstall assistant: @${def.id} ${assistantInstallRecord.version} ${assistantInstallRecord.installId}") 35 | return 36 | } 37 | } 38 | 39 | val newAssistantInstallRecord = assistantInstallRepository.install(def) 40 | printer.printMessage("Installed assistant: @${def.id} ${def.version} ${newAssistantInstallRecord.installId}") 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /ask-api/src/main/kotlin/com/fluxtah/ask/api/commanding/commands/SetLogLevel.kt: -------------------------------------------------------------------------------- 1 | package com.fluxtah.ask.api.commanding.commands 2 | 3 | import com.fluxtah.ask.api.printers.AskResponsePrinter 4 | import com.fluxtah.ask.api.store.user.UserProperties 5 | import com.fluxtah.askpluginsdk.logging.AskLogger 6 | import com.fluxtah.askpluginsdk.logging.LogLevel 7 | 8 | data class SetLogLevel( 9 | val userProperties: UserProperties, 10 | val askLogger: AskLogger, 11 | val responsePrinter: AskResponsePrinter 12 | ) : Command() { 13 | override val requiresApiKey: Boolean = false 14 | override suspend fun execute(args: List) { 15 | if (args.size != 1) { 16 | responsePrinter 17 | .printMessage("Invalid number of arguments for /log-level, expected a log level ERROR, DEBUG, INFO or OFF following the command, current log level: ${userProperties.getLogLevel()}") 18 | return 19 | } 20 | 21 | try { 22 | val logLevel = LogLevel.valueOf(args.first()) 23 | userProperties.setLogLevel(logLevel) 24 | askLogger.setLogLevel(logLevel) 25 | userProperties.save() 26 | } catch (e: IllegalArgumentException) { 27 | responsePrinter.printMessage("Invalid log level: ${args.first()}") 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /ask-api/src/main/kotlin/com/fluxtah/ask/api/commanding/commands/SetModel.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024 Ian Warwick 3 | * Released under the MIT license 4 | * https://opensource.org/licenses/MIT 5 | */ 6 | 7 | package com.fluxtah.ask.api.commanding.commands 8 | 9 | import com.fluxtah.ask.api.printers.AskResponsePrinter 10 | import com.fluxtah.ask.api.store.user.UserProperties 11 | 12 | class SetModel( 13 | private val userProperties: UserProperties, 14 | private val printer: AskResponsePrinter 15 | ) : Command() { 16 | override val requiresApiKey: Boolean = false 17 | override suspend fun execute(args: List) { 18 | if (args.size != 1) { 19 | printer.printMessage("Invalid number of arguments for /model, expected a model ID following the command") 20 | return 21 | } 22 | 23 | val modelId = args.first() 24 | userProperties.setModel(modelId) 25 | userProperties.save() 26 | printer.printMessage("Model set to $modelId, all targeted assistants will use this model until you /model-clear") 27 | } 28 | } -------------------------------------------------------------------------------- /ask-api/src/main/kotlin/com/fluxtah/ask/api/commanding/commands/SetOpenAiApiKey.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024 Ian Warwick 3 | * Released under the MIT license 4 | * https://opensource.org/licenses/MIT 5 | */ 6 | 7 | package com.fluxtah.ask.api.commanding.commands 8 | 9 | import com.fluxtah.ask.api.printers.AskResponsePrinter 10 | import com.fluxtah.ask.api.store.user.UserProperties 11 | 12 | class SetOpenAiApiKey( 13 | private val userProperties: UserProperties, 14 | private val responsePrinter: AskResponsePrinter 15 | ) : Command() { 16 | override val requiresApiKey: Boolean = false 17 | override suspend fun execute(args: List) { 18 | if (args.size != 1) { 19 | responsePrinter.printMessage("Invalid number of arguments for /set-key, expected an API key following the command") 20 | return 21 | } 22 | val apiKey = args[0] 23 | userProperties.setOpenAiApiKey(apiKey) 24 | userProperties.save() 25 | } 26 | } -------------------------------------------------------------------------------- /ask-api/src/main/kotlin/com/fluxtah/ask/api/commanding/commands/ShellExec.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024 Ian Warwick 3 | * Released under the MIT license 4 | * https://opensource.org/licenses/MIT 5 | */ 6 | 7 | package com.fluxtah.ask.api.commanding.commands 8 | 9 | import com.fluxtah.ask.api.printers.AskResponsePrinter 10 | import java.io.BufferedReader 11 | import java.io.InputStreamReader 12 | 13 | class ShellExec(private val responsePrinter: AskResponsePrinter) : Command() { 14 | override val requiresApiKey: Boolean = false 15 | override suspend fun execute(args: List) { 16 | if (args.isEmpty()) { 17 | responsePrinter.printMessage("Invalid number of arguments for /exec, expected a shell command following the command") 18 | return 19 | } 20 | 21 | val command = args.joinToString(" ") 22 | executeShellCommand(command) 23 | } 24 | 25 | private fun executeShellCommand(command: String) { 26 | try { 27 | val process = ProcessBuilder(*command.split(" ").toTypedArray()).start() 28 | BufferedReader(InputStreamReader(process.inputStream)).use { reader -> 29 | val result = reader.readLines().joinToString("\n") 30 | responsePrinter.printMessage(result) 31 | } 32 | BufferedReader(InputStreamReader(process.errorStream)).use { reader -> 33 | val error = reader.readLines().joinToString("\n") 34 | if (error.isNotEmpty()) { 35 | responsePrinter.printMessage(error) 36 | } 37 | } 38 | } catch (e: Exception) { 39 | responsePrinter.printMessage("Shell command error: ${e.message}") 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /ask-api/src/main/kotlin/com/fluxtah/ask/api/commanding/commands/ShowHttpLog.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024 Ian Warwick 3 | * Released under the MIT license 4 | * https://opensource.org/licenses/MIT 5 | */ 6 | 7 | package com.fluxtah.ask.api.commanding.commands 8 | 9 | import com.fluxtah.ask.api.clients.openai.assistants.HTTP_LOG 10 | import com.fluxtah.ask.api.printers.AskResponsePrinter 11 | 12 | class ShowHttpLog(private val printer: AskResponsePrinter) : Command() { 13 | override val requiresApiKey: Boolean = false 14 | override suspend fun execute(args: List) { 15 | HTTP_LOG.forEach { 16 | printer.printMessage(it) 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /ask-api/src/main/kotlin/com/fluxtah/ask/api/commanding/commands/SkipTts.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024 Ian Warwick 3 | * Released under the MIT license 4 | * https://opensource.org/licenses/MIT 5 | */ 6 | 7 | package com.fluxtah.ask.api.commanding.commands 8 | 9 | import com.fluxtah.ask.api.audio.TextToSpeechPlayer 10 | 11 | class SkipTts(private val player: TextToSpeechPlayer) : Command() { 12 | override suspend fun execute(args: List) { 13 | player.stop() 14 | player.skipNext() 15 | player.playNext() 16 | } 17 | 18 | override val requiresApiKey: Boolean = false 19 | } 20 | 21 | -------------------------------------------------------------------------------- /ask-api/src/main/kotlin/com/fluxtah/ask/api/commanding/commands/SwitchThread.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024 Ian Warwick 3 | * Released under the MIT license 4 | * https://opensource.org/licenses/MIT 5 | */ 6 | 7 | package com.fluxtah.ask.api.commanding.commands 8 | 9 | import com.fluxtah.ask.api.printers.AskResponsePrinter 10 | import com.fluxtah.ask.api.repository.ThreadRepository 11 | import com.fluxtah.ask.api.store.user.UserProperties 12 | 13 | class SwitchThread( 14 | private val userProperties: UserProperties, 15 | private val threadRepository: ThreadRepository, 16 | private val printer: AskResponsePrinter 17 | ) : Command() { 18 | override val requiresApiKey: Boolean = true 19 | override suspend fun execute(args: List) { 20 | if (args.size != 1) { 21 | printer.printMessage("Invalid number of arguments for /thread-switch, expected a thread ID following the command") 22 | return 23 | } 24 | 25 | val threadId = args.first() 26 | val thread = threadRepository.getThreadById(threadId) 27 | if (thread != null) { 28 | userProperties.setThreadId(threadId) 29 | userProperties.save() 30 | printer.printMessage("Switched to thread: $threadId") 31 | } else { 32 | printer.printMessage("Thread with ID $threadId not found") 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /ask-api/src/main/kotlin/com/fluxtah/ask/api/commanding/commands/ThreadNew.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024 Ian Warwick 3 | * Released under the MIT license 4 | * https://opensource.org/licenses/MIT 5 | */ 6 | 7 | package com.fluxtah.ask.api.commanding.commands 8 | 9 | import com.fluxtah.ask.api.clients.openai.assistants.AssistantsApi 10 | import com.fluxtah.ask.api.printers.AskResponsePrinter 11 | import com.fluxtah.ask.api.store.user.UserProperties 12 | import com.fluxtah.ask.api.repository.ThreadRepository 13 | import java.util.* 14 | 15 | class ThreadNew( 16 | private val assistantsApi: AssistantsApi, 17 | private val userProperties: UserProperties, 18 | private val threadRepository: ThreadRepository, 19 | private val printer: AskResponsePrinter 20 | ) : 21 | Command() { 22 | override val requiresApiKey: Boolean = true 23 | override suspend fun execute(args: List) { 24 | val title = if (args.isNotEmpty()) args.joinToString(" ") else null 25 | 26 | val thread = assistantsApi.threads.createThread() 27 | printer.printMessage("Created thread: ${thread.id} at ${Date(thread.createdAt)}") 28 | userProperties.setThreadId(thread.id) 29 | userProperties.save() 30 | threadRepository.createThread(thread.id, title ?: "") 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /ask-api/src/main/kotlin/com/fluxtah/ask/api/commanding/commands/ThreadRecall.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024 Ian Warwick 3 | * Released under the MIT license 4 | * https://opensource.org/licenses/MIT 5 | */ 6 | 7 | package com.fluxtah.ask.api.commanding.commands 8 | 9 | import com.fluxtah.ask.api.ansi.green 10 | import com.fluxtah.ask.api.clients.openai.assistants.AssistantsApi 11 | import com.fluxtah.ask.api.markdown.AnsiMarkdownRenderer 12 | import com.fluxtah.ask.api.markdown.MarkdownParser 13 | import com.fluxtah.ask.api.printers.AskResponsePrinter 14 | import com.fluxtah.ask.api.store.user.UserProperties 15 | 16 | class ThreadRecall( 17 | private val assistantsApi: AssistantsApi, 18 | private val userProperties: UserProperties, 19 | private val printer: AskResponsePrinter 20 | ) : Command() { 21 | override val requiresApiKey: Boolean = true 22 | override suspend fun execute(args: List) { 23 | val threadId = userProperties.getThreadId() 24 | if (threadId.isEmpty()) { 25 | printer.printMessage("You need to create a thread first. Use /thread-new") 26 | return 27 | } 28 | val messages = assistantsApi.messages.listMessages(threadId) 29 | printer 30 | .begin() 31 | .println() 32 | .println("-- Thread Recall $threadId --") 33 | .println() 34 | .apply { 35 | messages.data.reversed().forEach { message -> 36 | if (message.role == "user") { 37 | print("${green("ask ➜")} ") 38 | message.content.forEach { content -> 39 | println(content.text.value) 40 | } 41 | } else { 42 | println("\u001B[0m") 43 | message.content.forEach { content -> 44 | val markdownParser = MarkdownParser(content.text.value) 45 | val ansiMarkdown = AnsiMarkdownRenderer().render(markdownParser.parse()) 46 | println(ansiMarkdown) 47 | } 48 | println() 49 | } 50 | println() 51 | } 52 | } 53 | .end() 54 | } 55 | } -------------------------------------------------------------------------------- /ask-api/src/main/kotlin/com/fluxtah/ask/api/commanding/commands/ThreadRename.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024 Ian Warwick 3 | * Released under the MIT license 4 | * https://opensource.org/licenses/MIT 5 | */ 6 | 7 | package com.fluxtah.ask.api.commanding.commands 8 | 9 | import com.fluxtah.ask.api.printers.AskResponsePrinter 10 | import com.fluxtah.ask.api.repository.ThreadRepository 11 | 12 | class ThreadRename( 13 | private val threadRepository: ThreadRepository, 14 | private val responsePrinter: AskResponsePrinter 15 | ) : Command() { 16 | override val requiresApiKey: Boolean = false 17 | override suspend fun execute(args: List) { 18 | if (args.size != 2) { 19 | responsePrinter.printMessage("Invalid number of arguments for /thread-rename, expected a thread ID and new title following the command") 20 | return 21 | } 22 | 23 | val threadId = args[0] 24 | val newTitle = args[1] 25 | threadRepository.renameThread(threadId, newTitle) 26 | } 27 | } -------------------------------------------------------------------------------- /ask-api/src/main/kotlin/com/fluxtah/ask/api/commanding/commands/ToShortDateTimeString.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024 Ian Warwick 3 | * Released under the MIT license 4 | * https://opensource.org/licenses/MIT 5 | */ 6 | 7 | package com.fluxtah.ask.api.commanding.commands 8 | 9 | import java.util.* 10 | 11 | fun Long.toShortDateTimeString(): String { 12 | val dt = Date(this * 1000) 13 | return String.format("%tF %) { 19 | val number = args.firstOrNull()?.toIntOrNull() 20 | if (number == null) { 21 | val currentValue = userProperties.getTruncateLastMessages() 22 | printer.printMessage("Current truncate last messages value: $currentValue. Usage: /truncate-last-messages ") 23 | return 24 | } 25 | 26 | if (number < 0) { 27 | printer.printMessage("Invalid number. Please provide a number between 0 and a positive integer.") 28 | } else { 29 | userProperties.setTruncateLastMessages(number) 30 | userProperties.save() 31 | printer.printMessage("Set the truncate last messages value to: $number") 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /ask-api/src/main/kotlin/com/fluxtah/ask/api/commanding/commands/UnInstallAssistant.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024 Ian Warwick 3 | * Released under the MIT license 4 | * https://opensource.org/licenses/MIT 5 | */ 6 | 7 | package com.fluxtah.ask.api.commanding.commands 8 | 9 | import com.fluxtah.ask.api.assistants.AssistantInstallRepository 10 | import com.fluxtah.ask.api.assistants.AssistantRegistry 11 | import com.fluxtah.ask.api.printers.AskResponsePrinter 12 | 13 | class UnInstallAssistant( 14 | private val assistantRegistry: AssistantRegistry, 15 | private val assistantInstallRepository: AssistantInstallRepository, 16 | private val printer: AskResponsePrinter 17 | ) : Command() { 18 | override val requiresApiKey: Boolean = true 19 | override suspend fun execute(args: List) { 20 | 21 | if (args.size != 1) { 22 | printer.printMessage("Invalid number of arguments for /assistant-uninstall, expected an assistant ID following the command") 23 | return 24 | } 25 | 26 | val assistantId = args.first() 27 | 28 | val def = assistantRegistry.getAssistantById(assistantId) 29 | 30 | if (def == null) { 31 | printer.printMessage("Assistant not found: @$assistantId") 32 | return 33 | } 34 | 35 | val assistantInstallRecord = assistantInstallRepository.getAssistantInstallRecord(assistantId) 36 | 37 | if (assistantInstallRecord == null) { 38 | printer.printMessage("Assistant @${def.id} ${def.version} not installed.") 39 | return 40 | } 41 | 42 | if (assistantInstallRepository.uninstall(assistantInstallRecord)) { 43 | printer.printMessage("Uninstalled assistant: @${def.id} ${assistantInstallRecord.version} ${assistantInstallRecord.installId}") 44 | } else { 45 | printer.printMessage("Failed to uninstall assistant: @${def.id} ${assistantInstallRecord.version} ${assistantInstallRecord.installId}") 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /ask-api/src/main/kotlin/com/fluxtah/ask/api/commanding/commands/VoiceAutoSendCommand.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024 Ian Warwick 3 | * Released under the MIT license 4 | * https://opensource.org/licenses/MIT 5 | */ 6 | 7 | package com.fluxtah.ask.api.commanding.commands 8 | 9 | import com.fluxtah.ask.api.store.user.UserProperties 10 | import com.fluxtah.ask.api.printers.AskResponsePrinter 11 | 12 | class VoiceAutoSendCommand( 13 | private val userProperties: UserProperties, 14 | private val responsePrinter: AskResponsePrinter 15 | ) : Command() { 16 | override val requiresApiKey: Boolean = false 17 | 18 | override suspend fun execute(args: List) { 19 | val enabled = userProperties.getAutoSendVoice() 20 | userProperties.setAutoSendVoice(!enabled) 21 | responsePrinter.printMessage("Voice auto-send mode is now ${if (!enabled) "enabled" else "disabled"}.") 22 | } 23 | } 24 | 25 | -------------------------------------------------------------------------------- /ask-api/src/main/kotlin/com/fluxtah/ask/api/commanding/commands/WhichAssistant.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024 Ian Warwick 3 | * Released under the MIT license 4 | * https://opensource.org/licenses/MIT 5 | */ 6 | 7 | package com.fluxtah.ask.api.commanding.commands 8 | 9 | import com.fluxtah.ask.api.assistants.AssistantInstallRepository 10 | import com.fluxtah.ask.api.assistants.AssistantRegistry 11 | import com.fluxtah.ask.api.printers.AskResponsePrinter 12 | import com.fluxtah.ask.api.store.user.UserProperties 13 | 14 | class WhichAssistant( 15 | private val userProperties: UserProperties, 16 | private val assistantRegistry: AssistantRegistry, 17 | private val assistantInstallRepository: AssistantInstallRepository, 18 | private val printer: AskResponsePrinter, 19 | ) : Command() { 20 | override val requiresApiKey: Boolean = false 21 | override suspend fun execute(args: List) { 22 | val assistantId = userProperties.getAssistantId() 23 | if (assistantId.isEmpty()) { 24 | printer.printMessage("You need to select an assistant first. Use /assistant-list to see available assistants") 25 | return 26 | } 27 | 28 | assistantRegistry.getAssistantById(assistantId)?.let { 29 | val installedAssistants = assistantInstallRepository.getAssistantInstallRecords() 30 | val installed = installedAssistants.find { record -> record.id == it.id } != null 31 | printer.printMessage("@${it.id} - ${it.name} ${it.version}, ${it.model}, installed: $installed") 32 | } ?: printer.printMessage("Assistant not found") 33 | } 34 | } 35 | 36 | -------------------------------------------------------------------------------- /ask-api/src/main/kotlin/com/fluxtah/ask/api/commanding/commands/WhichModel.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024 Ian Warwick 3 | * Released under the MIT license 4 | * https://opensource.org/licenses/MIT 5 | */ 6 | 7 | package com.fluxtah.ask.api.commanding.commands 8 | 9 | import com.fluxtah.ask.api.printers.AskResponsePrinter 10 | import com.fluxtah.ask.api.store.user.UserProperties 11 | 12 | class WhichModel( 13 | private val userProperties: UserProperties, 14 | private val printer: AskResponsePrinter 15 | ) : Command() { 16 | override val requiresApiKey: Boolean = false 17 | override suspend fun execute(args: List) { 18 | val modelId = userProperties.getModel() 19 | if (modelId.isEmpty()) { 20 | printer.printMessage("No model set, all targeted assistants will use their default model") 21 | return 22 | } 23 | printer.printMessage("Model set to $modelId, all targeted assistants will use this model until you /model-clear") 24 | } 25 | } -------------------------------------------------------------------------------- /ask-api/src/main/kotlin/com/fluxtah/ask/api/commanding/commands/WhichThread.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024 Ian Warwick 3 | * Released under the MIT license 4 | * https://opensource.org/licenses/MIT 5 | */ 6 | 7 | package com.fluxtah.ask.api.commanding.commands 8 | 9 | import com.fluxtah.ask.api.printers.AskResponsePrinter 10 | import com.fluxtah.ask.api.store.user.UserProperties 11 | 12 | class WhichThread( 13 | private val userProperties: UserProperties, 14 | private val printer: AskResponsePrinter 15 | ) : Command() { 16 | override val requiresApiKey: Boolean = false 17 | override suspend fun execute(args: List) { 18 | printer.printMessage("Current thread: ${userProperties.getThreadId().ifEmpty { "None" }}") 19 | } 20 | } -------------------------------------------------------------------------------- /ask-api/src/main/kotlin/com/fluxtah/ask/api/di/AskApiModule.kt: -------------------------------------------------------------------------------- 1 | package com.fluxtah.ask.api.di 2 | 3 | import AudioPlayer 4 | import com.fluxtah.ask.api.AssistantRunManager 5 | import com.fluxtah.ask.api.AssistantRunner 6 | import com.fluxtah.ask.api.InputHandler 7 | import com.fluxtah.ask.api.assistants.AssistantInstallRepository 8 | import com.fluxtah.ask.api.assistants.AssistantRegistry 9 | import com.fluxtah.ask.api.audio.AudioRecorder 10 | import com.fluxtah.ask.api.audio.TextToSpeechPlayer 11 | import com.fluxtah.ask.api.clients.openai.assistants.AssistantsApi 12 | import com.fluxtah.ask.api.clients.openai.audio.AudioApi 13 | import com.fluxtah.ask.api.repository.ThreadRepository 14 | import com.fluxtah.ask.api.store.PropertyStore 15 | import com.fluxtah.ask.api.store.user.UserProperties 16 | import com.fluxtah.ask.api.tools.fn.FunctionInvoker 17 | import com.fluxtah.askpluginsdk.logging.AskLogger 18 | import org.koin.core.module.dsl.singleOf 19 | import org.koin.dsl.module 20 | 21 | val askApiModule = module { 22 | single { UserProperties(PropertyStore("user.properties")) } 23 | singleOf(::AskLogger) 24 | single { 25 | AssistantsApi( 26 | apiKeyProvider = { get().getOpenaiApiKey() } 27 | ) 28 | } 29 | single { 30 | AudioApi( 31 | apiKeyProvider = { get().getOpenaiApiKey() } 32 | ) 33 | } 34 | singleOf(::AssistantRegistry) 35 | singleOf(::AssistantInstallRepository) 36 | singleOf(::ThreadRepository) 37 | singleOf(::FunctionInvoker) 38 | singleOf(::AssistantRunner) 39 | singleOf(::AssistantRunManager) 40 | singleOf(::AudioPlayer) 41 | singleOf(::TextToSpeechPlayer) 42 | singleOf(::AudioRecorder) 43 | singleOf(::InputHandler) 44 | 45 | 46 | } -------------------------------------------------------------------------------- /ask-api/src/main/kotlin/com/fluxtah/ask/api/di/CommandFactoryModule.kt: -------------------------------------------------------------------------------- 1 | package com.fluxtah.ask.api.di 2 | 3 | import com.fluxtah.ask.api.commanding.CommandFactory 4 | import com.fluxtah.ask.api.commanding.commands.Clear 5 | import com.fluxtah.ask.api.commanding.commands.ClearModel 6 | import com.fluxtah.ask.api.commanding.commands.DeleteThread 7 | import com.fluxtah.ask.api.commanding.commands.EnableTalkCommand 8 | import com.fluxtah.ask.api.commanding.commands.Exit 9 | import com.fluxtah.ask.api.commanding.commands.GetAssistant 10 | import com.fluxtah.ask.api.commanding.commands.GetThread 11 | import com.fluxtah.ask.api.commanding.commands.Help 12 | import com.fluxtah.ask.api.commanding.commands.InstallAssistant 13 | import com.fluxtah.ask.api.commanding.commands.ListAssistants 14 | import com.fluxtah.ask.api.commanding.commands.ListMessages 15 | import com.fluxtah.ask.api.commanding.commands.ListRunSteps 16 | import com.fluxtah.ask.api.commanding.commands.ListRuns 17 | import com.fluxtah.ask.api.commanding.commands.ListThreads 18 | import com.fluxtah.ask.api.commanding.commands.MaxCompletionTokens 19 | import com.fluxtah.ask.api.commanding.commands.MaxPromptTokens 20 | import com.fluxtah.ask.api.commanding.commands.PlayTts 21 | import com.fluxtah.ask.api.commanding.commands.RecordVoice 22 | import com.fluxtah.ask.api.commanding.commands.RecoverRun 23 | import com.fluxtah.ask.api.commanding.commands.ReinstallAssistant 24 | import com.fluxtah.ask.api.commanding.commands.SetLogLevel 25 | import com.fluxtah.ask.api.commanding.commands.SetModel 26 | import com.fluxtah.ask.api.commanding.commands.SetOpenAiApiKey 27 | import com.fluxtah.ask.api.commanding.commands.ShellExec 28 | import com.fluxtah.ask.api.commanding.commands.ShowHttpLog 29 | import com.fluxtah.ask.api.commanding.commands.SkipTts 30 | import com.fluxtah.ask.api.commanding.commands.SwitchThread 31 | import com.fluxtah.ask.api.commanding.commands.ThreadNew 32 | import com.fluxtah.ask.api.commanding.commands.ThreadRecall 33 | import com.fluxtah.ask.api.commanding.commands.ThreadRename 34 | import com.fluxtah.ask.api.commanding.commands.TruncateLastMessages 35 | import com.fluxtah.ask.api.commanding.commands.UnInstallAssistant 36 | import com.fluxtah.ask.api.commanding.commands.VoiceAutoSendCommand 37 | import com.fluxtah.ask.api.commanding.commands.WhichAssistant 38 | import com.fluxtah.ask.api.commanding.commands.WhichModel 39 | import com.fluxtah.ask.api.commanding.commands.WhichThread 40 | import org.koin.dsl.module 41 | 42 | val commandFactoryModule = module { 43 | single { 44 | CommandFactory(get(), get()).apply { 45 | registerCommand( 46 | name = "max-completion-tokens", 47 | description = " - Set the max completion tokens value" 48 | ) 49 | registerCommand( 50 | name = "max-prompt-tokens", 51 | description = " - Set the max prompt tokens value" 52 | ) 53 | registerCommand( 54 | name = "help", 55 | description = "Show this help" 56 | ) 57 | registerCommand( 58 | name = "exit", 59 | description = "Exits ask" 60 | ) 61 | registerCommand( 62 | name = "clear", 63 | description = "Clears the screen" 64 | ) 65 | registerCommand( 66 | name = "truncate-last-messages", 67 | description = " - Set or get the truncate last messages value" 68 | ) 69 | registerCommand( 70 | name = "assistant-install", 71 | description = " Installs an assistant" 72 | ) 73 | registerCommand( 74 | name = "assistant-uninstall", 75 | description = " Uninstalls an assistant" 76 | ) 77 | registerCommand( 78 | name = "assistant-list", 79 | description = "Displays all available assistants", 80 | ) 81 | registerCommand( 82 | name = "assistant-which", 83 | description = "Displays the current assistant thread" 84 | ) 85 | registerCommand( 86 | name = "assistant-info", 87 | description = " Displays info for the assistant" 88 | ) 89 | registerCommand( 90 | name = "model", 91 | description = " Set model override affecting all assistants (gpt-3.5-turbo-16k, gpt-4-turbo, etc.)" 92 | ) 93 | registerCommand( 94 | name = "model-clear", 95 | description = "Clears the current model override" 96 | ) 97 | registerCommand( 98 | name = "model-which", 99 | description = "Displays the current model override" 100 | ) 101 | registerCommand( 102 | name = "thread-new", 103 | description = "Creates a new assistant thread" 104 | ) 105 | registerCommand( 106 | name = "thread-which", 107 | description = "Displays the current assistant thread" 108 | ) 109 | registerCommand( 110 | name = "thread-info", 111 | description = " - Displays the assistant thread", 112 | ) 113 | registerCommand( 114 | name = "thread-delete", 115 | description = " - Delete the thread by the given id" 116 | ) 117 | 118 | registerCommand( 119 | name = "thread-list", 120 | description = "Lists all assistant threads" 121 | ) 122 | registerCommand( 123 | name = "thread-switch", 124 | description = " - Switches to the given thread" 125 | ) 126 | registerCommand( 127 | name = "thread-rename", 128 | description = " - Renames the given thread" 129 | ) 130 | registerCommand( 131 | name = "thread-recall", 132 | description = "Recalls the current assistant thread messages (prints out message history)" 133 | ) 134 | registerCommand( 135 | name = "message-list", 136 | description = "Lists all messages in the current assistant thread" 137 | ) 138 | registerCommand( 139 | name = "run-list", 140 | description = "Lists all runs in the current assistant thread" 141 | ) 142 | registerCommand( 143 | name = "run-step-list", 144 | description = "Lists all run steps in the current assistant thread" 145 | ) 146 | registerCommand( 147 | name = "run-recover", 148 | description = "Recovers the last run in the current assistant thread" 149 | ) 150 | registerCommand( 151 | name = "http-log", 152 | description = "Displays the last 10 HTTP requests" 153 | ) 154 | registerCommand( 155 | name = "set-key", 156 | description = " - Set your openai api key" 157 | ) 158 | 159 | registerCommand( 160 | name = "log-level", 161 | description = " Set the log level (ERROR, DEBUG, INFO, OFF)" 162 | ) 163 | 164 | registerCommand( 165 | name = "exec", 166 | description = " - Executes a shell command for convenience" 167 | ) 168 | registerCommand( 169 | name = "assistant-reinstall", 170 | description = " Reinstall an assistant" 171 | ) 172 | 173 | registerCommand( 174 | name = "voice-auto-send", 175 | description = "Toggles auto-send mode for voice commands" 176 | ) 177 | registerCommand( 178 | name = "r", 179 | description = "Start recording audio" 180 | ) 181 | registerCommand( 182 | name = "s", 183 | description = "Skip the current text-to-speech segment" 184 | ) 185 | registerCommand( 186 | name = "p", 187 | description = "Play the current text-to-speech segment" 188 | ) 189 | registerCommand( 190 | name = "talk", 191 | description = "Stop the current text-to-speech segment" 192 | ) 193 | } 194 | } 195 | } -------------------------------------------------------------------------------- /ask-api/src/main/kotlin/com/fluxtah/ask/api/di/CommandsModule.kt: -------------------------------------------------------------------------------- 1 | package com.fluxtah.ask.api.di 2 | 3 | import com.fluxtah.ask.api.commanding.commands.Clear 4 | import com.fluxtah.ask.api.commanding.commands.ClearModel 5 | import com.fluxtah.ask.api.commanding.commands.DeleteThread 6 | import com.fluxtah.ask.api.commanding.commands.EnableTalkCommand 7 | import com.fluxtah.ask.api.commanding.commands.Exit 8 | import com.fluxtah.ask.api.commanding.commands.GetAssistant 9 | import com.fluxtah.ask.api.commanding.commands.GetThread 10 | import com.fluxtah.ask.api.commanding.commands.Help 11 | import com.fluxtah.ask.api.commanding.commands.InstallAssistant 12 | import com.fluxtah.ask.api.commanding.commands.ListAssistants 13 | import com.fluxtah.ask.api.commanding.commands.ListMessages 14 | import com.fluxtah.ask.api.commanding.commands.ListRunSteps 15 | import com.fluxtah.ask.api.commanding.commands.ListRuns 16 | import com.fluxtah.ask.api.commanding.commands.ListThreads 17 | import com.fluxtah.ask.api.commanding.commands.MaxCompletionTokens 18 | import com.fluxtah.ask.api.commanding.commands.MaxPromptTokens 19 | import com.fluxtah.ask.api.commanding.commands.PlayTts 20 | import com.fluxtah.ask.api.commanding.commands.RecordVoice 21 | import com.fluxtah.ask.api.commanding.commands.RecoverRun 22 | import com.fluxtah.ask.api.commanding.commands.ReinstallAssistant 23 | import com.fluxtah.ask.api.commanding.commands.SetLogLevel 24 | import com.fluxtah.ask.api.commanding.commands.SetModel 25 | import com.fluxtah.ask.api.commanding.commands.SetOpenAiApiKey 26 | import com.fluxtah.ask.api.commanding.commands.ShellExec 27 | import com.fluxtah.ask.api.commanding.commands.ShowHttpLog 28 | import com.fluxtah.ask.api.commanding.commands.SkipTts 29 | import com.fluxtah.ask.api.commanding.commands.SwitchThread 30 | import com.fluxtah.ask.api.commanding.commands.ThreadNew 31 | import com.fluxtah.ask.api.commanding.commands.ThreadRecall 32 | import com.fluxtah.ask.api.commanding.commands.ThreadRename 33 | import com.fluxtah.ask.api.commanding.commands.TruncateLastMessages 34 | import com.fluxtah.ask.api.commanding.commands.UnInstallAssistant 35 | import com.fluxtah.ask.api.commanding.commands.VoiceAutoSendCommand 36 | import com.fluxtah.ask.api.commanding.commands.WhichAssistant 37 | import com.fluxtah.ask.api.commanding.commands.WhichModel 38 | import com.fluxtah.ask.api.commanding.commands.WhichThread 39 | import org.koin.core.module.dsl.factoryOf 40 | import org.koin.dsl.module 41 | 42 | val commandsModule = module { 43 | factoryOf(::MaxCompletionTokens) 44 | factoryOf(::MaxPromptTokens) 45 | factoryOf(::Help) 46 | factoryOf(::Exit) 47 | factoryOf(::Clear) 48 | factoryOf(::TruncateLastMessages) 49 | factoryOf(::InstallAssistant) 50 | factoryOf(::UnInstallAssistant) 51 | factoryOf(::ListAssistants) 52 | factoryOf(::WhichAssistant) 53 | factoryOf(::GetAssistant) 54 | factoryOf(::SetModel) 55 | factoryOf(::ClearModel) 56 | factoryOf(::WhichModel) 57 | factoryOf(::ThreadNew) 58 | factoryOf(::WhichThread) 59 | factoryOf(::GetThread) 60 | factoryOf(::DeleteThread) 61 | factoryOf(::ListThreads) 62 | factoryOf(::SwitchThread) 63 | factoryOf(::ThreadRename) 64 | factoryOf(::ThreadRecall) 65 | factoryOf(::ListMessages) 66 | factoryOf(::ListRuns) 67 | factoryOf(::ListRunSteps) 68 | factoryOf(::RecoverRun) 69 | factoryOf(::ShowHttpLog) 70 | factoryOf(::SetOpenAiApiKey) 71 | factoryOf(::SetLogLevel) 72 | factoryOf(::ShellExec) 73 | factoryOf(::ReinstallAssistant) 74 | factoryOf(::VoiceAutoSendCommand) 75 | factoryOf(::RecordVoice) 76 | factoryOf(::SkipTts) 77 | factoryOf(::PlayTts) 78 | factoryOf(::EnableTalkCommand) 79 | } 80 | -------------------------------------------------------------------------------- /ask-api/src/main/kotlin/com/fluxtah/ask/api/kotlin/KotlinFileRepository.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024 Ian Warwick 3 | * Released under the MIT license 4 | * https://opensource.org/licenses/MIT 5 | */ 6 | 7 | package com.fluxtah.ask.api.kotlin 8 | 9 | import org.jetbrains.kotlin.cli.common.CLIConfigurationKeys 10 | import org.jetbrains.kotlin.cli.common.messages.MessageCollector 11 | import org.jetbrains.kotlin.cli.jvm.compiler.EnvironmentConfigFiles 12 | import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment 13 | import org.jetbrains.kotlin.com.intellij.openapi.Disposable 14 | import org.jetbrains.kotlin.com.intellij.openapi.util.Disposer 15 | import org.jetbrains.kotlin.config.CompilerConfiguration 16 | import org.jetbrains.kotlin.psi.KtFile 17 | import org.jetbrains.kotlin.com.intellij.psi.PsiFileFactory 18 | import org.jetbrains.kotlin.idea.KotlinFileType 19 | import java.nio.file.Files 20 | import java.nio.file.Paths 21 | 22 | class KotlinFileRepository { 23 | private val disposable: Disposable = Disposer.newDisposable() 24 | private val environment = setupKotlinEnvironment() 25 | 26 | private fun setupKotlinEnvironment(): KotlinCoreEnvironment { 27 | val configuration = CompilerConfiguration().apply { 28 | put(CLIConfigurationKeys.MESSAGE_COLLECTOR_KEY, MessageCollector.NONE) 29 | } 30 | return KotlinCoreEnvironment.createForProduction(disposable, configuration, EnvironmentConfigFiles.JVM_CONFIG_FILES) 31 | } 32 | 33 | fun parseFile(filePath: String): KtFile? { 34 | val fileContent = try { 35 | Files.readString(Paths.get(filePath)) 36 | } catch (e: Exception) { 37 | println("Error reading file: $e") 38 | return null 39 | } 40 | 41 | val psiFile = PsiFileFactory.getInstance(environment.project).createFileFromText("temp.kt", KotlinFileType.INSTANCE, fileContent) 42 | return psiFile as? KtFile 43 | } 44 | 45 | fun disposeResources() { 46 | Disposer.dispose(disposable) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /ask-api/src/main/kotlin/com/fluxtah/ask/api/markdown/AnsiMarkdownRenderer.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024 Ian Warwick 3 | * Released under the MIT license 4 | * https://opensource.org/licenses/MIT 5 | */ 6 | package com.fluxtah.ask.api.markdown 7 | 8 | import com.fluxtah.ask.api.ansi.blue 9 | import com.fluxtah.ask.api.ansi.cyan 10 | 11 | class AnsiMarkdownRenderer { 12 | fun render(tokens: List): String { 13 | val builder = StringBuilder() 14 | tokens.forEach { token -> 15 | when (token) { 16 | is Token.CodeBlock -> { 17 | builder.appendLine(cyan(token.content.trim())) 18 | } 19 | 20 | is Token.Text -> { 21 | builder.append(token.content) 22 | } 23 | 24 | is Token.Code -> { 25 | builder.append(cyan(token.content)) 26 | } 27 | 28 | is Token.Bold -> { 29 | builder.append("\u001B[1m${token.content}\u001B[0m") 30 | } 31 | } 32 | } 33 | 34 | return builder.toString() 35 | } 36 | } -------------------------------------------------------------------------------- /ask-api/src/main/kotlin/com/fluxtah/ask/api/markdown/MarkdownParser.kt: -------------------------------------------------------------------------------- 1 | package com.fluxtah.ask.api.markdown 2 | 3 | class MarkdownParser(private val input: String) { 4 | private val tokens = mutableListOf() 5 | private var pos = 0 // Manually controlled position variable 6 | private val buffer = StringBuilder() 7 | fun parse(): List { 8 | while (pos < input.length) { 9 | val c = input[pos] 10 | buffer.append(c) 11 | 12 | when { 13 | buffer.endsWith("```") -> { 14 | tokens.add(Token.Text(buffer.dropLast(3).toString())) 15 | buffer.clear() 16 | val language = readLanguage() 17 | readCodeBlock(language) 18 | } 19 | input.startsWith("`", pos) && !input.startsWith("``", pos) -> { 20 | tokens.add(Token.Text(buffer.dropLast(1).toString())) 21 | buffer.clear() 22 | pos++ 23 | readInlineCodeSegment() 24 | } 25 | buffer.endsWith("**") -> { 26 | tokens.add(Token.Text(buffer.dropLast(2).toString())) 27 | buffer.clear() 28 | pos++ 29 | readBoldSegment() 30 | } 31 | } 32 | 33 | pos++ // Increment position after handling the current character 34 | } 35 | 36 | if(buffer.isNotEmpty()) { 37 | tokens.add(Token.Text(buffer.toString())) 38 | buffer.clear() 39 | } 40 | 41 | return tokens 42 | } 43 | 44 | private fun readBoldSegment() { 45 | val bold = StringBuilder() 46 | while (pos < input.length) { 47 | val c = input[pos] 48 | bold.append(c) 49 | if (bold.endsWith("**")) { 50 | tokens.add(Token.Bold(bold.dropLast(2).toString())) 51 | buffer.clear() 52 | break 53 | } 54 | pos++ 55 | } 56 | } 57 | 58 | 59 | private fun readInlineCodeSegment() { 60 | while (pos < input.length) { 61 | val c = input[pos] 62 | buffer.append(c) 63 | if (buffer.endsWith("`")) { 64 | tokens.add(Token.Code(buffer.dropLast(1).toString())) 65 | buffer.clear() 66 | break 67 | } 68 | pos++ 69 | } 70 | } 71 | 72 | fun readLanguage(): String { 73 | val language = StringBuilder() 74 | while (pos < input.length) { 75 | val c = input[pos] 76 | if (c == '\n') { 77 | break 78 | } 79 | language.append(c) 80 | pos++ 81 | } 82 | return language.toString() 83 | } 84 | 85 | fun readCodeBlock(language: String) { 86 | val codeBlock = StringBuilder() 87 | while (pos < input.length) { 88 | val c = input[pos] 89 | codeBlock.append(c) 90 | if (codeBlock.endsWith("```")) { 91 | tokens.add(Token.CodeBlock(language, codeBlock.dropLast(3).toString())) 92 | break 93 | } 94 | pos++ 95 | } 96 | } 97 | } 98 | 99 | 100 | -------------------------------------------------------------------------------- /ask-api/src/main/kotlin/com/fluxtah/ask/api/markdown/Token.kt: -------------------------------------------------------------------------------- 1 | package com.fluxtah.ask.api.markdown 2 | 3 | sealed class Token { 4 | data class Text(val content: String) : Token() 5 | data class CodeBlock(val language: String?, val content: String) : Token() 6 | data class Code(val content: String) : Token() 7 | data class Bold(val content: String) : Token() 8 | } 9 | -------------------------------------------------------------------------------- /ask-api/src/main/kotlin/com/fluxtah/ask/api/plugins/AskPluginLoader.kt: -------------------------------------------------------------------------------- 1 | package com.fluxtah.ask.api.plugins 2 | 3 | import com.fluxtah.askpluginsdk.AskPlugin 4 | import com.fluxtah.askpluginsdk.AssistantDefinition 5 | import com.fluxtah.askpluginsdk.CreateAssistantDefinitionsConfig 6 | import com.fluxtah.askpluginsdk.io.getUserConfigDirectory 7 | import com.fluxtah.askpluginsdk.logging.AskLogger 8 | import java.io.File 9 | import java.net.URLClassLoader 10 | import java.util.* 11 | 12 | class AskPluginLoader(private val logger: AskLogger) { 13 | fun loadPlugins(): List { 14 | val plugins = mutableListOf() 15 | val pluginsDir = File(getUserConfigDirectory(), "plugins") 16 | if (!pluginsDir.exists()) { 17 | pluginsDir.mkdirs() 18 | } 19 | val urls = 20 | pluginsDir.listFiles { file -> file.path.endsWith(".jar") }?.map { it.toURI().toURL() }?.toTypedArray() 21 | val classLoader = URLClassLoader(urls, Thread.currentThread().contextClassLoader) 22 | 23 | val services = ServiceLoader.load(AskPlugin::class.java, classLoader) 24 | for (plugin in services) { 25 | plugin.createAssistantDefinitions(CreateAssistantDefinitionsConfig(logger)).forEach { 26 | plugins.add(it) 27 | } 28 | } 29 | 30 | return plugins 31 | } 32 | 33 | fun loadPlugin(file: File): AssistantDefinition { 34 | val plugins = mutableListOf() 35 | val urls = listOf(file).map { it.toURI().toURL() }.toTypedArray() 36 | val classLoader = URLClassLoader(urls, Thread.currentThread().contextClassLoader) 37 | 38 | val services = ServiceLoader.load(AskPlugin::class.java, classLoader) 39 | for (plugin in services) { 40 | plugin.createAssistantDefinitions(CreateAssistantDefinitionsConfig(logger)).forEach { 41 | plugins.add(it) 42 | } 43 | } 44 | 45 | return plugins.first() 46 | } 47 | } -------------------------------------------------------------------------------- /ask-api/src/main/kotlin/com/fluxtah/ask/api/printers/AskConsoleResponsePrinter.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024 Ian Warwick 3 | * Released under the MIT license 4 | * https://opensource.org/licenses/MIT 5 | */ 6 | 7 | package com.fluxtah.ask.api.printers 8 | 9 | class ConsolePrinterContext(val printer: AskConsoleResponsePrinter) : PrinterContext { 10 | override fun println(line: String?): PrinterContext { 11 | kotlin.io.println(line ?: "") 12 | return this 13 | } 14 | 15 | override fun print(text: String): PrinterContext { 16 | kotlin.io.print(text) 17 | return this 18 | } 19 | 20 | override fun end() { 21 | printer.currentContext = null 22 | } 23 | } 24 | 25 | class AskConsoleResponsePrinter : AskResponsePrinter { 26 | var currentContext: ConsolePrinterContext? = null 27 | 28 | override fun begin(): PrinterContext { 29 | if (currentContext != null) { 30 | throw IllegalStateException("Printer context already in use, call end() before starting a new context") 31 | } 32 | return ConsolePrinterContext(this) 33 | } 34 | } -------------------------------------------------------------------------------- /ask-api/src/main/kotlin/com/fluxtah/ask/api/printers/AskResponsePrinter.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024 Ian Warwick 3 | * Released under the MIT license 4 | * https://opensource.org/licenses/MIT 5 | */ 6 | 7 | package com.fluxtah.ask.api.printers 8 | 9 | interface AskResponsePrinter { 10 | fun begin(): PrinterContext 11 | fun printMessage(message: String) { 12 | begin().println(message).end() 13 | } 14 | } 15 | 16 | interface PrinterContext { 17 | fun println(line: String? = null): PrinterContext 18 | fun print(text: String): PrinterContext 19 | fun end() 20 | } -------------------------------------------------------------------------------- /ask-api/src/main/kotlin/com/fluxtah/ask/api/repository/ThreadRepository.kt: -------------------------------------------------------------------------------- 1 | package com.fluxtah.ask.api.repository 2 | 3 | import com.fluxtah.askpluginsdk.io.getUserConfigDirectory 4 | import org.jetbrains.exposed.dao.IntEntity 5 | import org.jetbrains.exposed.dao.IntEntityClass 6 | import org.jetbrains.exposed.dao.id.EntityID 7 | import org.jetbrains.exposed.dao.id.IntIdTable 8 | import org.jetbrains.exposed.sql.Database 9 | import org.jetbrains.exposed.sql.SchemaUtils.create 10 | import org.jetbrains.exposed.sql.transactions.transaction 11 | import org.jetbrains.exposed.sql.update 12 | 13 | object Threads : IntIdTable() { 14 | val threadId = varchar("thread_id", 512).index() 15 | val title = varchar("title", 512) 16 | } 17 | 18 | class Thread(id: EntityID) : IntEntity(id) { 19 | companion object : IntEntityClass(Threads) 20 | 21 | var threadId by Threads.threadId 22 | var title by Threads.title 23 | } 24 | 25 | class ThreadRepository { 26 | 27 | init { 28 | val dbPath = getUserConfigDirectory().resolve("ask-api.db").absolutePath 29 | Database.connect("jdbc:sqlite:$dbPath", driver = "org.sqlite.JDBC") 30 | 31 | transaction { 32 | create(Threads) 33 | } 34 | } 35 | 36 | fun createThread(threadId: String, title: String) { 37 | transaction { 38 | Thread.new { 39 | this.threadId = threadId 40 | this.title = title 41 | } 42 | } 43 | } 44 | 45 | fun renameThread(threadId: String, newName: String) { 46 | transaction { 47 | Thread.find { Threads.threadId eq threadId }.firstOrNull()?.let { 48 | Threads.update({ Threads.threadId eq threadId }) { 49 | it[title] = newName 50 | } 51 | } 52 | } 53 | } 54 | 55 | fun listThreads(): List { 56 | return transaction { 57 | Thread.all().toList() 58 | } 59 | } 60 | 61 | fun getThreadById(threadId: String): Thread? { 62 | return transaction { 63 | Thread.find { Threads.threadId eq threadId }.firstOrNull() 64 | } 65 | } 66 | 67 | fun deleteThread(threadId: String) { 68 | transaction { 69 | Thread.find { Threads.threadId eq threadId }.firstOrNull()?.delete() 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /ask-api/src/main/kotlin/com/fluxtah/ask/api/store/PropertyStore.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024 Ian Warwick 3 | * Released under the MIT license 4 | * https://opensource.org/licenses/MIT 5 | */ 6 | 7 | package com.fluxtah.ask.api.store 8 | 9 | import com.fluxtah.askpluginsdk.io.getUserConfigDirectory 10 | import java.io.File 11 | import java.io.FileInputStream 12 | import java.io.FileOutputStream 13 | import java.util.* 14 | 15 | class PropertyStore(private val filename: String) { 16 | private val properties = Properties() 17 | 18 | init { 19 | load() 20 | } 21 | 22 | @Synchronized 23 | fun setProperty(key: String, value: String) { 24 | properties.setProperty(key, value) 25 | save() 26 | } 27 | 28 | @Synchronized 29 | fun getProperty(key: String, defaultValue: String = ""): String { 30 | return properties.getProperty(key, defaultValue) 31 | } 32 | 33 | fun load() { 34 | val file = File(getUserConfigDirectory(), filename) 35 | if (file.exists()) { 36 | FileInputStream(file).use { properties.load(it) } 37 | } 38 | } 39 | 40 | fun save() { 41 | val file = File(getUserConfigDirectory(), filename) 42 | if(!file.exists()) file.createNewFile() 43 | FileOutputStream(file).use { properties.store(it, null) } 44 | } 45 | } -------------------------------------------------------------------------------- /ask-api/src/main/kotlin/com/fluxtah/ask/api/store/user/UserProperties.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024 Ian Warwick 3 | * Released under the MIT license 4 | * https://opensource.org/licenses/MIT 5 | */ 6 | 7 | package com.fluxtah.ask.api.store.user 8 | 9 | import com.fluxtah.ask.api.clients.openai.assistants.model.TruncationStrategy 10 | import com.fluxtah.ask.api.store.PropertyStore 11 | import com.fluxtah.askpluginsdk.logging.LogLevel 12 | 13 | class UserProperties(private val store: PropertyStore) { 14 | companion object { 15 | const val THREAD_ID = "threadId" 16 | const val RUN_ID = "runId" 17 | const val OPENAI_API_KEY = "openaiApiKey" 18 | const val MODEL = "model" 19 | const val ASSISTANT_ID = "assistantId" 20 | const val MAX_PROMPT_TOKENS = "maxPromptTokens" 21 | const val MAX_COMPLETION_TOKENS = "maxCompletionTokens" 22 | const val LOG_LEVEL = "logLevel" 23 | const val TRUNCATE_LAST_MESSAGES = "truncateLastMessages" 24 | const val AUTO_SEND_VOICE = "autoSendVoice" 25 | const val TALK_ENABLED = "talkEnabled" 26 | } 27 | 28 | fun getThreadId(): String { 29 | return store.getProperty(THREAD_ID) 30 | } 31 | 32 | fun setThreadId(threadId: String) { 33 | store.setProperty(THREAD_ID, threadId) 34 | } 35 | 36 | fun getRunId(): String { 37 | return store.getProperty(RUN_ID) 38 | } 39 | 40 | fun setRunId(runId: String) { 41 | store.setProperty(RUN_ID, runId) 42 | } 43 | 44 | fun getOpenaiApiKey(): String { 45 | return store.getProperty(OPENAI_API_KEY) 46 | } 47 | 48 | fun setOpenAiApiKey(openaiApiKey: String) { 49 | store.setProperty(OPENAI_API_KEY, openaiApiKey) 50 | } 51 | 52 | fun getModel(): String { 53 | return store.getProperty(MODEL) 54 | } 55 | 56 | fun setModel(model: String) { 57 | store.setProperty(MODEL, model) 58 | } 59 | 60 | fun getMaxCompletionTokens(): Int { 61 | return store.getProperty(MAX_COMPLETION_TOKENS, "0").toInt() 62 | } 63 | 64 | fun setMaxCompletionTokens(maxCompletionTokens: Int) { 65 | store.setProperty(MAX_COMPLETION_TOKENS, maxCompletionTokens.toString()) 66 | } 67 | 68 | fun getMaxPromptTokens(): Int { 69 | return store.getProperty(MAX_PROMPT_TOKENS, "0").toInt() 70 | } 71 | 72 | fun setMaxPromptTokens(maxPromptTokens: Int) { 73 | store.setProperty(MAX_PROMPT_TOKENS, maxPromptTokens.toString()) 74 | } 75 | 76 | fun getAssistantId(): String { 77 | return store.getProperty(ASSISTANT_ID) 78 | } 79 | 80 | fun setAssistantId(assistantId: String) { 81 | store.setProperty(ASSISTANT_ID, assistantId) 82 | } 83 | 84 | fun getLogLevel(): LogLevel { 85 | return LogLevel.valueOf(store.getProperty(LOG_LEVEL, LogLevel.OFF.name)) 86 | } 87 | 88 | fun setLogLevel(logLevel: LogLevel) { 89 | store.setProperty(LOG_LEVEL, logLevel.name) 90 | } 91 | 92 | fun getTruncateLastMessages(): Int { 93 | return store.getProperty(TRUNCATE_LAST_MESSAGES, "0").toInt() 94 | } 95 | 96 | fun setTruncateLastMessages(value: Int) { 97 | store.setProperty(TRUNCATE_LAST_MESSAGES, value.toString()) 98 | } 99 | 100 | fun getMaxCompletionTokensOrNull() = if (getMaxCompletionTokens() > 0) { 101 | getMaxCompletionTokens() 102 | } else { 103 | null 104 | } 105 | 106 | fun getMaxPromptTokensOrNull() = if (getMaxPromptTokens() > 0) { 107 | getMaxPromptTokens() 108 | } else { 109 | null 110 | } 111 | 112 | fun getTruncationStrategyOrNull() = if (getTruncateLastMessages() > 0) { 113 | TruncationStrategy.LastMessages(getTruncateLastMessages()) 114 | } else { 115 | null 116 | } 117 | 118 | fun getAutoSendVoice(): Boolean { 119 | return store.getProperty(AUTO_SEND_VOICE, "false").toBoolean() 120 | } 121 | 122 | fun setAutoSendVoice(autoSendVoice: Boolean) { 123 | store.setProperty(AUTO_SEND_VOICE, autoSendVoice.toString()) 124 | } 125 | 126 | fun getTalkEnabled(): Boolean { 127 | return store.getProperty(TALK_ENABLED, "false").toBoolean() 128 | } 129 | 130 | fun setTalkEnabled(talkEnabled: Boolean) { 131 | store.setProperty(TALK_ENABLED, talkEnabled.toString()) 132 | } 133 | 134 | fun load() { 135 | store.load() 136 | } 137 | 138 | fun save() { 139 | store.save() 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /ask-api/src/main/kotlin/com/fluxtah/ask/api/tools/fn/FunctionInvoker.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024 Ian Warwick 3 | * Released under the MIT license 4 | * https://opensource.org/licenses/MIT 5 | */ 6 | 7 | package com.fluxtah.ask.api.tools.fn 8 | 9 | import com.fluxtah.ask.api.clients.openai.assistants.model.AssistantRunStepDetails.ToolCalls.ToolCallDetails.FunctionToolCallDetails 10 | import kotlinx.serialization.InternalSerializationApi 11 | import kotlinx.serialization.KSerializer 12 | import kotlinx.serialization.Serializable 13 | import kotlinx.serialization.encodeToString 14 | import kotlinx.serialization.json.* 15 | import kotlinx.serialization.serializer 16 | import kotlinx.serialization.serializerOrNull 17 | import java.lang.reflect.InvocationTargetException 18 | import kotlin.reflect.KClass 19 | import kotlin.reflect.KFunction 20 | import kotlin.reflect.KParameter 21 | import kotlin.reflect.KType 22 | import kotlin.reflect.full.findAnnotation 23 | import kotlin.reflect.full.memberFunctions 24 | import kotlin.reflect.full.memberProperties 25 | import kotlin.reflect.full.primaryConstructor 26 | import kotlin.reflect.jvm.javaConstructor 27 | import kotlin.reflect.jvm.jvmErasure 28 | 29 | class FunctionInvoker { 30 | @OptIn(InternalSerializationApi::class) 31 | fun invokeFunction(targetInstance: T, callDetails: FunctionToolCallDetails): String { 32 | val function = targetInstance::class.memberFunctions.find { it.name == callDetails.function.name } 33 | ?: throw IllegalArgumentException("Function not found: ${callDetails.function.name}") 34 | 35 | try { 36 | val argsMap = Json.decodeFromString>(callDetails.function.arguments.replace("\\$", "$")) 37 | val args = prepareArguments(function, argsMap) 38 | val result = function.call(targetInstance, *args) 39 | 40 | // return result.toString() 41 | // Check if the result is @Serializable 42 | if (result != null && function.returnType.jvmErasure.findAnnotation() != null) { 43 | // If the result type is serializable, encode it to JSON string 44 | val serializer = result::class.serializerOrNull() 45 | return Json.encodeToString(serializer as KSerializer, result) 46 | } else { 47 | // Otherwise, return the result as a string 48 | return result.toString() 49 | } 50 | } catch (e: Exception) { 51 | println("Error decoding arguments: ${callDetails.function.arguments}") 52 | throw e 53 | } 54 | } 55 | 56 | private fun prepareArguments(function: KFunction<*>, argsMap: Map): Array { 57 | return function.parameters.drop(1).map { parameter -> 58 | val paramName = 59 | parameter.name ?: throw IllegalArgumentException("Unnamed parameter in function: ${function.name}") 60 | val jsonElement = 61 | argsMap[paramName] ?: JsonNull 62 | deserializeArgument(jsonElement, parameter.type, parameter) 63 | }.toTypedArray() 64 | } 65 | 66 | fun decodeFromString(jsonString: String, type: KType): Any? { 67 | val serializer = Json.serializersModule.serializer(type) 68 | return Json.decodeFromString(serializer, jsonString) 69 | } 70 | 71 | private fun deserializeArgument(jsonElement: JsonElement, type: KType, parameter: KParameter): Any? { 72 | return when (type.classifier) { 73 | String::class -> jsonElement.jsonPrimitive.safeContentOrNull() ?: "" 74 | Int::class -> jsonElement.jsonPrimitive.safeIntOrNull() ?: 0 75 | Long::class -> jsonElement.jsonPrimitive.safeLongOrNull() ?: 0L 76 | Boolean::class -> jsonElement.jsonPrimitive.safeBooleanOrNull() ?: false 77 | else -> { 78 | decodeFromString(jsonElement.toString(), type) 79 | } 80 | } 81 | } 82 | 83 | private fun JsonPrimitive.safeContentOrNull(): String? = if (isString) content else null 84 | private fun JsonPrimitive.safeIntOrNull(): Int? = try { 85 | int 86 | } catch (e: Exception) { 87 | null 88 | } 89 | 90 | private fun JsonPrimitive.safeLongOrNull(): Long? = try { 91 | long 92 | } catch (e: Exception) { 93 | null 94 | } 95 | 96 | private fun JsonPrimitive.safeBooleanOrNull(): Boolean? = try { 97 | boolean 98 | } catch (e: Exception) { 99 | null 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /ask-api/src/main/kotlin/com/fluxtah/ask/api/tools/fn/FunctionToolGenerator.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024 Ian Warwick 3 | * Released under the MIT license 4 | * https://opensource.org/licenses/MIT 5 | */ 6 | 7 | package com.fluxtah.ask.api.tools.fn 8 | 9 | import com.fluxtah.askpluginsdk.Fun 10 | import com.fluxtah.askpluginsdk.FunParam 11 | import com.fluxtah.ask.api.clients.openai.assistants.model.AssistantTool 12 | import kotlin.reflect.KClass 13 | import kotlin.reflect.KFunction 14 | import kotlin.reflect.KVisibility 15 | import kotlin.reflect.full.findAnnotation 16 | import kotlin.reflect.full.memberFunctions 17 | import kotlin.reflect.full.memberProperties 18 | 19 | class FunctionToolGenerator { 20 | fun generateToolsForInstance(targetInstance: T): List { 21 | return targetInstance::class.memberFunctions.filterPublicAskFun().map { function -> 22 | val description = function.findAnnotation()?.description ?: "" 23 | AssistantTool.FunctionTool( 24 | function = AssistantTool.FunctionTool.FunctionSpec( 25 | name = function.name, 26 | description = description, 27 | parameters = createParametersSpec(function) 28 | ) 29 | ) 30 | } 31 | } 32 | 33 | private fun createPropertiesSpecForDataClass(kClass: KClass<*>): Map { 34 | return kClass.memberProperties.associate { property -> 35 | val description = property.findAnnotation()?.description ?: "" 36 | property.name to AssistantTool.FunctionTool.PropertySpec( 37 | type = when (property.returnType.classifier) { 38 | String::class -> "string" 39 | Int::class -> "integer" 40 | Long::class -> "long" 41 | Boolean::class -> "boolean" 42 | else -> if (property.returnType.classifier is KClass<*>) { 43 | "object" 44 | } else { 45 | throw IllegalArgumentException("Unsupported property type: ${property.returnType} for property: ${property.name}") 46 | } 47 | }, 48 | description = description, 49 | properties = if (property.returnType.classifier is KClass<*>) { 50 | createPropertiesSpecForDataClass(property.returnType.classifier as KClass<*>) 51 | } else { 52 | emptyMap() 53 | } 54 | ) 55 | } 56 | } 57 | 58 | private fun createParametersSpec(function: KFunction<*>): AssistantTool.FunctionTool.ParametersSpec { 59 | val properties = function.parameters.drop(1).associate { parameter -> 60 | val paramDescription = parameter.findAnnotation()?.description ?: "No specific description" 61 | val parameterType = parameter.type.classifier 62 | 63 | (parameter.name ?: throw IllegalArgumentException("Unnamed parameter in function: ${function.name}")) to 64 | if (parameterType is KClass<*> && parameterType.isData) { 65 | AssistantTool.FunctionTool.PropertySpec( 66 | type = "object", 67 | description = paramDescription, 68 | properties = createPropertiesSpecForDataClass(parameterType) 69 | ) 70 | } else { 71 | AssistantTool.FunctionTool.PropertySpec( 72 | type = when (parameterType) { 73 | String::class -> "string" 74 | Int::class -> "integer" 75 | Long::class -> "long" 76 | Boolean::class -> "boolean" 77 | else -> throw IllegalArgumentException("Unsupported parameter type: $parameterType for function: ${function.name}") 78 | }, 79 | description = paramDescription 80 | ) 81 | } 82 | } 83 | return AssistantTool.FunctionTool.ParametersSpec( 84 | type = "object", 85 | properties = properties, 86 | required = properties.keys.toList() 87 | ) 88 | } 89 | } 90 | 91 | private fun Collection>.filterPublicAskFun(): List> { 92 | return filter { 93 | it.visibility == KVisibility.PUBLIC && it.findAnnotation() != null 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /ask-api/src/main/kotlin/com/fluxtah/ask/api/version/VersionUtils.kt: -------------------------------------------------------------------------------- 1 | package com.fluxtah.ask.api.version 2 | 3 | object VersionUtils { 4 | fun isVersionGreater(version1: String, version2: String): Boolean { 5 | val parts1 = version1.split(".") 6 | val parts2 = version2.split(".") 7 | 8 | // Assume all version strings have the same length and format 9 | for (i in parts1.indices) { 10 | val number1 = parts1[i].toInt() 11 | val number2 = parts2[i].toInt() 12 | if (number1 > number2) return true 13 | if (number1 < number2) return false 14 | } 15 | return false 16 | } 17 | } -------------------------------------------------------------------------------- /ask-api/src/test/kotlin/com/fluxtah/ask/api/KotlinFileRepositoryTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024 Ian Warwick 3 | * Released under the MIT license 4 | * https://opensource.org/licenses/MIT 5 | */ 6 | package com.fluxtah.ask.api 7 | 8 | import com.fluxtah.ask.api.kotlin.KotlinFileRepository 9 | import com.fluxtah.askpluginsdk.io.getCurrentWorkingDirectory 10 | import org.jetbrains.kotlin.psi.KtClass 11 | import org.jetbrains.kotlin.psi.KtNamedFunction 12 | import org.jetbrains.kotlin.psi.KtProperty 13 | import org.jetbrains.kotlin.psi.psiUtil.startOffset 14 | import kotlin.test.Test 15 | 16 | class KotlinFileRepositoryTest { 17 | @Test 18 | fun qux() { 19 | val repository = KotlinFileRepository() 20 | val ktFile = repository.parseFile(getCurrentWorkingDirectory() + "/src/main/kotlin/com/fluxtah/ask/api/clients/openai/assistants/AssistantsApi.kt") 21 | 22 | val classes = ktFile!!.declarations.filterIsInstance() 23 | val properties = ktFile.declarations.filterIsInstance() 24 | for (property in properties) { 25 | println("PROP ${property.name} ${property.textRange.startOffset}:${property.textRange.endOffset}") 26 | } 27 | 28 | val functions = ktFile.declarations.filterIsInstance() 29 | for (function in functions) { 30 | println(" FUN ${function.name} ${function.textRange.startOffset}:${function.textRange.endOffset}") 31 | } 32 | 33 | for (klass in classes) { 34 | println("CLASS ${klass.name} ${klass.textRange.startOffset}:${klass.textRange.endOffset}") 35 | val properties = klass.declarations.filterIsInstance() 36 | for (property in properties) { 37 | println(" PROP ${property.name}: ${property.textRange.startOffset}:${property.textRange.endOffset}") 38 | } 39 | val functions = klass.declarations.filterIsInstance() 40 | for (function in functions) { 41 | println(" FUN ${function.name} ${function.textRange.startOffset}:${function.textRange.endOffset}") 42 | //println(" FUN ${function.name}${function.valueParameterList?.text ?: "()"}: ${function.typeReference?.text ?: "Unit"}") 43 | } 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /ask-api/src/test/kotlin/com/fluxtah/ask/api/audio/TextToSpeechPlayerTest.kt: -------------------------------------------------------------------------------- 1 | package com.fluxtah.ask.api.audio 2 | 3 | import AudioPlayer 4 | import com.fluxtah.ask.api.audio.TextToSpeechPlayer.TtsSegment 5 | import com.fluxtah.ask.api.clients.openai.audio.AudioApi 6 | import io.mockk.coEvery 7 | import io.mockk.coVerify 8 | import io.mockk.mockk 9 | import io.mockk.verify 10 | import kotlinx.coroutines.CoroutineScope 11 | import kotlinx.coroutines.Dispatchers 12 | import kotlinx.coroutines.ExperimentalCoroutinesApi 13 | import kotlinx.coroutines.test.advanceUntilIdle 14 | import kotlinx.coroutines.test.runTest 15 | import org.junit.Assert.assertEquals 16 | import org.junit.Assert.assertTrue 17 | import org.junit.Before 18 | import kotlin.test.Test 19 | 20 | @ExperimentalCoroutinesApi 21 | class TextToSpeechPlayerTest { 22 | 23 | private val audioApi: AudioApi = mockk(relaxed = true) 24 | private val audioPlayer: AudioPlayer = mockk(relaxed = true) 25 | 26 | private val coroutineScope = CoroutineScope(Dispatchers.Default) 27 | private val textToSpeechPlayer = TextToSpeechPlayer(audioApi, audioPlayer, coroutineScope) 28 | 29 | @Test 30 | fun `queue should add text segments`() = runTest { 31 | val text = "This is a test text." 32 | textToSpeechPlayer.queue(text) 33 | 34 | assertEquals(1, textToSpeechPlayer.ttsSegments.size) 35 | assertTrue(textToSpeechPlayer.ttsSegments.first() is TtsSegment.Text) 36 | assertTrue((textToSpeechPlayer.ttsSegments.first() as TtsSegment.Text).autoPlay) 37 | } 38 | 39 | @Test 40 | fun `queue should add code block segments`() = runTest { 41 | val text =""" 42 | This is a test text. 43 | 44 | ```kotlin 45 | println('Hello World!') 46 | ``` 47 | 48 | This is more text. 49 | 50 | """.trimIndent() 51 | 52 | textToSpeechPlayer.queue(text) 53 | 54 | assertEquals(3, textToSpeechPlayer.ttsSegments.size) 55 | assertTrue(textToSpeechPlayer.ttsSegments[0] is TtsSegment.Text) 56 | assertTrue(textToSpeechPlayer.ttsSegments[1] is TtsSegment.CodeBlock) 57 | assertTrue(textToSpeechPlayer.ttsSegments[2] is TtsSegment.Text) 58 | } 59 | 60 | @Test 61 | fun `text after code block segments should autoplay`() = runTest { 62 | val text =""" 63 | This is a test text. 64 | 65 | ```kotlin 66 | println('Hello World!') 67 | ``` 68 | 69 | This is more text. 70 | 71 | """.trimIndent() 72 | 73 | textToSpeechPlayer.queue(text) 74 | 75 | assertEquals(3, textToSpeechPlayer.ttsSegments.size) 76 | assertTrue((textToSpeechPlayer.ttsSegments[2] as TtsSegment.Text).autoPlay) 77 | } 78 | 79 | @Test 80 | fun `skipNext should skip the next segment`() = runTest { 81 | val text =""" 82 | This is a test text. 83 | 84 | ```kotlin 85 | println('Hello World!') 86 | ``` 87 | 88 | This is more text. 89 | 90 | """.trimIndent() 91 | 92 | textToSpeechPlayer.queue(text) 93 | 94 | assertEquals(3, textToSpeechPlayer.ttsSegments.size) 95 | 96 | textToSpeechPlayer.skipNext() 97 | 98 | assertEquals(2, textToSpeechPlayer.ttsSegments.size) 99 | } 100 | 101 | @Test 102 | fun `text before code block segments should end with play or skip advice`() = runTest { 103 | val expected = "This is a test text.\n\n$ADVICE_PLAY_OR_SKIP_CODE" 104 | val text =""" 105 | This is a test text. 106 | 107 | ```kotlin 108 | println('Hello World!') 109 | ``` 110 | 111 | This is more text. 112 | 113 | """.trimIndent() 114 | 115 | textToSpeechPlayer.queue(text) 116 | 117 | assertEquals(3, textToSpeechPlayer.ttsSegments.size) 118 | assertEquals(expected, (textToSpeechPlayer.ttsSegments[0] as TtsSegment.Text).content) 119 | 120 | } 121 | 122 | @Test 123 | fun `playNext should play the next segment`() = runTest { 124 | coEvery { audioApi.createSpeech(any()) } returns byteArrayOf(1, 2, 3) 125 | val text = "This is a test text." 126 | textToSpeechPlayer.queue(text) 127 | textToSpeechPlayer.playNext() 128 | 129 | coVerify { audioPlayer.play(any(), any()) } 130 | } 131 | 132 | @Test 133 | fun `playNext should queue autoPlay for next text segment`() = runTest { 134 | coEvery { audioApi.createSpeech(any()) } returns byteArrayOf(1, 2, 3) 135 | val text = "This is a test text." 136 | val code = "println('Hello World!')" 137 | textToSpeechPlayer.queue(text) 138 | textToSpeechPlayer.queue(code) 139 | textToSpeechPlayer.playNext() 140 | 141 | coVerify { audioPlayer.play(any(), any()) } 142 | assertTrue(textToSpeechPlayer.ttsSegments.first().autoPlay) 143 | } 144 | 145 | @Test 146 | fun `stop should stop the audio player`() = runTest { 147 | textToSpeechPlayer.stop() 148 | 149 | verify { audioPlayer.stop() } 150 | } 151 | 152 | @Test 153 | fun `clear should clear the segments`() = runTest { 154 | val text = "This is a test text." 155 | textToSpeechPlayer.queue(text) 156 | textToSpeechPlayer.clear() 157 | 158 | assertTrue(textToSpeechPlayer.ttsSegments.isEmpty()) 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /ask-api/src/test/kotlin/com/fluxtah/ask/api/markdown/MarkdownParserTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024 Ian Warwick 3 | * Released under the MIT license 4 | * https://opensource.org/licenses/MIT 5 | */ 6 | package com.fluxtah.ask.api.markdown 7 | 8 | import kotlin.test.Test 9 | import kotlin.test.assertEquals 10 | 11 | class MarkdownParserTest { 12 | @Test 13 | fun testMarkdownParser() { 14 | // Given 15 | val markdownParser = MarkdownParser(""" 16 | This is a program that prints "Hello, World!" to the console. 17 | 18 | ```kotlin 19 | fun main() { 20 | println("Hello, World!") 21 | } 22 | ``` 23 | 24 | You should save this code in a file named `HelloWorld.kt` and run it using the Kotlin compiler. 25 | """.trimIndent()) 26 | 27 | // When 28 | val parsedMarkdown = markdownParser.parse() 29 | 30 | // Then 31 | // parsedMarkdown.get(0) is the correct type 32 | assertEquals( 33 | "This is a program that prints \"Hello, World!\" to the console.\n\n" 34 | , (parsedMarkdown[0] as Token.Text).content) 35 | assertEquals( 36 | "\nfun main() {\n println(\"Hello, World!\")\n}\n" 37 | , (parsedMarkdown[1] as Token.CodeBlock).content) 38 | assertEquals( 39 | "\n\nYou should save this code in a file named " 40 | , (parsedMarkdown[2] as Token.Text).content) 41 | assertEquals("HelloWorld.kt", (parsedMarkdown[3] as Token.Code).content) 42 | assertEquals(" and run it using the Kotlin compiler.", (parsedMarkdown[4] as Token.Text).content) 43 | } 44 | } -------------------------------------------------------------------------------- /ask-api/src/test/kotlin/com/fluxtah/ask/api/tools/fn/FunctionInvokerTest.kt: -------------------------------------------------------------------------------- 1 | package com.fluxtah.ask.api.tools.fn 2 | 3 | import com.fluxtah.ask.api.clients.openai.assistants.model.AssistantRunStepDetails.ToolCalls.ToolCallDetails.FunctionToolCallDetails 4 | import kotlinx.serialization.Serializable 5 | import kotlinx.serialization.json.Json 6 | import kotlinx.serialization.json.JsonPrimitive 7 | import kotlinx.serialization.json.buildJsonObject 8 | import kotlinx.serialization.json.put 9 | import org.junit.jupiter.api.Assertions.assertEquals 10 | import org.junit.jupiter.api.Test 11 | import kotlin.reflect.KFunction 12 | import kotlin.reflect.full.memberFunctions 13 | 14 | class FunctionInvokerTest { 15 | 16 | class TestTarget { 17 | fun testFunction(param1: String, param2: Int): String { 18 | return "param1: $param1, param2: $param2" 19 | } 20 | 21 | fun testFunction2(param1: String, param2: String): String { 22 | return "param1: $param1, param2: $param2" 23 | } 24 | 25 | @Serializable 26 | data class TestTargetData(val param1: String, val param2: Int) 27 | 28 | fun testFunctionData(data: TestTargetData): String { 29 | return "param1: ${data.param1}, param2: ${data.param2}" 30 | } 31 | 32 | fun testFunctionMixed(param1: String, data: TestTargetData): String { 33 | return "param1: $param1, param2: ${data.param2}" 34 | } 35 | } 36 | 37 | private val functionInvoker = FunctionInvoker() 38 | 39 | @Test 40 | fun `test invokeFunction`() { 41 | val target = TestTarget() 42 | val callDetails = FunctionToolCallDetails( 43 | function = FunctionToolCallDetails.FunctionSpec( 44 | name = "testFunction", 45 | arguments = "{\"param1\":\"test\",\"param2\":42}" 46 | ), 47 | id = "testId" 48 | ) 49 | 50 | val result = functionInvoker.invokeFunction(target, callDetails) 51 | assertEquals("param1: test, param2: 42", result) 52 | } 53 | 54 | @Test 55 | fun `test invokeFunction missing arg`() { 56 | val target = TestTarget() 57 | val callDetails = FunctionToolCallDetails( 58 | function = FunctionToolCallDetails.FunctionSpec( 59 | name = "testFunction", 60 | arguments = "{\"param1\":\"test\"}" 61 | ), 62 | id = "testId" 63 | ) 64 | 65 | val result = functionInvoker.invokeFunction(target, callDetails) 66 | assertEquals("param1: test, param2: 0", result) 67 | } 68 | 69 | @Test 70 | fun `test invokeFunction2 missing arg`() { 71 | val target = TestTarget() 72 | val callDetails = FunctionToolCallDetails( 73 | function = FunctionToolCallDetails.FunctionSpec( 74 | name = "testFunction2", 75 | arguments = "{\"param1\":\"test\"}" 76 | ), 77 | id = "testId" 78 | ) 79 | 80 | val result = functionInvoker.invokeFunction(target, callDetails) 81 | assertEquals("param1: test, param2: ", result) 82 | } 83 | 84 | 85 | @Test 86 | fun `test invokeFunction with data class`() { 87 | val target = TestTarget() 88 | val callDetails = FunctionToolCallDetails( 89 | function = FunctionToolCallDetails.FunctionSpec( 90 | name = "testFunctionData", 91 | arguments = "{\"data\":{\"param1\":\"test\",\"param2\":42}}" 92 | ), 93 | id = "testId" 94 | ) 95 | 96 | val result = functionInvoker.invokeFunction(target, callDetails) 97 | assertEquals("param1: test, param2: 42", result) 98 | } 99 | 100 | @Test 101 | fun `test invokeFunction with mixed arguments`() { 102 | val target = TestTarget() 103 | val callDetails = FunctionToolCallDetails( 104 | function = FunctionToolCallDetails.FunctionSpec( 105 | name = "testFunctionMixed", 106 | arguments = "{\"param1\":\"test\",\"data\":{\"param1\":\"test\",\"param2\":42}}" 107 | ), 108 | id = "testId" 109 | ) 110 | 111 | val result = functionInvoker.invokeFunction(target, callDetails) 112 | assertEquals("param1: test, param2: 42", result) 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile 2 | import java.util.* 3 | 4 | val ktor_version: String by project 5 | 6 | val versionProps = Properties().apply { 7 | load(file("version.properties").inputStream()) 8 | } 9 | val appVersion = versionProps["version"].toString() 10 | 11 | plugins { 12 | kotlin("jvm") version "1.9.10" 13 | application 14 | id("org.jetbrains.kotlin.plugin.serialization") version "1.8.21" 15 | id("com.github.johnrengelman.shadow") version "7.1.0" 16 | } 17 | 18 | group = "com.fluxtah.ask" 19 | version = appVersion 20 | 21 | repositories { 22 | mavenCentral() 23 | maven { url = uri("https://repo.gradle.org/gradle/libs-releases") } 24 | maven { url = uri("https://jitpack.io") } 25 | } 26 | 27 | dependencies { 28 | implementation("com.github.fluxtah:ask-plugin-sdk:0.7.2") 29 | implementation("com.fluxtah.ask:ask-plugin-koder:0.7.2") 30 | 31 | implementation(project(":ask-api")) 32 | 33 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1") 34 | implementation("io.ktor:ktor-client-core:$ktor_version") 35 | implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3") 36 | 37 | implementation("org.gradle:gradle-tooling-api:8.4") 38 | implementation("org.jetbrains.kotlin:kotlin-compiler-embeddable:1.7.0") 39 | 40 | implementation("org.jline:jline:3.25.0") 41 | 42 | implementation("org.jetbrains.exposed:exposed-dao:0.37.3") 43 | 44 | implementation("ch.qos.logback:logback-classic:1.4.12") 45 | 46 | implementation("io.insert-koin:koin-core:3.5.6") 47 | 48 | testImplementation(kotlin("test")) 49 | } 50 | 51 | tasks.test { 52 | useJUnitPlatform() 53 | } 54 | 55 | tasks.withType { 56 | kotlinOptions.jvmTarget = "17" 57 | } 58 | 59 | application { 60 | mainClass.set("MainKt") 61 | } 62 | 63 | 64 | tasks.shadowJar { 65 | archiveClassifier.set("") 66 | configurations = listOf(project.configurations.runtimeClasspath.get()) 67 | destinationDirectory.set(file("$buildDir/libs")) 68 | } 69 | 70 | tasks.register("packageDistribution") { 71 | dependsOn("shadowJar") 72 | doLast { 73 | // Directory where everything will be packaged 74 | val distDir = file("$buildDir/dist") 75 | 76 | // Ensure directory exists 77 | distDir.mkdirs() 78 | 79 | copy { 80 | from("$buildDir/libs") 81 | from("scripts") // Assumes 'scripts' is at the project root 82 | into(distDir) 83 | } 84 | 85 | // Define the path for the tarball within the dist directory 86 | val tarPath = "$distDir/ask-$appVersion.tar.gz" 87 | 88 | // Create tarball containing all contents of the dist directory 89 | exec { 90 | commandLine("tar", "czvf", tarPath, "-C", distDir.path, ".") 91 | } 92 | 93 | // Log the output path for verification 94 | println("Distribution package created at: $tarPath") 95 | 96 | exec { 97 | println("SHA256 checksum:") 98 | commandLine("shasum", "-a", "256", tarPath) 99 | } 100 | } 101 | } 102 | 103 | tasks.register("generateVersionFile") { 104 | doLast { 105 | val fileContent = """ 106 | package com.fluxtah.ask 107 | 108 | object Version { 109 | const val APP_VERSION = "$appVersion" 110 | } 111 | """.trimIndent() 112 | val file = File("src/main/kotlin/com/fluxtah/ask/Version.kt") 113 | file.parentFile.mkdirs() 114 | file.writeText(fileContent) 115 | } 116 | } 117 | 118 | tasks.withType { 119 | dependsOn("generateVersionFile") 120 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.code.style=official 2 | 3 | ktor_version=2.3.1 4 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluxtah/ask/96f87b42732c5dbe8b841791ade22489bafd0739/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 84 | 85 | APP_NAME="Gradle" 86 | APP_BASE_NAME=${0##*/} 87 | 88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | 142 | # Increase the maximum file descriptors if we can. 143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 144 | case $MAX_FD in #( 145 | max*) 146 | MAX_FD=$( ulimit -H -n ) || 147 | warn "Could not query maximum file descriptor limit" 148 | esac 149 | case $MAX_FD in #( 150 | '' | soft) :;; #( 151 | *) 152 | ulimit -n "$MAX_FD" || 153 | warn "Could not set maximum file descriptor limit to $MAX_FD" 154 | esac 155 | fi 156 | 157 | # Collect all arguments for the java command, stacking in reverse order: 158 | # * args from the command line 159 | # * the main class name 160 | # * -classpath 161 | # * -D...appname settings 162 | # * --module-path (only if needed) 163 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 164 | 165 | # For Cygwin or MSYS, switch paths to Windows format before running java 166 | if "$cygwin" || "$msys" ; then 167 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 168 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 169 | 170 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 171 | 172 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 173 | for arg do 174 | if 175 | case $arg in #( 176 | -*) false ;; # don't mess with options #( 177 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 178 | [ -e "$t" ] ;; #( 179 | *) false ;; 180 | esac 181 | then 182 | arg=$( cygpath --path --ignore --mixed "$arg" ) 183 | fi 184 | # Roll the args list around exactly as many times as the number of 185 | # args, so each arg winds up back in the position where it started, but 186 | # possibly modified. 187 | # 188 | # NB: a `for` loop captures its iteration list before it begins, so 189 | # changing the positional parameters here affects neither the number of 190 | # iterations, nor the values presented in `arg`. 191 | shift # remove old arg 192 | set -- "$@" "$arg" # push replacement arg 193 | done 194 | fi 195 | 196 | # Collect all arguments for the java command; 197 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 198 | # shell script including quotes and variable substitutions, so put them in 199 | # double quotes to make sure that they get re-expanded; and 200 | # * put everything else in single quotes, so that it's not re-expanded. 201 | 202 | set -- \ 203 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 204 | -classpath "$CLASSPATH" \ 205 | org.gradle.wrapper.GradleWrapperMain \ 206 | "$@" 207 | 208 | # Use "xargs" to parse quoted args. 209 | # 210 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 211 | # 212 | # In Bash we could simply go: 213 | # 214 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 215 | # set -- "${ARGS[@]}" "$@" 216 | # 217 | # but POSIX shell has neither arrays nor command substitution, so instead we 218 | # post-process each arg (as a line of input to sed) to backslash-escape any 219 | # character that might be a shell metacharacter, then use eval to reverse 220 | # that process (while maintaining the separation between arguments), and wrap 221 | # the whole thing up as a single "set" statement. 222 | # 223 | # This will of course break if any of these variables contains a newline or 224 | # an unmatched quote. 225 | # 226 | 227 | eval "set -- $( 228 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 229 | xargs -n1 | 230 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 231 | tr '\n' ' ' 232 | )" '"$@"' 233 | 234 | exec "$JAVACMD" "$@" 235 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /scripts/ask.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Default Java command 4 | JAVA_CMD="java -jar PATH_TO_JAR" 5 | 6 | # Check if the --test-plugin argument is present 7 | if [[ " $@ " =~ " --test-plugin " ]]; then 8 | # Modify the command to start the Java debugger 9 | JAVA_CMD="java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005 -jar PATH_TO_JAR" 10 | fi 11 | 12 | # Execute the command with all passed arguments 13 | $JAVA_CMD "$@" 14 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("org.gradle.toolchains.foojay-resolver-convention") version "0.5.0" 3 | } 4 | rootProject.name = "ask" 5 | includeBuild("../ask-plugin-sdk") 6 | includeBuild("../ask-plugin-koder") 7 | include("ask-api") 8 | -------------------------------------------------------------------------------- /src/main/kotlin/Main.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024 Ian Warwick 3 | * Released under the MIT license 4 | * https://opensource.org/licenses/MIT 5 | */ 6 | 7 | import com.fluxtah.ask.Version 8 | import com.fluxtah.ask.api.di.askApiModule 9 | import com.fluxtah.ask.app.ConsoleApplication 10 | import com.fluxtah.ask.app.di.appModule 11 | import com.fluxtah.ask.api.di.commandFactoryModule 12 | import com.fluxtah.ask.api.di.commandsModule 13 | import org.koin.core.context.GlobalContext 14 | import org.koin.core.context.startKoin 15 | import java.io.File 16 | 17 | suspend fun main(args: Array) { 18 | val testing = args.contains("--test-plugin") || args.contains("-t") 19 | val interactive = args.contains("--interactive") || args.contains("-i") 20 | val help = args.contains("--help") || args.contains("-h") 21 | val version = args.contains("--version") || args.contains("-v") 22 | val startsWithProgramName = args.firstOrNull()?.startsWith("ask") ?: false 23 | 24 | when { 25 | version -> { 26 | println("Ask Version: ${Version.APP_VERSION}") 27 | return 28 | } 29 | 30 | help -> { 31 | println("Usage: ask [options]") 32 | println("Ask is a command line tool for interacting with OpenAI's Assistants API") 33 | println() 34 | println("Options:") 35 | println(" --version, -v Print the version of the application") 36 | println(" --help, -h Print this help message") 37 | println(" --interactive, -i Run ask in interactive mode") 38 | println(" --test-plugin, -t Test a plugin") 39 | println(" Run a command in ask, run in interactive mode for available commands to setup first") 40 | return 41 | } 42 | } 43 | 44 | startKoin { 45 | modules(commandsModule, commandFactoryModule, askApiModule, appModule) 46 | } 47 | 48 | val consoleApplication: ConsoleApplication = GlobalContext.get().get() 49 | 50 | if (testing) { 51 | val testPluginArgIndexLong = args.indexOf("--test-plugin") 52 | val testPluginArgIndexShort = args.indexOf("-t") 53 | val testPluginArgIndex = if (testPluginArgIndexLong != -1) testPluginArgIndexLong else testPluginArgIndexShort 54 | val testPluginFilePath = args.getOrNull(testPluginArgIndex + 1) 55 | if (testPluginFilePath == null) { 56 | println("Usage: ask --test-plugin ") 57 | return 58 | } else { 59 | val pluginFile = File(testPluginFilePath) 60 | if (!pluginFile.exists()) { 61 | println("Plugin not found: $testPluginFilePath") 62 | return 63 | } 64 | consoleApplication.debugPlugin(pluginFile) 65 | } 66 | } 67 | 68 | if (interactive) { 69 | consoleApplication.run() 70 | return 71 | } 72 | 73 | if (!startsWithProgramName) { 74 | consoleApplication.runOneShotCommand(args.joinToString(" ")) 75 | return 76 | } 77 | 78 | if (args.size < 2) { 79 | println("Usage: ask ") 80 | } else { 81 | consoleApplication.runOneShotCommand(args.drop(1).joinToString(" ")) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/main/kotlin/com/fluxtah/ask/Version.kt: -------------------------------------------------------------------------------- 1 | package com.fluxtah.ask 2 | 3 | object Version { 4 | const val APP_VERSION = "0.8.5" 5 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/fluxtah/ask/app/AskCommandCompleter.kt: -------------------------------------------------------------------------------- 1 | package com.fluxtah.ask.app 2 | 3 | import com.fluxtah.ask.api.assistants.AssistantRegistry 4 | import com.fluxtah.ask.api.repository.ThreadRepository 5 | import com.fluxtah.ask.api.commanding.CommandFactory 6 | import com.fluxtah.askpluginsdk.io.getCurrentWorkingDirectory 7 | import org.jetbrains.kotlin.util.prefixIfNot 8 | import org.jline.reader.Candidate 9 | import org.jline.reader.Completer 10 | import org.jline.reader.LineReader 11 | import org.jline.reader.ParsedLine 12 | import java.io.File 13 | 14 | class AskCommandCompleter( 15 | private val assistantRegistry: AssistantRegistry, 16 | private val commandFactory: CommandFactory, 17 | private val threadRepository: ThreadRepository 18 | ) : Completer { 19 | 20 | private val models = listOf("gpt-3.5-turbo-16k", "gpt-4-turbo", "gpt-4o") 21 | 22 | private val logLevels = listOf("ERROR", "DEBUG", "INFO", "OFF") 23 | 24 | override fun complete(reader: LineReader?, line: ParsedLine?, candidates: MutableList?) { 25 | if (line == null || candidates == null) return 26 | 27 | val words = line.words() 28 | val wordIndex = line.wordIndex() 29 | val currentWord = if (wordIndex < words.size) words[wordIndex] else "" 30 | 31 | when { 32 | currentWord.startsWith("@") -> { 33 | assistantRegistry.getAssistants().map { it.id.prefixIfNot("@") } 34 | .filter { it.startsWith(currentWord) } 35 | .forEach { candidates.add(Candidate(it)) } 36 | } 37 | 38 | line.line().endsWith("file:") -> { 39 | if (wordIndex == words.size - 1) { 40 | File(getCurrentWorkingDirectory()).listFiles()?.forEach { 41 | candidates.add( 42 | Candidate( 43 | if (it.isDirectory) 44 | "file:${it.name}/" 45 | else 46 | "file:${it.name}", 47 | if (it.isDirectory) 48 | "${it.name}/" 49 | else 50 | it.name, 51 | null, 52 | null, 53 | null, 54 | null, 55 | !it.isDirectory 56 | ), 57 | ) 58 | } 59 | } 60 | } 61 | 62 | line.word().matches("file:.+/$".toRegex()) -> { 63 | // Properly form the path to include a separator between the directory parts 64 | val path = line.line().substringAfterLast("file:").dropLast(1) 65 | val fullPath = getCurrentWorkingDirectory() + "/" + path 66 | File(fullPath).listFiles()?.forEach { 67 | candidates.add( 68 | Candidate( 69 | if (it.isDirectory) 70 | "file:$path/${it.name}/" 71 | else 72 | "file:$path/${it.name}", 73 | if (it.isDirectory) 74 | "${it.name}/" 75 | else 76 | it.name, 77 | null, null, null, null, !it.isDirectory 78 | ) 79 | ) 80 | } 81 | } 82 | 83 | words.size > 1 && words[0].startsWith("/assistant-info") || 84 | words[0].startsWith("/assistant-install") || 85 | words[0].startsWith("/assistant-uninstall") -> { 86 | assistantRegistry.getAssistants().forEach { candidates.add(Candidate(it.id)) } 87 | } 88 | 89 | words.size > 1 && words[0].startsWith("/model") -> { 90 | if (wordIndex == 1) { 91 | models.forEach { candidates.add(Candidate(it)) } 92 | } 93 | } 94 | 95 | words.size > 1 && words[0].startsWith("/log-level") -> { 96 | if (wordIndex == 1) { 97 | logLevels.forEach { candidates.add(Candidate(it)) } 98 | } 99 | } 100 | 101 | words.size > 1 && (words[0].startsWith("/thread-delete") || 102 | words[0].startsWith("/thread-switch")) -> { 103 | if (wordIndex == 1) { 104 | threadRepository.listThreads().forEach { candidates.add(Candidate(it.threadId)) } 105 | } 106 | } 107 | 108 | wordIndex == 0 -> { 109 | commandFactory.getCommandsSortedByName().map { "/${it.name}" }.filter { it.startsWith(currentWord) } 110 | .forEach { candidates.add(Candidate(it)) } 111 | } 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/main/kotlin/com/fluxtah/ask/app/ConsoleApplication.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024 Ian Warwick 3 | * Released under the MIT license 4 | * https://opensource.org/licenses/MIT 5 | */ 6 | package com.fluxtah.ask.app 7 | 8 | import ch.qos.logback.classic.Level 9 | import ch.qos.logback.classic.Logger 10 | import com.fluxtah.ask.api.AssistantRunManager 11 | import com.fluxtah.ask.api.InputHandler 12 | import com.fluxtah.ask.api.RunManagerStatus 13 | import com.fluxtah.ask.api.ansi.green 14 | import com.fluxtah.ask.api.ansi.red 15 | import com.fluxtah.ask.api.assistants.AssistantRegistry 16 | import com.fluxtah.ask.api.audio.AudioRecorder 17 | import com.fluxtah.ask.api.audio.TextToSpeechPlayer 18 | import com.fluxtah.ask.api.clients.openai.audio.AudioApi 19 | import com.fluxtah.ask.api.clients.openai.audio.model.CreateTranscriptionRequest 20 | import com.fluxtah.ask.api.plugins.AskPluginLoader 21 | import com.fluxtah.ask.api.printers.AskResponsePrinter 22 | import com.fluxtah.ask.api.store.user.UserProperties 23 | import com.fluxtah.askpluginsdk.logging.AskLogger 24 | import com.fluxtah.askpluginsdk.logging.LogLevel 25 | import kotlinx.coroutines.CoroutineScope 26 | import kotlinx.coroutines.launch 27 | import kotlinx.coroutines.runBlocking 28 | import org.jline.reader.LineReader 29 | import java.io.File 30 | import kotlin.system.exitProcess 31 | 32 | class ConsoleApplication( 33 | private val logger: AskLogger, 34 | private val coroutineScope: CoroutineScope, 35 | private val userProperties: UserProperties, 36 | private val audioApi: AudioApi, 37 | private val assistantRegistry: AssistantRegistry, 38 | private val responsePrinter: AskResponsePrinter, 39 | assistantRunManager: AssistantRunManager, 40 | private val tts: TextToSpeechPlayer, 41 | private val audioRecorder: AudioRecorder, 42 | private val inputHandler: InputHandler, 43 | private val consoleOutputRenderer: ConsoleOutputRenderer, 44 | private val lineReader: LineReader 45 | ) { 46 | private var transcribedText: String = "" 47 | 48 | init { 49 | val exposedLogger = org.jetbrains.exposed.sql.exposedLogger as Logger 50 | exposedLogger.level = Level.OFF 51 | 52 | userProperties.load() 53 | 54 | AskPluginLoader(logger).loadPlugins().forEach { 55 | assistantRegistry.register(it) 56 | } 57 | 58 | tts.enabled = userProperties.getTalkEnabled() 59 | 60 | assistantRunManager.onStatusChanged = ::onAssistantStatusChanged 61 | } 62 | 63 | suspend fun runOneShotCommand(command: String) { 64 | inputHandler.handleInput(command) 65 | exitProcess(0) 66 | } 67 | 68 | suspend fun run() { 69 | consoleOutputRenderer.renderWelcomeMessage() 70 | initLogLevelAndPrint() 71 | 72 | Runtime.getRuntime().addShutdownHook(Thread { 73 | coroutineScope.launch { 74 | audioRecorder.stop() 75 | } 76 | }) 77 | 78 | while (true) { 79 | println() 80 | 81 | try { 82 | val input = commandPromptReadLine() 83 | transcribedText = "" 84 | 85 | if (audioRecorder.isRecording()) { 86 | handleRecordingComplete() 87 | continue 88 | } 89 | 90 | inputHandler.handleInput(input) 91 | } catch (e: Exception) { 92 | logger.log(LogLevel.ERROR, "Error: ${e.message}") 93 | } 94 | } 95 | } 96 | 97 | private suspend fun handleRecordingComplete() { 98 | endAudioRecording() 99 | transcribeAudioRecording() 100 | if (userProperties.getAutoSendVoice()) { 101 | println() 102 | responsePrinter.printMessage("${green(promptText())} $transcribedText") 103 | inputHandler.handleInput(transcribedText) 104 | transcribedText = "" 105 | } 106 | } 107 | 108 | suspend fun debugPlugin(pluginFile: File) { 109 | assistantRegistry.register(AskPluginLoader(logger).loadPlugin(pluginFile)) 110 | 111 | run() 112 | } 113 | 114 | private fun onAssistantStatusChanged(status: RunManagerStatus) { 115 | when (status) { 116 | is RunManagerStatus.Response -> { 117 | consoleOutputRenderer.renderAssistantResponse(status.response) 118 | tts.queue(status.response) 119 | tts.playNext() 120 | } 121 | 122 | is RunManagerStatus.ToolCall -> { 123 | consoleOutputRenderer.renderAssistantToolCall(status.details) 124 | } 125 | 126 | is RunManagerStatus.MessageCreated -> { 127 | consoleOutputRenderer.renderAssistantMessage(status.message) 128 | tts.queue(status.message.content.firstOrNull()?.text?.value ?: "") 129 | tts.playNext() 130 | } 131 | 132 | is RunManagerStatus.Error -> { 133 | consoleOutputRenderer.renderAssistantError(status) 134 | tts.queue(status.message) 135 | tts.playNext() 136 | } 137 | 138 | is RunManagerStatus.RunStatusChanged -> { 139 | consoleOutputRenderer.renderAssistantRunStatusChanged(status.runStatus) 140 | } 141 | 142 | RunManagerStatus.BeforeBeginRun -> { 143 | responsePrinter.begin().println().end() 144 | tts.stop() 145 | tts.clear() 146 | } 147 | } 148 | } 149 | 150 | private fun transcribeAudioRecording() { 151 | runBlocking { 152 | val response = audioApi.createTranscription( 153 | CreateTranscriptionRequest(audioRecorder.getAudioFile()) 154 | ) 155 | 156 | transcribedText = response.text 157 | } 158 | } 159 | 160 | private fun endAudioRecording() { 161 | coroutineScope.launch { 162 | audioRecorder.stop() 163 | } 164 | clearLinesAndSleep() 165 | } 166 | 167 | private fun clearLinesAndSleep(backTimes: Int = 2, waitTime: Long = 200) { 168 | for (i in 1..backTimes) { 169 | responsePrinter.printMessage("\u001b[1A\u001b[2K") 170 | } 171 | Thread.sleep(waitTime) 172 | } 173 | 174 | private fun initLogLevelAndPrint() { 175 | val logLevel = userProperties.getLogLevel() 176 | if (logLevel != LogLevel.OFF) { 177 | logger.log(LogLevel.DEBUG, "Log level: $logLevel") 178 | } 179 | 180 | logger.setLogLevel(logLevel) 181 | } 182 | 183 | private fun commandPromptReadLine(): String { 184 | val prompt = promptText() 185 | 186 | return when { 187 | audioRecorder.isRecording() -> { 188 | lineReader.readLine(red("[Recording \uD83C\uDFA4 - ENTER to stop]")) 189 | } 190 | 191 | else -> { 192 | if (transcribedText.isNotEmpty()) { 193 | lineReader.readLine("${green(prompt)} ", null, transcribedText) 194 | } else { 195 | lineReader.readLine(green(prompt) + " ") 196 | } 197 | } 198 | } 199 | } 200 | 201 | private fun promptText(): String { 202 | val prompt = if (userProperties.getAssistantId().isEmpty()) { 203 | "ask ➜" 204 | } else { 205 | "ask@${userProperties.getAssistantId()} ➜" 206 | } 207 | return prompt 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /src/main/kotlin/com/fluxtah/ask/app/ConsoleOutputRenderer.kt: -------------------------------------------------------------------------------- 1 | package com.fluxtah.ask.app 2 | 3 | import com.fluxtah.ask.Version 4 | import com.fluxtah.ask.api.RunManagerStatus 5 | import com.fluxtah.ask.api.ansi.blue 6 | import com.fluxtah.ask.api.ansi.green 7 | import com.fluxtah.ask.api.clients.openai.assistants.model.AssistantRunStepDetails 8 | import com.fluxtah.ask.api.clients.openai.assistants.model.Message 9 | import com.fluxtah.ask.api.clients.openai.assistants.model.RunStatus 10 | import com.fluxtah.ask.api.markdown.AnsiMarkdownRenderer 11 | import com.fluxtah.ask.api.markdown.MarkdownParser 12 | import com.fluxtah.ask.api.printers.AskResponsePrinter 13 | import com.fluxtah.ask.api.commanding.CommandFactory 14 | import kotlinx.coroutines.runBlocking 15 | 16 | class ConsoleOutputRenderer( 17 | private val responsePrinter: AskResponsePrinter, 18 | private val workingSpinner: WorkingSpinner, 19 | ) { 20 | fun renderWelcomeMessage() { 21 | responsePrinter 22 | .begin() 23 | .println() 24 | .println( 25 | """ 26 | ░▒▓██████▓▒░ ░▒▓███████▓▒░▒▓█▓▒░░▒▓█▓▒░ 27 | ░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░ 28 | ░▒▓████████▓▒░░▒▓██████▓▒░░▒▓██████▓▒░ 29 | ░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░ 30 | ░▒▓█▓▒░░▒▓█▓▒░▒▓███████▓▒░░▒▓█▓▒░░▒▓█▓▒░ 31 | """.trimIndent() 32 | ) 33 | .println("░▒▓█▓░ ASSISTANT KOMMANDER v${Version.APP_VERSION} ░▓█▓▒░") 34 | .println() 35 | .println("Type /help for a list of commands, to quit press Ctrl+C or type /exit") 36 | .end() 37 | } 38 | 39 | fun renderAssistantResponse(response: String) { 40 | val markdownParser = MarkdownParser(response) 41 | val renderedMarkdown = AnsiMarkdownRenderer().render(markdownParser.parse()) 42 | responsePrinter 43 | .begin() 44 | .println(renderedMarkdown) 45 | .end() 46 | } 47 | 48 | fun renderAssistantToolCall(details: AssistantRunStepDetails.ToolCalls.ToolCallDetails.FunctionToolCallDetails) { 49 | responsePrinter 50 | .begin() 51 | .print("\u001b[1A\u001b[2K") 52 | .println(" ${green("==>")} ${blue(details.function.name)} (${details.function.arguments})") 53 | .println() 54 | .println() 55 | .end() 56 | } 57 | 58 | fun renderAssistantMessage(message: Message) { 59 | responsePrinter 60 | .begin() 61 | .print("\u001b[1A\u001b[2K") 62 | .println(message.content.joinToString(" ") { it.text.value }) 63 | .println() 64 | .println() 65 | .end() 66 | } 67 | 68 | fun renderAssistantError(error: RunManagerStatus.Error) { 69 | responsePrinter 70 | .begin() 71 | .println() 72 | .println(error.message) 73 | .end() 74 | } 75 | 76 | fun renderAssistantRunStatusChanged(runStatus: RunStatus) { 77 | val indicator = when (runStatus) { 78 | RunStatus.FAILED, 79 | RunStatus.CANCELLED, 80 | RunStatus.EXPIRED -> "x" 81 | 82 | RunStatus.COMPLETED -> green("✔") 83 | else -> blue(workingSpinner.next()) 84 | } 85 | 86 | responsePrinter 87 | .begin() 88 | .print("\u001b[1A\u001b[2K") 89 | .println(" $indicator $runStatus") 90 | .end() 91 | } 92 | 93 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/fluxtah/ask/app/WorkingSpinner.kt: -------------------------------------------------------------------------------- 1 | package com.fluxtah.ask.app 2 | 3 | class WorkingSpinner { 4 | private val loadingChars = listOf("|", "/", "-", "\\") 5 | private var loadingCharIndex = 0 6 | 7 | fun next(): String { 8 | loadingCharIndex = (loadingCharIndex + 1) % loadingChars.size 9 | return loadingChars[loadingCharIndex] 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/kotlin/com/fluxtah/ask/app/di/AppModule.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024 Ian Warwick 3 | * Released under the MIT license 4 | * https://opensource.org/licenses/MIT 5 | */ 6 | package com.fluxtah.ask.app.di 7 | 8 | import com.fluxtah.ask.api.printers.AskConsoleResponsePrinter 9 | import com.fluxtah.ask.api.printers.AskResponsePrinter 10 | import com.fluxtah.ask.app.AskCommandCompleter 11 | import com.fluxtah.ask.app.ConsoleApplication 12 | import com.fluxtah.ask.app.ConsoleOutputRenderer 13 | import com.fluxtah.ask.app.WorkingSpinner 14 | import kotlinx.coroutines.CoroutineScope 15 | import kotlinx.coroutines.Dispatchers 16 | import org.jline.reader.LineReaderBuilder 17 | import org.jline.terminal.TerminalBuilder 18 | import org.koin.core.module.dsl.singleOf 19 | import org.koin.dsl.module 20 | 21 | val appModule = module { 22 | single { CoroutineScope(Dispatchers.Default) } 23 | singleOf(::AskConsoleResponsePrinter) 24 | singleOf(::ConsoleOutputRenderer) 25 | singleOf(::AskCommandCompleter) 26 | singleOf(::WorkingSpinner) 27 | 28 | singleOf(::ConsoleApplication) 29 | single { 30 | TerminalBuilder.builder() 31 | .system(true) 32 | .build() 33 | } 34 | single { 35 | LineReaderBuilder.builder() 36 | .terminal(get()) 37 | .completer(get()) 38 | .build() 39 | } 40 | } 41 | 42 | -------------------------------------------------------------------------------- /src/test/kotlin/com/fluxtah/ask/VersionUtilsTest.kt: -------------------------------------------------------------------------------- 1 | package com.fluxtah.ask 2 | 3 | import com.fluxtah.ask.api.version.VersionUtils 4 | import org.junit.jupiter.api.Assertions.* 5 | import org.junit.jupiter.api.Test 6 | 7 | class VersionUtilsTest { 8 | @Test 9 | fun `test isVersionGreater`() { 10 | assertTrue(VersionUtils.isVersionGreater("0.1.1", "0.1.0")) 11 | assertFalse(VersionUtils.isVersionGreater("0.1.0", "0.1.1")) 12 | assertTrue(VersionUtils.isVersionGreater("1.0.0", "0.9.9")) 13 | assertFalse(VersionUtils.isVersionGreater("1.0.0", "1.0.0")) 14 | assertTrue(VersionUtils.isVersionGreater("1.0.10", "1.0.2")) 15 | assertFalse(VersionUtils.isVersionGreater("1.0.2", "1.0.10")) 16 | } 17 | } -------------------------------------------------------------------------------- /version.properties: -------------------------------------------------------------------------------- 1 | version=0.8.5 --------------------------------------------------------------------------------