Make AI do OS operations

To enable business processes, we'll often need OS operations. To enable this, we can knock up a tool, which can do that. There are serious security implications. You should probably do this sort of thing in a sandbox.

Here's a simple example of an OS tool. Note how simple this is to create.

import smithy4s.*
import smithy4s.deriving.{*, given}

import cats.effect.IO
import cats.effect.std.Console

@hints(smithy.api.Documentation("Local file and os operations"))
/** Local file and os operations
  */
trait OsTool derives API:
  /** Creates a temporary directory on the local file system.
    */
  def makeTempDir(dirPrefix: String): IO[String] =
    IO.println("Creating a temporary directory") >>
      IO.blocking {
        val outDir = os.temp.dir(deleteOnExit = false, prefix = dirPrefix).toString
        outDir.toString
      }

  def createOrOverwriteFileInDir(dir: String, fileName: String, contents: Option[String]): IO[String] =
    IO.println(s"Creating a file in $dir") >>
      IO.blocking {
        val filePath = os.Path(dir) / fileName
        os.write.over(filePath, contents.getOrElse(""))
        filePath.toString
      }

  def askForHelp(question: String): IO[String] =
    for
      _ <- Console[IO].println(s"I need guidance with: $question")
      n <- Console[IO].readLine
    yield n

end OsTool

The below mechanism, demonstrates how one may supply that tool to the AI, and dispatch an arbirary function call.

import io.github.quafadas.dairect.*
import cats.effect.IOApp
import cats.effect.IO
import scala.annotation.experimental
import smithy4s.Document
import smithy4s.kinds.PolyFunction
import smithy4s.deriving.{*, given}
import scala.concurrent.duration.*

import cats.effect.unsafe.implicits.global
import scala.concurrent.Future
import io.github.quafadas.dairect.ChatGpt.AiMessage
import smithy4s.json.Json
import smithy4s.Blob
import fs2.io.file.*
import cats.effect.ExitCode
import org.http4s.ember.client.EmberClientBuilder
import ciris.*

val osImpl = new OsTool() {}
// osImpl: OsTool = repl.MdocSession$MdocApp$$anon$1@19062630

val logFile = fs2.io.file.Path("easychat.txt")
// logFile: Path = easychat.txt
val chatGpt = ChatGpt.defaultAuthLogToFile(logFile).allocated.map(_._1).Ø
// chatGpt: ChatGpt = io.github.quafadas.dairect.ChatGpt$proxy$1@184ada50
val osTools = API[OsTool].liftService(osImpl)
// osTools: Free[<none>] = LiftedAlgebra(
//   alg = repl.MdocSession$MdocApp$$anon$1@19062630,
//   fk = smithy4s.kinds.PolyFunction5$$anon$11@75bc2c54
// )

val schema = ioToolGen.toJsonSchema(osTools)
// schema: Document = DArray(
//   value = ArraySeq(
//     DObject(
//       value = Map(
//         "type" -> DString(value = "function"),
//         "function" -> DObject(
//           value = Map(
//             "name" -> DString(value = "askForHelp"),
//             "description" -> DString(value = "askForHelp"),
//             "parameters" -> DObject(
//               value = Map(
//                 "type" -> DString(value = "object"),
//                 "required" -> DArray(value = ArraySeq(DString(value = "question"))),
//                 "properties" -> DObject(
//                   value = Map(
//                     "question" -> DObject(value = Map("type" -> DString(value = "string")))
//                   )
//                 )
//               )
//             )
//           )
//         )
//       )
//     ),
//     DObject(
//       value = Map(
//         "type" -> DString(value = "function"),
//         "function" -> DObject(
//           value = Map(
//             "name" -> DString(value = "readTextFile"),
//             "description" -> DString(value = "readTextFile"),
//             "parameters" -> DObject(
//               value = Map(
//                 "type" -> DString(value = "object"),
//                 "required" -> DArray(value = ArraySeq(DString(value = "filePath"))),
//                 "properties" -> DObject(
//                   value = Map(
//                     "filePath" -> DObject(value = Map("type" -> DString(value = "string")))
//                   )
//                 )
//               )
//             )
//           )
//         )
//       )
// ...
val osDispatch = ioToolGen.openAiSmithyFunctionDispatch(osTools)
// osDispatch: Function1[FunctionCall, IO[Document]] = io.github.quafadas.dairect.SmithyOpenAIUtil$$Lambda$5450/0x00000008019fefe8@7d610e52

