├── .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 |
5 |
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