├── project ├── build.properties └── plugins.sbt ├── src ├── main │ └── scala │ │ └── com │ │ └── supercoder │ │ ├── base │ │ ├── Tool.scala │ │ └── Agent.scala │ │ ├── lib │ │ ├── Console.scala │ │ └── CursorRulesLoader.scala │ │ ├── Main.scala │ │ ├── tools │ │ ├── FileReadTool.scala │ │ ├── UrlFetchTool.scala │ │ ├── CommandExecutionTool.scala │ │ ├── CodeSearchTool.scala │ │ ├── CodeEditTool.scala │ │ ├── WebSearchTool.scala │ │ └── ProjectStructureTool.scala │ │ ├── config │ │ └── ArgsParser.scala │ │ ├── agents │ │ └── CoderAgent.scala │ │ └── ui │ │ └── TerminalChat.scala └── test │ └── scala │ └── MySuite.scala ├── .gitignore ├── .github └── workflows │ └── scala.yml └── README.md /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.10.10 2 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin( 2 | "org.scalameta" % "sbt-scalafmt" % "2.5.2" 3 | ) // Check for latest version 4 | addSbtPlugin("com.github.sbt" % "sbt-native-packager" % "1.11.1") 5 | addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.11.0") -------------------------------------------------------------------------------- /src/main/scala/com/supercoder/base/Tool.scala: -------------------------------------------------------------------------------- 1 | package com.supercoder.base 2 | 3 | import com.openai.models.FunctionDefinition 4 | 5 | trait Tool { 6 | val functionDefinition: FunctionDefinition 7 | def execute(arguments: String): String 8 | } 9 | -------------------------------------------------------------------------------- /src/test/scala/MySuite.scala: -------------------------------------------------------------------------------- 1 | // For more information on writing tests, see 2 | 3 | // https://scalameta.org/munit/docs/getting-started.html 4 | class MySuite extends munit.FunSuite { 5 | 6 | test("example test that succeeds") { 7 | val obtained = 42 8 | val expected = 42 9 | assertEquals(obtained, expected) 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # macOS 2 | .DS_Store 3 | 4 | # sbt specific 5 | dist/* 6 | target/ 7 | lib_managed/ 8 | src_managed/ 9 | project/boot/ 10 | project/plugins/project/ 11 | project/local-plugins.sbt 12 | .history 13 | .ensime 14 | .ensime_cache/ 15 | .sbt-scripted/ 16 | local.sbt 17 | 18 | # Bloop 19 | .bsp 20 | 21 | # VS Code 22 | .vscode/ 23 | 24 | # Metals 25 | .bloop/ 26 | .metals/ 27 | metals.sbt 28 | 29 | # IDEA 30 | .idea 31 | .idea_modules 32 | /.worksheet/ 33 | -------------------------------------------------------------------------------- /src/main/scala/com/supercoder/lib/Console.scala: -------------------------------------------------------------------------------- 1 | package com.supercoder.lib 2 | 3 | import scala.io.AnsiColor.* 4 | 5 | object Console { 6 | def bold(text: String): String = s"${BOLD}$text${RESET}" 7 | 8 | def underline(text: String): String = 9 | s"${UNDERLINED}$text${RESET}" 10 | 11 | def black(text: String): String = s"${BLACK}$text${RESET}" 12 | 13 | def blue(text: String): String = s"${BLUE}$text${RESET}" 14 | 15 | def green(text: String): String = s"${GREEN}$text${RESET}" 16 | 17 | def red(text: String): String = s"${RED}$text${RESET}" 18 | 19 | def yellow(text: String): String = 20 | s"${YELLOW}$text${RESET}" 21 | 22 | def white(text: String): String = 23 | s"${WHITE}$text${RESET}" 24 | } 25 | -------------------------------------------------------------------------------- /src/main/scala/com/supercoder/Main.scala: -------------------------------------------------------------------------------- 1 | package com.supercoder 2 | 3 | import com.supercoder.ui.TerminalChat 4 | import com.supercoder.agents.CoderAgent 5 | import com.supercoder.config.{ArgsParser, Config} 6 | import com.supercoder.lib.CursorRulesLoader 7 | 8 | object Main { 9 | var AppConfig: Config = Config() 10 | 11 | def main(args: Array[String]): Unit = { 12 | ArgsParser.parse(args) match { 13 | case Some(config) => 14 | AppConfig = config 15 | val additionalPrompt = if AppConfig.useCursorRules then CursorRulesLoader.loadRules() else "" 16 | val modelName = AppConfig.model 17 | val agent = new CoderAgent(additionalPrompt, modelName) 18 | TerminalChat.run(agent) 19 | case None => 20 | // invalid options, usage error message is already printed by scopt 21 | sys.exit(1) 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/scala/com/supercoder/tools/FileReadTool.scala: -------------------------------------------------------------------------------- 1 | package com.supercoder.tools 2 | 3 | import com.openai.models.FunctionDefinition 4 | import com.supercoder.base.Tool 5 | import com.supercoder.lib.Console.green 6 | import io.circe.* 7 | import io.circe.generic.auto.* 8 | import io.circe.parser.* 9 | 10 | import scala.sys.process.* 11 | 12 | case class FileReadToolArguments(fileName: String) 13 | 14 | object FileReadTool extends Tool { 15 | 16 | override val functionDefinition = FunctionDefinition 17 | .builder() 18 | .name("file-read") 19 | .description( 20 | "Read a file to understand its content. Use this tool to read a file and understand its content. Arguments: {\"fileName\": \"\"}" 21 | ) 22 | .build() 23 | 24 | override def execute(arguments: String): String = { 25 | val parsedArguments = decode[FileReadToolArguments](arguments) 26 | parsedArguments match { 27 | case Right(args) => { 28 | val fileName = args.fileName 29 | println(green(s"📂 Reading file: ${fileName}")) 30 | val command = s"""cat ${fileName}""" 31 | val output: String = command.!! 32 | return output 33 | } 34 | case _ => "Error: Invalid arguments" 35 | } 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/main/scala/com/supercoder/tools/UrlFetchTool.scala: -------------------------------------------------------------------------------- 1 | package com.supercoder.tools 2 | 3 | import com.openai.models.FunctionDefinition 4 | import com.supercoder.base.Tool 5 | import com.supercoder.lib.Console.green 6 | import io.circe.* 7 | import io.circe.generic.auto.* 8 | import io.circe.parser.* 9 | 10 | case class UrlFetchToolArguments(url: String) 11 | 12 | object UrlFetchTool extends Tool { 13 | override val functionDefinition: FunctionDefinition = FunctionDefinition.builder() 14 | .name("url-fetch") 15 | .description("Fetch content from a specified URL. Arguments: {\"url\": \"\"}") 16 | .build() 17 | 18 | override def execute(arguments: String): String = { 19 | val parsedArguments = decode[UrlFetchToolArguments](arguments) 20 | parsedArguments match { 21 | case Right(args) => 22 | val url = args.url 23 | println(green(s"\uD83D\uDD0D Fetching URL: $url")) 24 | try { 25 | val response = requests.get( 26 | url, 27 | connectTimeout = 5000, 28 | readTimeout = 10000 29 | ) 30 | response.text() 31 | } catch { 32 | case e: Exception => s"Error: ${e.getMessage}" 33 | } 34 | case Left(error) => 35 | s"Error: Invalid arguments - ${error.getMessage}" 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/scala/com/supercoder/tools/CommandExecutionTool.scala: -------------------------------------------------------------------------------- 1 | package com.supercoder.tools 2 | 3 | import com.openai.models.FunctionDefinition 4 | import com.supercoder.base.Tool 5 | import com.supercoder.lib.Console.green 6 | import io.circe.* 7 | import io.circe.generic.auto.* 8 | import io.circe.parser.* 9 | 10 | import scala.sys.process.* 11 | 12 | case class CommandExecutionToolArguments(command: String) 13 | 14 | object CommandExecutionTool extends Tool { 15 | 16 | override val functionDefinition = FunctionDefinition 17 | .builder() 18 | .name("command-execution") 19 | .description( 20 | "Execute a shell command on the user's terminal, and pass the output back to the agent. Arguments: {\"command\": \"\"}" 21 | ) 22 | .build() 23 | 24 | override def execute(arguments: String): String = { 25 | val parsedArguments = decode[CommandExecutionToolArguments](arguments) 26 | parsedArguments match { 27 | case Right(args) => { 28 | val command = args.command 29 | println(green(s"🔍 Execute shell command: ${command}")) 30 | try { 31 | val output: String = command.!! 32 | return output 33 | } catch { 34 | case e: Exception => s"Error: ${e.getMessage}" 35 | } 36 | } 37 | case _ => "Error: Invalid arguments" 38 | } 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/main/scala/com/supercoder/tools/CodeSearchTool.scala: -------------------------------------------------------------------------------- 1 | package com.supercoder.tools 2 | 3 | import com.openai.models.FunctionDefinition 4 | import com.supercoder.base.Tool 5 | import com.supercoder.lib.Console.green 6 | import io.circe.* 7 | import io.circe.generic.auto.* 8 | import io.circe.parser.* 9 | 10 | import scala.sys.process.* 11 | 12 | case class CodeSearchToolArguments(query: String) 13 | 14 | object CodeSearchTool extends Tool { 15 | 16 | override val functionDefinition = FunctionDefinition 17 | .builder() 18 | .name("code-search") 19 | .description( 20 | "Search for code in a given repository. The query parameter should be a regular expression. Arguments: {\"query\": \"\"}" 21 | ) 22 | .build() 23 | 24 | override def execute(arguments: String): String = { 25 | val parsedArguments = decode[CodeSearchToolArguments](arguments) 26 | parsedArguments match { 27 | case Right(args) => { 28 | val query = args.query 29 | println(green(s"🔍 Search code for query: ${query}")) 30 | try { 31 | val command = s"""git grep -n "$query" .""" 32 | val output: String = command.!! 33 | return output 34 | } catch { 35 | case e: Exception => s"Error: ${e.getMessage}" 36 | } 37 | } 38 | case _ => "Error: Invalid arguments" 39 | } 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/main/scala/com/supercoder/config/ArgsParser.scala: -------------------------------------------------------------------------------- 1 | package com.supercoder.config 2 | 3 | import scopt.OParser 4 | 5 | case class Config( 6 | useCursorRules: Boolean = false, 7 | model: String = "", 8 | isDebugMode: Boolean = false, 9 | temperature: Double = 0.2, 10 | top_p: Double = 0.1 11 | ) 12 | 13 | object ArgsParser { 14 | def parse(args: Array[String]): Option[Config] = { 15 | val builder = OParser.builder[Config] 16 | val parser = { 17 | import builder._ 18 | OParser.sequence( 19 | programName("SuperCoder"), 20 | opt[String]('c', "use-cursor-rules") 21 | .action((x, c) => c.copy(useCursorRules = (x == "true"))) 22 | .text("use Cursor rules for the agent"), 23 | opt[String]('m', "model") 24 | .action((x, c) => c.copy(model = x)) 25 | .text("model to use for the agent"), 26 | opt[String]('d', "debug") 27 | .action((x, c) => c.copy(isDebugMode = (x == "true"))) 28 | .text("enable debug mode"), 29 | opt[Double]('t', "temperature") 30 | .action((x, c) => c.copy(temperature = x)) 31 | .text("temperature for LLM calls (default: 0.2)"), 32 | opt[Double]('p', "top_p") 33 | .action((x, c) => c.copy(top_p = x)) 34 | .text("top_p for LLM nucleus sampling (default: 0.1)"), 35 | help("help").text("prints this usage text") 36 | ) 37 | } 38 | OParser.parse(parser, args, Config()) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/scala/com/supercoder/tools/CodeEditTool.scala: -------------------------------------------------------------------------------- 1 | package com.supercoder.tools 2 | 3 | import com.openai.models.FunctionDefinition 4 | import com.supercoder.base.Tool 5 | import com.supercoder.lib.Console.green 6 | import io.circe.* 7 | import io.circe.generic.auto.* 8 | import io.circe.parser.* 9 | 10 | import java.io.{File, PrintWriter} 11 | 12 | case class CodeEditToolArguments(filepath: String, content: String) 13 | 14 | object CodeEditTool extends Tool { 15 | 16 | override val functionDefinition = FunctionDefinition 17 | .builder() 18 | .name("code-edit") 19 | .description( 20 | "Edit a code file in the repository. Provide the file path and the new content for the file. Arguments: {\"filepath\": \"\", \"content\": \"\"}" 21 | ) 22 | .build() 23 | 24 | override def execute(arguments: String): String = { 25 | val parsedArguments = decode[CodeEditToolArguments](arguments) 26 | parsedArguments match { 27 | case Right(args) => { 28 | val filepath = args.filepath 29 | val content = args.content 30 | println(green(s"✏️ Editing file: ${filepath}")) 31 | 32 | try { 33 | val file = new File(filepath) 34 | // Create directory if it doesn't exist 35 | file.getParentFile.mkdirs() 36 | 37 | // Write the content to the file 38 | val writer = new PrintWriter(file) 39 | writer.write(content) 40 | writer.close() 41 | 42 | s"Successfully edited file: ${filepath}" 43 | } catch { 44 | case e: Exception => s"Error editing file: ${e.getMessage}" 45 | } 46 | } 47 | case Left(error) => s"Error: Invalid arguments - ${error.getMessage}" 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/scala/com/supercoder/tools/WebSearchTool.scala: -------------------------------------------------------------------------------- 1 | package com.supercoder.tools 2 | 3 | import com.openai.models.FunctionDefinition 4 | import com.supercoder.base.Tool 5 | import com.supercoder.lib.Console.green 6 | import io.circe.* 7 | import io.circe.generic.auto.* 8 | import io.circe.parser.* 9 | 10 | import java.net.URLEncoder 11 | 12 | case class WebSearchToolArguments(query: String, limit: Int) 13 | 14 | object WebSearchTool extends Tool { 15 | val searxngInstance: String = sys.env.getOrElse("SEARXNG_URL", "") 16 | 17 | override val functionDefinition: FunctionDefinition = FunctionDefinition.builder() 18 | .name("web-search") 19 | .description("Perform web search using SearxNG. Use this when you need to find information that is not in the codebase or when you need to find a specific library or tool. Arguments: {\"query\": \"\", \"limit\": }") 20 | .build() 21 | 22 | override def execute(arguments: String): String = { 23 | if (searxngInstance.isEmpty) { 24 | return "Error: SearxNG instance URL is not set. Please set the SEARXNG_URL environment variable." 25 | } 26 | val parsedArguments = decode[WebSearchToolArguments](arguments) 27 | parsedArguments match { 28 | case Right(args) => 29 | val query = args.query 30 | val encodedQuery = URLEncoder.encode(args.query, "UTF-8") 31 | val limit = args.limit 32 | println(green(s"🔍 Searching the web for: $query")) 33 | try { 34 | val searchUrl = s"$searxngInstance/search?q=$encodedQuery&format=json&limit=$limit" 35 | val response = requests.get(searchUrl) 36 | response.text() 37 | } catch { 38 | case e: Exception => s"Error: ${e.getMessage}" 39 | } 40 | case Left(error) => 41 | s"Error: Invalid arguments - ${error.getMessage}" 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/scala/com/supercoder/lib/CursorRulesLoader.scala: -------------------------------------------------------------------------------- 1 | package com.supercoder.lib 2 | 3 | import java.nio.file.{Files, Paths, Path} 4 | import java.nio.charset.StandardCharsets 5 | import scala.jdk.CollectionConverters._ 6 | 7 | object CursorRulesLoader { 8 | /** 9 | * Loads all *.mdc files in the project's .cursor/rules directory, then reads the .cursorrules file in the project's root, 10 | * combines them and returns them as a single string. 11 | * 12 | * The returned string consists of the content of all .mdc files (separated by newlines) followed by a newline and the 13 | * content of the .cursorrules file. 14 | */ 15 | def loadRules(): String = { 16 | // Determine the project root directory 17 | val projectRoot = Paths.get(System.getProperty("user.dir")) 18 | 19 | // Define the path for .cursorrules file in the project's root 20 | val cursorRulesFile: Path = projectRoot.resolve(".cursorrules") 21 | 22 | // Define the directory path for .cursor/rules 23 | val rulesDir: Path = projectRoot.resolve(Paths.get(".cursor", "rules")) 24 | 25 | // Initialize content for .mdc files 26 | val mdcContent: String = if (Files.exists(rulesDir) && Files.isDirectory(rulesDir)) { 27 | // List all files in the rulesDir ending with .mdc 28 | val mdcFiles = Files.list(rulesDir).iterator().asScala 29 | .filter(path => Files.isRegularFile(path) && path.getFileName.toString.endsWith(".mdc")) 30 | .toList 31 | // Read each file and concatenate contents separated by newline 32 | mdcFiles.map { path => 33 | new String(Files.readAllBytes(path), StandardCharsets.UTF_8) 34 | }.mkString("\n") 35 | } else { 36 | "" 37 | } 38 | 39 | // Read the .cursorrules file if it exists 40 | val mainRulesContent: String = if (Files.exists(cursorRulesFile) && Files.isRegularFile(cursorRulesFile)) { 41 | new String(Files.readAllBytes(cursorRulesFile), StandardCharsets.UTF_8) 42 | } else { 43 | "" 44 | } 45 | 46 | // Combine the content from .mdc files and .cursorrules file 47 | // Separating the two parts with a newline if both are non-empty 48 | if (mdcContent.nonEmpty && mainRulesContent.nonEmpty) { 49 | mdcContent + "\n" + mainRulesContent 50 | } else { 51 | mdcContent + mainRulesContent 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/main/scala/com/supercoder/agents/CoderAgent.scala: -------------------------------------------------------------------------------- 1 | package com.supercoder.agents 2 | 3 | import com.openai.models.FunctionDefinition 4 | import com.supercoder.base.{BaseChatAgent, ToolCallDescription} 5 | import com.supercoder.tools.{CodeEditTool, CodeSearchTool, CommandExecutionTool, FileReadTool, ProjectStructureTool, UrlFetchTool, WebSearchTool} 6 | 7 | val coderAgentPrompt = s""" 8 | You are a senior software engineer AI agent. Your task is to help the user with their coding needs. 9 | 10 | You have access to the following tools: 11 | 12 | - ${CodeSearchTool.functionDefinition.name}: ${CodeSearchTool.functionDefinition.description} 13 | - ${ProjectStructureTool.functionDefinition.name}: ${ProjectStructureTool.functionDefinition.description} 14 | - ${FileReadTool.functionDefinition.name}: ${FileReadTool.functionDefinition.description} 15 | - ${CodeEditTool.functionDefinition.name}: ${CodeEditTool.functionDefinition.description} 16 | - ${CommandExecutionTool.functionDefinition.name}: ${CommandExecutionTool.functionDefinition.description} 17 | - ${UrlFetchTool.functionDefinition.name}: ${UrlFetchTool.functionDefinition.description} 18 | - ${WebSearchTool.functionDefinition.name}: ${WebSearchTool.functionDefinition.description} 19 | 20 | You can use these tools to help you with the user's request. 21 | 22 | When using the web-search tool, make sure you also use the url-fetch tool to read the content of the result URLs if needed. 23 | 24 | 25 | The discussion is about the code of the current project/folder. Always use the relevant tool to learn about the 26 | project if you are unsure before giving answer. 27 | """ 28 | 29 | class CoderAgent(additionalPrompt: String = "", model: String = "") 30 | extends BaseChatAgent(coderAgentPrompt + additionalPrompt, model) { 31 | 32 | final val availableTools = List( 33 | CodeSearchTool, 34 | ProjectStructureTool, 35 | FileReadTool, 36 | CodeEditTool, 37 | CommandExecutionTool, 38 | UrlFetchTool, 39 | WebSearchTool 40 | ) 41 | 42 | override def toolDefinitionList: List[FunctionDefinition] = 43 | availableTools.map(_.functionDefinition) 44 | 45 | override def toolExecution(toolCall: ToolCallDescription): String = { 46 | availableTools.find(_.functionDefinition.name == toolCall.name) match { 47 | case Some(tool) => tool.execute(toolCall.arguments) 48 | case None => 49 | throw new IllegalArgumentException(s"Tool ${toolCall.name} not found") 50 | } 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /.github/workflows/scala.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | tags: 7 | - 'v*.*.*' 8 | pull_request: 9 | branches: [ "main" ] 10 | 11 | permissions: 12 | contents: write # needed for creating releases 13 | packages: write # needed for creating releases 14 | 15 | jobs: 16 | build: 17 | runs-on: ubuntu-latest 18 | strategy: 19 | matrix: 20 | java-version: [22] 21 | 22 | steps: 23 | - uses: actions/checkout@v4 24 | with: 25 | fetch-depth: 0 # Fetch all history for tags 26 | 27 | - name: Set up JDK 28 | uses: actions/setup-java@v4 29 | with: 30 | java-version: ${{ matrix.java-version }} 31 | distribution: 'temurin' 32 | cache: 'sbt' 33 | 34 | - name: Setup sbt 35 | uses: sbt/setup-sbt@v1 36 | 37 | - name: Run tests 38 | run: sbt test 39 | 40 | - name: Build universal package 41 | run: sbt 'set version := "${{ github.ref_name }}"' Universal/packageBin 42 | 43 | - name: Get package path 44 | id: get_package 45 | run: | 46 | echo "PACKAGE_PATH=$(find target/universal -name 'supercoder-*.zip' -type f)" >> $GITHUB_ENV 47 | echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV 48 | 49 | - name: Create a Release 50 | if: startsWith(github.ref, 'refs/tags/') 51 | uses: softprops/action-gh-release@v1 52 | with: 53 | files: ${{ env.PACKAGE_PATH }} 54 | name: Release ${{ env.VERSION }} 55 | body: | 56 | ## SuperCoder Release ${{ env.VERSION }} 57 | 58 | ### Installation 59 | Download the zip file and extract it to your desired location. 60 | 61 | ### Running 62 | After extraction, you can run the application using: 63 | ```bash 64 | ./bin/supercoder 65 | ``` 66 | 67 | ### Changes 68 | See the commit history for detailed changes. 69 | draft: false 70 | prerelease: false 71 | token: ${{ secrets.GITHUB_TOKEN }} 72 | 73 | - name: Upload artifact 74 | uses: actions/upload-artifact@v4 75 | with: 76 | name: supercoder-package 77 | path: ${{ env.PACKAGE_PATH }} 78 | retention-days: 5 79 | 80 | - name: Cleanup before cache 81 | run: | 82 | rm -rf "$HOME/.ivy2/local" || true 83 | find $HOME/Library/Caches/Coursier/v1 -name "ivydata-*.properties" -delete || true 84 | find $HOME/.ivy2/cache -name "ivydata-*.properties" -delete || true 85 | find $HOME/.cache/coursier/v1 -name "ivydata-*.properties" -delete || true 86 | find $HOME/.sbt -name "*.lock" -delete || true -------------------------------------------------------------------------------- /src/main/scala/com/supercoder/tools/ProjectStructureTool.scala: -------------------------------------------------------------------------------- 1 | package com.supercoder.tools 2 | 3 | import com.openai.models.FunctionDefinition 4 | import com.supercoder.base.Tool 5 | import com.supercoder.lib.Console.{green, yellow} 6 | 7 | import java.io.File 8 | import scala.io.Source 9 | import scala.collection.mutable.ArrayBuffer 10 | 11 | object ProjectStructureTool extends Tool { 12 | private val gitignorePatterns = ArrayBuffer.empty[String] 13 | 14 | override val functionDefinition = FunctionDefinition 15 | .builder() 16 | .name("project-structure") 17 | .description("Get the structure of the current project. Arguments: 'null'") 18 | .build() 19 | 20 | override def execute(arguments: String): String = { 21 | println(green("🔎 Reading project structure...")) 22 | loadGitignore() 23 | buildProjectTree(new File("."), 0) 24 | } 25 | 26 | private def loadGitignore(): Unit = { 27 | val gitignoreFile = new File(".gitignore") 28 | if (gitignoreFile.exists()) { 29 | val source = Source.fromFile(gitignoreFile) 30 | try { 31 | gitignorePatterns ++= source.getLines() 32 | .map(_.trim) 33 | .filter(_.nonEmpty) 34 | .filter(!_.startsWith("#")) 35 | 36 | // Include some default patterns for common directories 37 | gitignorePatterns ++= List( 38 | "node_modules", 39 | "build", 40 | "dist", 41 | "out", 42 | "target", 43 | ".idea", 44 | ".vscode", 45 | ".git" 46 | ) 47 | } finally { 48 | source.close() 49 | } 50 | } 51 | } 52 | 53 | private def isIgnored(file: File): Boolean = { 54 | val path = file.getPath.replace(File.separator, "/") 55 | val relativePath = path.stripPrefix("./") 56 | 57 | gitignorePatterns.exists { pattern => 58 | val isDirPattern = pattern.endsWith("/") 59 | val cleanPattern = pattern.stripSuffix("/") 60 | 61 | if (isDirPattern && !file.isDirectory) { 62 | false 63 | } else if (cleanPattern.contains("/")) { 64 | // Handle patterns with path components 65 | relativePath.matches(cleanPattern 66 | .replace(".", "\\.") 67 | .replace("*", "[^/]*") 68 | .replace("**", ".*")) 69 | } else { 70 | // Handle simple patterns 71 | file.getName.matches(cleanPattern 72 | .replace(".", "\\.") 73 | .replace("*", ".*")) 74 | } 75 | } 76 | } 77 | 78 | private def buildProjectTree(dir: File, depth: Int): String = { 79 | val builder = new StringBuilder 80 | val prefix = "│ " * depth 81 | 82 | if (depth == 0) { 83 | builder.append(".\n") 84 | } 85 | 86 | val (dirs, files) = dir.listFiles().partition(_.isDirectory) 87 | val filteredDirs = dirs.filterNot(isIgnored).sortBy(_.getName) 88 | val filteredFiles = files.filterNot(isIgnored).sortBy(_.getName) 89 | 90 | filteredDirs.foreach { subDir => 91 | builder.append(s"$prefix├── ${subDir.getName}/\n") 92 | builder.append(buildProjectTree(subDir, depth + 1)) 93 | } 94 | 95 | filteredFiles.foreach { file => 96 | builder.append(s"$prefix├── ${file.getName}\n") 97 | } 98 | 99 | builder.toString() 100 | } 101 | } -------------------------------------------------------------------------------- /src/main/scala/com/supercoder/ui/TerminalChat.scala: -------------------------------------------------------------------------------- 1 | package com.supercoder.ui 2 | 3 | import com.supercoder.base.BaseChatAgent 4 | import com.supercoder.lib.Console 5 | import com.supercoder.lib.Console.{blue, bold, green, underline} 6 | import com.supercoder.build.BuildInfo 7 | import org.jline.reader.{LineReader, LineReaderBuilder, Reference, Widget} 8 | import org.jline.terminal.{Terminal, TerminalBuilder} 9 | 10 | object TerminalChat { 11 | 12 | def clearScreen(): Unit = { 13 | print("\u001b[2J") 14 | print("\u001b[H") 15 | } 16 | 17 | def printHeader(agent: BaseChatAgent): Unit = { 18 | clearScreen() 19 | println(blue("█▀ █░█ █▀█ █▀▀ █▀█ █▀▀ █▀█ █▀▄ █▀▀ █▀█")) 20 | println(blue("▄█ █▄█ █▀▀ ██▄ █▀▄ █▄▄ █▄█ █▄▀ ██▄ █▀▄")) 21 | println(blue(s"v${BuildInfo.version}")) 22 | println() 23 | println(blue(s"Model: ${agent.selectedModel}")) 24 | println(blue("Type '/help' for available commands.\n")) 25 | } 26 | 27 | def showHelp(): Unit = { 28 | println(underline("Available commands:")) 29 | println(s" ${bold("/help")} - Display this help message") 30 | println(s" ${bold("/clear")} - Clear the terminal screen") 31 | println(s" ${bold("exit")}\t- Terminate the chat session") 32 | println(s" ${bold("bye")}\t- Terminate the chat session\n") 33 | println("Just type any message to chat with the agent.") 34 | println("To insert a new line in your message, use Shift+Enter.") 35 | } 36 | 37 | def run(agent: BaseChatAgent): Unit = { 38 | printHeader(agent) 39 | val terminal: Terminal = TerminalBuilder.builder().system(true).build() 40 | val reader: LineReader = LineReaderBuilder.builder().terminal(terminal).build() 41 | 42 | // Add a widget to insert a newline when Shift+Enter is pressed. 43 | // Note: The escape sequence for Shift+Enter can vary between terminals; 44 | // here we assume "\u001b[13;2u" is the sequence for Shift+Enter. 45 | reader.getWidgets.put("insert-newline", new Widget { 46 | override def apply(): Boolean = { 47 | // Insert a newline character into the current buffer 48 | reader.getBuffer.write("\n") 49 | // Refresh display to show the new line in the prompt 50 | reader.callWidget(LineReader.REDRAW_LINE) 51 | reader.callWidget(LineReader.REDISPLAY) 52 | true 53 | } 54 | }) 55 | 56 | // Bind the Shift+Enter key sequence to our widget. 57 | // The escape sequence here (\u001b[13;2u) might need adjustment based on your terminal emulator. 58 | val mainKeyMap = reader.getKeyMaps.get(LineReader.MAIN) 59 | mainKeyMap.bind(new Reference("insert-newline"), "\u001b[13;2u") 60 | 61 | var keepRunning = true 62 | while (keepRunning) { 63 | try { 64 | val input = reader.readLine(bold("> ")) 65 | if (input == null) { 66 | keepRunning = false 67 | } else { 68 | input.trim match { 69 | case "" => // ignore empty input 70 | case "/help" => showHelp() 71 | case "/clear" => 72 | clearScreen() 73 | printHeader(agent) 74 | case "exit" | "bye" => 75 | println(blue("\nChat session terminated. Goodbye!")) 76 | keepRunning = false 77 | case message => 78 | agent.chat(message) 79 | } 80 | } 81 | } catch { 82 | case _: org.jline.reader.UserInterruptException => // Handle ctrl+C gracefully 83 | println(blue("\nChat session terminated. Goodbye!")) 84 | keepRunning = false 85 | case e: Exception => e.printStackTrace() 86 | } 87 | } 88 | } 89 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SuperCoder 2 | 3 | Welcome to SuperCoder! A coding agent that runs in your terminal. 4 | 5 | image 6 | 7 | ## Features 8 | 9 | SuperCoder equips you with an array of powerful tools to simplify your development workflow. It offers the following features: 10 | 11 | - Code Search: Performs complex code searches across your project to quickly locate specific patterns. 12 | - Project Structure Exploration: Provides an organized view of your project's folders and files, making navigation a breeze. 13 | - Code Editing: Enables you to modify your codebase seamlessly with natural language commands. 14 | - Bug Fixing: Automatically fixes bugs and implements improvements based on your detailed requests. 15 | - Cursor Rules Support: Leverages Cursor Rules to intelligently understand and modify your code at precise locations. 16 | 17 | ## Installation 18 | 19 | We have a pre-built binary that works on Linux, MacOS and Windows. 20 | 21 | - **Step 1:** Download the ZIP bundle from the [Release](https://github.com/huytd/supercoder/releases) page. 22 | 23 | image 24 | 25 | - **Step 2:** Extract to a folder on your computer, and make sure the `bin/supercoder` or `bin/supercoder.bat` binary is accessible in your system's `PATH`. 26 | - **Step 3:** In your terminal, run the `supercoder` command from any folder you want to work on. 27 | 28 | ## Usage 29 | 30 | ### Configure the Agent 31 | 32 | #### Option 1: Using OpenAI API 33 | Before running the agent, you need to have the `OPENAI_API_KEY` environment variable configured. You can obtain an API key by signing up at [OpenAI](https://platform.openai.com/). 34 | 35 | ```shell 36 | export OPENAI_API_KEY= 37 | export OPENAI_MODEL= # default to "o3-mini", so watch your wallet 38 | ``` 39 | 40 | #### Option 2: Using Local Models or any OpenAI-compatible API 41 | If you have a local model or any other OpenAI-compatible API, you can configure SuperCoder to use it, by setting the following environment variables: 42 | 43 | ```shell 44 | export SUPERCODER_BASE_URL= 45 | export SUPERCODER_API_KEY= 46 | export SUPERCODER_MODEL= 47 | ``` 48 | 49 | Note that, if you are using Google Gemini, you will need to set `SUPERCODER_GEMINI_MODE=true` as well. 50 | 51 | It's important to note that the model you are using should support tools calling. 52 | 53 | ### Running the Coding Agent 54 | 55 | After building the project, extract and run the generated binary. Once running, you can type natural language commands such as: 56 | 57 | - "Search for usage of function XYZ" 58 | - "Edit file path/to/file.scala to add a new method" 59 | - "Show me the project structure" 60 | 61 | The agent will interpret your commands and invoke the appropriate tool. 62 | 63 | ### Interacting with the Tools 64 | 65 | SuperCoder supports the following tools: 66 | 67 | - **CodeSearchTool**: Helps in searching for specific code patterns across the project. 68 | - **CodeEditTool**: Allows editing of files within the project. 69 | - **FileReadTool**: Reads and displays file content. 70 | - **ProjectStructureTool**: Provides an overview of the project folders and files. 71 | - **CodeExecutionTool**: Executes shell commands based on the agent's assessment. 72 | 73 | ## Development 74 | 75 | For development purposes, follow these instructions to set up your environment: 76 | 77 | ### Prerequisites 78 | 79 | - Java 8 or above 80 | - SBT (Scala Build Tool) 81 | 82 | ### Setup 83 | 84 | 1. Clone the repository: 85 | ```bash 86 | git clone 87 | cd SuperCoder 88 | ``` 89 | 90 | 2. Build the project using SBT: 91 | ```bash 92 | sbt compile 93 | ``` 94 | 95 | 3. Run tests to ensure everything is working as expected: 96 | ```bash 97 | sbt test 98 | ``` 99 | 100 | ## Contributing 101 | 102 | Contributions, issues, and feature requests are welcome! Please check the [issues page](issues) if you want to contribute. 103 | 104 | ## License 105 | 106 | This project is open source and available under the MIT License. 107 | -------------------------------------------------------------------------------- /src/main/scala/com/supercoder/base/Agent.scala: -------------------------------------------------------------------------------- 1 | package com.supercoder.base 2 | 3 | import com.openai.client.okhttp.OpenAIOkHttpClient 4 | import com.openai.core.http.Headers 5 | import com.openai.models.* 6 | import com.supercoder.Main 7 | import com.supercoder.Main.AppConfig 8 | import com.supercoder.lib.Console.{blue, red, green, yellow, bold as consoleBold, underline} 9 | import io.circe.* 10 | import io.circe.generic.auto.* 11 | import io.circe.parser.* 12 | 13 | import java.util 14 | import java.util.Optional 15 | import scala.collection.mutable.ListBuffer 16 | 17 | object AgentConfig { 18 | val BasePrompt = s""" 19 | # Tool calling 20 | For each function call, return a json object with function name and arguments within <@TOOL> XML tags: 21 | 22 | <@TOOL> 23 | {"name": , "arguments": ""} 24 | 25 | 26 | The arguments value is ALWAYS a JSON-encoded string, when there is no arguments, use empty string "". 27 | 28 | For example: 29 | <@TOOL> 30 | {"name": "file-read", "arguments": "{\"fileName\": \"example.txt\"}"} 31 | 32 | 33 | <@TOOL> 34 | {"name": "project-structure", "arguments": ""} 35 | 36 | 37 | The client will response with <@TOOL_RESULT>[content] XML tags to provide the result of the function call. 38 | Use it to continue the conversation with the user. 39 | 40 | # Safety 41 | Please refuse to answer any unsafe or unethical requests. 42 | Do not execute any command that could harm the system or access sensitive information. 43 | When you want to execute some potentially unsafe command, please ask for user confirmation first before generating the tool call instruction. 44 | 45 | # Agent Instructions 46 | """ 47 | val OpenAIAPIBaseURL: String = sys.env.get("SUPERCODER_BASE_URL") 48 | .orElse(sys.env.get("OPENAI_BASE_URL")) 49 | .getOrElse("https://api.openai.com/v1") 50 | 51 | val OpenAIModel: String = sys.env.get("SUPERCODER_MODEL") 52 | .orElse(sys.env.get("OPENAI_MODEL")) 53 | .getOrElse(ChatModel.O3_MINI.toString) 54 | 55 | val OpenAIAPIKey: String = sys.env.get("SUPERCODER_API_KEY") 56 | .orElse(sys.env.get("OPENAI_API_KEY")) 57 | .getOrElse(throw new RuntimeException("You need to config SUPERCODER_API_KEY or OPENAI_API_KEY variable")) 58 | } 59 | 60 | case class ToolCallDescription( 61 | name: String = "", 62 | arguments: String = "", 63 | ) { 64 | 65 | def addName(name: Optional[String]): ToolCallDescription = 66 | copy(name = this.name + name.orElse("")) 67 | 68 | def addArguments(arguments: Optional[String]): ToolCallDescription = 69 | copy(arguments = this.arguments + arguments.orElse("")) 70 | 71 | } 72 | 73 | abstract class BaseChatAgent(prompt: String, model: String = AgentConfig.OpenAIModel) { 74 | private val client = OpenAIOkHttpClient.builder() 75 | .baseUrl(AgentConfig.OpenAIAPIBaseURL) 76 | .apiKey(AgentConfig.OpenAIAPIKey) 77 | .headers(Headers.builder() 78 | .put("HTTP-Referer", "https://github.com/huytd/supercoder/") 79 | .put("X-Title", "SuperCoder") 80 | .build()) 81 | .build() 82 | 83 | private var chatHistory: ListBuffer[ChatCompletionMessageParam] = 84 | ListBuffer.empty 85 | 86 | def selectedModel: String = if (model.nonEmpty) model else AgentConfig.OpenAIModel 87 | 88 | def toolExecution(toolCall: ToolCallDescription): String 89 | def toolDefinitionList: List[FunctionDefinition] 90 | 91 | private def addMessageToHistory(message: ChatCompletionMessageParam): Unit = 92 | chatHistory = chatHistory :+ message 93 | 94 | private def createAssistantMessageBuilder( 95 | content: String 96 | ): ChatCompletionAssistantMessageParam.Builder = { 97 | ChatCompletionAssistantMessageParam 98 | .builder() 99 | .content(content) 100 | .refusal("") 101 | } 102 | 103 | private def createUserMessageBuilder( 104 | content: String 105 | ): ChatCompletionUserMessageParam.Builder = 106 | ChatCompletionUserMessageParam 107 | .builder() 108 | .content(content) 109 | 110 | // Helper method to build base parameters with system prompt and chat history 111 | private def buildBaseParams(): ChatCompletionCreateParams.Builder = { 112 | val params = ChatCompletionCreateParams 113 | .builder() 114 | .addSystemMessage(AgentConfig.BasePrompt + prompt) 115 | .model(selectedModel) 116 | .temperature(AppConfig.temperature) 117 | .topP(AppConfig.top_p) 118 | 119 | // Add all messages from chat history 120 | chatHistory.foreach(params.addMessage) 121 | params 122 | } 123 | 124 | def chat(message: String): Unit = { 125 | // Add user message to chat history 126 | if (message.nonEmpty) { 127 | addMessageToHistory( 128 | ChatCompletionMessageParam.ofUser( 129 | createUserMessageBuilder(message).build() 130 | ) 131 | ) 132 | } 133 | 134 | val params = buildBaseParams().build() 135 | val streamResponse = client.chat().completions().createStreaming(params) 136 | val currentMessageBuilder = new StringBuilder() 137 | var currentToolCall = ToolCallDescription() 138 | 139 | import sun.misc.{Signal, SignalHandler} 140 | var cancelStreaming = false 141 | var streamingStarted = false 142 | 143 | val intSignal = new Signal("INT") 144 | val oldHandler = Signal.handle(intSignal, new SignalHandler { 145 | override def handle(sig: Signal): Unit = { 146 | if (streamingStarted) { 147 | cancelStreaming = true 148 | } // else ignore Ctrl+C if streaming hasn't started 149 | } 150 | }) 151 | 152 | try { 153 | val it = streamResponse.stream().iterator() 154 | streamingStarted = true 155 | val wordBuffer = new StringBuilder() 156 | var isInToolTag = false 157 | var currentToolTagEndMarker: Option[String] = None 158 | val toolStart = "<@TOOL>" 159 | val toolEnd = "" 160 | val toolResultStart = "<@TOOL_RESULT>" 161 | val toolResultEnd = "" 162 | 163 | while(it.hasNext && !cancelStreaming) { 164 | val chunk = it.next() 165 | val delta = chunk.choices.getFirst.delta 166 | 167 | if (delta.content().isPresent) { 168 | wordBuffer.append(delta.content().get()) 169 | 170 | var continueProcessingBuffer = true 171 | while (continueProcessingBuffer) { 172 | continueProcessingBuffer = false // Assume we can't process further unless proven otherwise 173 | 174 | if (isInToolTag) { 175 | // Currently inside a tool tag, looking for the end marker 176 | currentToolTagEndMarker.foreach { endMarker => 177 | val endMarkerIndex = wordBuffer.indexOf(endMarker) 178 | if (endMarkerIndex != -1) { 179 | // Found the end marker 180 | val contentBeforeEnd = wordBuffer.substring(0, endMarkerIndex) 181 | val tagContentWithMarker = contentBeforeEnd + endMarker 182 | 183 | if (contentBeforeEnd.nonEmpty) { 184 | if (AppConfig.isDebugMode) print(red(contentBeforeEnd)) // Print content if debug 185 | currentMessageBuilder.append(contentBeforeEnd) 186 | } 187 | if (AppConfig.isDebugMode) print(red(endMarker)) // Print end marker if debug 188 | currentMessageBuilder.append(endMarker) 189 | 190 | wordBuffer.delete(0, tagContentWithMarker.length) 191 | isInToolTag = false 192 | currentToolTagEndMarker = None 193 | continueProcessingBuffer = true // Re-evaluate buffer from the start 194 | } else { 195 | // End marker not found, process safe portion if possible 196 | val safeLength = wordBuffer.length - endMarker.length + 1 197 | if (safeLength > 0) { 198 | val safeContent = wordBuffer.substring(0, safeLength) 199 | if (AppConfig.isDebugMode) print(red(safeContent)) // Print safe content if debug 200 | currentMessageBuilder.append(safeContent) 201 | wordBuffer.delete(0, safeLength) 202 | // No continueProcessingBuffer = true, need more data for the end tag 203 | } 204 | } 205 | } 206 | } else { 207 | // Not inside a tool tag, looking for a start marker 208 | val toolStartIndex = wordBuffer.indexOf(toolStart) 209 | val toolResultStartIndex = wordBuffer.indexOf(toolResultStart) 210 | 211 | // Find the earliest start tag index 212 | val firstTagIndex = (toolStartIndex, toolResultStartIndex) match { 213 | case (ts, tr) if ts >= 0 && tr >= 0 => Math.min(ts, tr) 214 | case (ts, -1) if ts >= 0 => ts 215 | case (-1, tr) if tr >= 0 => tr 216 | case _ => -1 217 | } 218 | 219 | if (firstTagIndex != -1) { 220 | // Found a start tag 221 | val textBeforeTag = wordBuffer.substring(0, firstTagIndex) 222 | if (textBeforeTag.nonEmpty) { 223 | print(blue(textBeforeTag)) 224 | currentMessageBuilder.append(textBeforeTag) 225 | } 226 | 227 | // Determine which tag was found and process it 228 | val (startTag, endMarker) = if (firstTagIndex == toolStartIndex) { 229 | (toolStart, toolEnd) 230 | } else { 231 | (toolResultStart, toolResultEnd) 232 | } 233 | 234 | if (AppConfig.isDebugMode) print(red(startTag)) 235 | currentMessageBuilder.append(startTag) 236 | 237 | wordBuffer.delete(0, firstTagIndex + startTag.length) 238 | isInToolTag = true 239 | currentToolTagEndMarker = Some(endMarker) 240 | continueProcessingBuffer = true // Re-evaluate buffer from the start 241 | } else { 242 | // No start tag found, process safe portion 243 | val maxTagLen = Math.max(toolStart.length, toolResultStart.length) 244 | val safeLength = wordBuffer.length - maxTagLen + 1 245 | if (safeLength > 0) { 246 | val safeContent = wordBuffer.substring(0, safeLength) 247 | print(blue(safeContent)) 248 | currentMessageBuilder.append(safeContent) 249 | wordBuffer.delete(0, safeLength) 250 | // No continueProcessingBuffer = true, need more data for a potential tag start 251 | } 252 | } 253 | } 254 | } // End while(continueProcessingBuffer) 255 | } // End if delta.content().isPresent 256 | } // End of main while(it.hasNext) loop 257 | 258 | // After the loop, process any remaining content in the buffer 259 | // Run the same logic, but process fully if tag not found 260 | var continueProcessingBuffer = true 261 | while (continueProcessingBuffer && wordBuffer.nonEmpty) { 262 | continueProcessingBuffer = false // Assume we stop unless a full tag is processed 263 | if (isInToolTag) { 264 | currentToolTagEndMarker.foreach { endMarker => 265 | val endMarkerIndex = wordBuffer.indexOf(endMarker) 266 | if (endMarkerIndex != -1) { 267 | val contentBeforeEnd = wordBuffer.substring(0, endMarkerIndex) 268 | val tagContentWithMarker = contentBeforeEnd + endMarker 269 | if (contentBeforeEnd.nonEmpty) { 270 | if (AppConfig.isDebugMode) print(red(contentBeforeEnd)) 271 | currentMessageBuilder.append(contentBeforeEnd) 272 | } 273 | if (AppConfig.isDebugMode) print(red(endMarker)) 274 | currentMessageBuilder.append(endMarker) 275 | wordBuffer.delete(0, tagContentWithMarker.length) 276 | isInToolTag = false 277 | currentToolTagEndMarker = None 278 | continueProcessingBuffer = true // Processed a tag, might be more 279 | } else { 280 | // End marker not found, process the rest (end of stream) 281 | if (AppConfig.isDebugMode) print(red(wordBuffer.toString)) 282 | currentMessageBuilder.append(wordBuffer.toString) 283 | wordBuffer.clear() 284 | } 285 | } 286 | } else { 287 | val toolStartIndex = wordBuffer.indexOf(toolStart) 288 | val toolResultStartIndex = wordBuffer.indexOf(toolResultStart) 289 | val firstTagIndex = (toolStartIndex, toolResultStartIndex) match { 290 | case (ts, tr) if ts >= 0 && tr >= 0 => Math.min(ts, tr) 291 | case (ts, -1) if ts >= 0 => ts 292 | case (-1, tr) if tr >= 0 => tr 293 | case _ => -1 294 | } 295 | if (firstTagIndex != -1) { 296 | val textBeforeTag = wordBuffer.substring(0, firstTagIndex) 297 | if (textBeforeTag.nonEmpty) { 298 | print(blue(textBeforeTag)) 299 | currentMessageBuilder.append(textBeforeTag) 300 | } 301 | val (startTag, endMarker) = if (firstTagIndex == toolStartIndex) { 302 | (toolStart, toolEnd) 303 | } else { 304 | (toolResultStart, toolResultEnd) 305 | } 306 | if (AppConfig.isDebugMode) print(red(startTag)) 307 | currentMessageBuilder.append(startTag) 308 | wordBuffer.delete(0, firstTagIndex + startTag.length) 309 | isInToolTag = true 310 | currentToolTagEndMarker = Some(endMarker) 311 | continueProcessingBuffer = true // Processed a tag, might be more 312 | } else { 313 | // No start tag found, process the rest (end of stream) 314 | print(blue(wordBuffer.toString)) 315 | currentMessageBuilder.append(wordBuffer.toString) 316 | wordBuffer.clear() 317 | } 318 | } 319 | } 320 | 321 | if (cancelStreaming) { 322 | println(blue("\nStreaming cancelled by user")) 323 | } 324 | } catch { 325 | case e: Exception => e.printStackTrace() 326 | } finally { 327 | // Restore original SIGINT handler and close stream 328 | Signal.handle(intSignal, oldHandler) 329 | streamResponse.close() 330 | if (currentMessageBuilder.nonEmpty) { 331 | println() 332 | val messageContent = currentMessageBuilder.toString() 333 | addMessageToHistory( 334 | ChatCompletionMessageParam.ofAssistant( 335 | createAssistantMessageBuilder(messageContent) 336 | .build() 337 | ) 338 | ) 339 | 340 | // Check if the message contains a tool call 341 | val toolCallRegex = """(?s)<@TOOL>(.*?)""".r 342 | val toolCallMatch = toolCallRegex.findFirstMatchIn(messageContent).map(_.group(1)) 343 | if (toolCallMatch.isDefined) { 344 | val toolCallJson = toolCallMatch.get 345 | try { 346 | val parseResult: Either[Error, ToolCallDescription] = decode[ToolCallDescription](toolCallJson) 347 | currentToolCall = parseResult.getOrElse(ToolCallDescription()) 348 | } catch { 349 | case e: Exception => 350 | println(red(s"Error parsing tool call: ${e.getMessage}")) 351 | } 352 | } 353 | } 354 | if (currentToolCall.name.nonEmpty) { 355 | handleToolCall(currentToolCall) 356 | } 357 | } 358 | } 359 | 360 | private def handleToolCall(toolCall: ToolCallDescription): Unit = { 361 | val toolResult = toolExecution(toolCall) 362 | 363 | // Add the result as assistant's message 364 | addMessageToHistory( 365 | ChatCompletionMessageParam.ofAssistant( 366 | createAssistantMessageBuilder(s"Calling ${toolCall.name} tool...").build() 367 | ) 368 | ) 369 | addMessageToHistory( 370 | ChatCompletionMessageParam.ofUser( 371 | createUserMessageBuilder(s"<@TOOL_RESULT>${toolResult}").build() 372 | ) 373 | ) 374 | 375 | // Trigger follow-up response from assistant 376 | chat("") 377 | } 378 | 379 | } 380 | --------------------------------------------------------------------------------