val resp = chatGpt
  .chat(
    List(AiMessage.system("You are a helpful assistant"), AiMessage.user("Create a temporary directory")),
    tools = schema.some
  )
// resp: ChatResponse = ChatResponse(
//   id = "chatcmpl-A1Zrj881u9xhQkxL7tq5ov9vChhdV",
//   created = 1724939559,
//   model = "gpt-4o-mini-2024-07-18",
//   choices = List(
//     AiChoice(
//       message = AiAnswer(
//         role = "assistant",
//         content = None,
//         tool_calls = Some(
//           value = List(
//             ToolCall(
//               id = "call_VG5MHpXrZaGMo61ASSbXKVEw",
//               type = "function",
//               function = FunctionCall(
//                 name = "makeTempDir",
//                 description = None,
//                 arguments = Some(value = "{\"dirPrefix\":\"temp\"}")
//               )
//             )
//           )
//         )
//       ),
//       finish_reason = Some(value = "tool_calls")
//     )
//   ),
//   usage = AiTokenUsage(
//     completion_tokens = 16,
//     prompt_tokens = 126,
//     total_tokens = 142
//   )
// )

val toolCall = resp.choices.head
// toolCall: AiChoice = AiChoice(
//   message = AiAnswer(
//     role = "assistant",
//     content = None,
//     tool_calls = Some(
//       value = List(
//         ToolCall(
//           id = "call_VG5MHpXrZaGMo61ASSbXKVEw",
//           type = "function",
//           function = FunctionCall(
//             name = "makeTempDir",
//             description = None,
//             arguments = Some(value = "{\"dirPrefix\":\"temp\"}")
//           )
//         )
//       )
//     )
//   ),
//   finish_reason = Some(value = "tool_calls")
// )

val fctCall = toolCall.message.tool_calls.get.head
// fctCall: ToolCall = ToolCall(
//   id = "call_VG5MHpXrZaGMo61ASSbXKVEw",
//   type = "function",
//   function = FunctionCall(
//     name = "makeTempDir",
//     description = None,
//     arguments = Some(value = "{\"dirPrefix\":\"temp\"}")
//   )
// )
val out = osDispatch(fctCall.function).Ø
// out: Document = DString(value = "/tmp/temp5996774433281101439")

val tooloutcome = AiMessage.tool(tool_call_id = fctCall.id, content = Json.writeDocumentAsPrettyString(out))
// tooloutcome: AiMessage = AiMessage(
//   role = "tool",
//   content = Some(value = "\"/tmp/temp5996774433281101439\""),
//   tool_calls = None,
//   tool_call_id = Some(value = "call_VG5MHpXrZaGMo61ASSbXKVEw"),
//   name = None
// )

chatGpt
  .chat(
    List(
      AiMessage.system("You are a helpful assistant"),
      AiMessage.user("Create a temporary directory"),
      toolCall.toMessage.head,
      tooloutcome
    ),
    tools = schema.some
  )
// res0: ChatResponse = ChatResponse(
//   id = "chatcmpl-A1ZrkGCySjudnH9rjQa5SrxNroe0X",
//   created = 1724939560,
//   model = "gpt-4o-mini-2024-07-18",
//   choices = List(
//     AiChoice(
//       message = AiAnswer(
//         role = "assistant",
//         content = Some(
//           value = "A temporary directory has been created at the path: `/tmp/temp5996774433281101439`."
//         ),
//         tool_calls = None
//       ),
//       finish_reason = Some(value = "stop")
//     )
//   ),
//   usage = AiTokenUsage(
//     completion_tokens = 22,
//     prompt_tokens = 162,
//     total_tokens = 184
//   )
// )