├── test ├── test_helper.exs ├── kino_copilot_test.exs └── kino_copilot │ └── code_writer_cell_test.exs ├── .formatter.exs ├── lib ├── kino_copilot.ex ├── kino_copilot │ ├── application.ex │ └── code_writer_cell.ex └── assets │ └── code_writer_cell │ ├── main.js │ └── main.css ├── .gitignore ├── mix.exs ├── README.md └── mix.lock /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /test/kino_copilot_test.exs: -------------------------------------------------------------------------------- 1 | defmodule KinoCopilotTest do 2 | use ExUnit.Case 3 | doctest KinoCopilot 4 | end 5 | -------------------------------------------------------------------------------- /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /lib/kino_copilot.ex: -------------------------------------------------------------------------------- 1 | defmodule KinoCopilot do 2 | @moduledoc """ 3 | Documentation for `KinoCopilot`. 4 | """ 5 | 6 | @doc """ 7 | Hello world. 8 | 9 | ## Examples 10 | 11 | iex> KinoCopilot.hello() 12 | :world 13 | 14 | """ 15 | def hello do 16 | :world 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/kino_copilot/application.ex: -------------------------------------------------------------------------------- 1 | defmodule KinoCopilot.Application do 2 | @moduledoc false 3 | 4 | use Application 5 | 6 | @impl true 7 | def start(_type, _args) do 8 | Kino.SmartCell.register(KinoCopilot.CodeWriterCell) 9 | 10 | children = [] 11 | opts = [strategy: :one_for_one, name: KinoDB.Supervisor] 12 | Supervisor.start_link(children, opts) 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where third-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | kino_copilot-*.tar 24 | 25 | # Temporary files, for example, from tests. 26 | /tmp/ 27 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule KinoCopilot.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :kino_copilot, 7 | version: "0.1.2", 8 | description: "Bringing ChatGPT to you livebook", 9 | elixir: "~> 1.15", 10 | start_permanent: Mix.env() == :prod, 11 | deps: deps(), 12 | package: package() 13 | ] 14 | end 15 | 16 | # Run "mix help compile.app" to learn about applications. 17 | def application do 18 | [ 19 | mod: {KinoCopilot.Application, []} 20 | ] 21 | end 22 | 23 | # Run "mix help deps" to learn about dependencies. 24 | defp deps do 25 | [ 26 | {:kino, "~> 0.7"}, 27 | {:openai, "~> 0.5.4"}, 28 | {:ex_doc, ">= 0.0.0", only: :dev, runtime: false} 29 | ] 30 | end 31 | 32 | def package do 33 | [ 34 | maintainers: ["Thomas Millar"], 35 | licenses: ["Apache-2.0"], 36 | links: %{ 37 | "GitHub" => "https://github.com/thmsmlr/kino_copilot" 38 | } 39 | ] 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /test/kino_copilot/code_writer_cell_test.exs: -------------------------------------------------------------------------------- 1 | defmodule KinoCopilot.CodeWriterCellTest do 2 | use ExUnit.Case 3 | 4 | alias KinoCopilot.CodeWriterCell 5 | 6 | test "parse_function_call handles triple quote" do 7 | response = "{\n \"code\": \"\"\"\n IO.puts(\"Hello world\") \"\"\"\n}" 8 | 9 | assert "IO.puts(\"Hello world\")" = 10 | CodeWriterCell.parse_function_call(response) |> String.trim() 11 | end 12 | 13 | test "parse_function_call handles extra escaped quotes" do 14 | response = "{\n \"code\": \"\n IO.puts(\\\"Hello world\\\") \"\n}" 15 | 16 | assert "IO.puts(\"Hello world\")" = 17 | CodeWriterCell.parse_function_call(response) |> String.trim() 18 | end 19 | 20 | test "parse_function_call handles extra unescaped quotes" do 21 | response = "{\n \"code\": \"\n IO.puts(\"Hello world\") \"\n}" 22 | 23 | assert "IO.puts(\"Hello world\")" = 24 | CodeWriterCell.parse_function_call(response) |> String.trim() 25 | end 26 | 27 | test "parse_function_call handles proper JSON response" do 28 | response = "{\n \"code\": \"IO.puts(\\\"Hello world\\\")\"\n}" 29 | 30 | assert "IO.puts(\"Hello world\")" = 31 | CodeWriterCell.parse_function_call(response) |> String.trim() 32 | 33 | assert "IO.puts(\"Hello world\")" = 34 | Jason.decode!(response)["code"] |> String.trim() 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kino Copilot 2 | 3 | [![Floki version](https://img.shields.io/hexpm/v/kino_copilot.svg)](https://hex.pm/packages/kino_copilot) 4 | [![Hex Docs](https://img.shields.io/badge/hex-docs-lightgreen.svg)](https://hexdocs.pm/kino_copilot/) 5 | 6 | Bringing the power of ChatGPT into [Livebook](https://livebook.dev)! 7 | KinoCopilot is a series of Kino SmartCells which allow you to have an AI Copilot help you write code. 8 | 9 | ![demo](https://github.com/thmsmlr/kino_copilot/assets/167206/4d38b1a3-4ca5-4898-a762-8170c6072aa9) 10 | 11 | ## Installation 12 | 13 | To bring KinoCopilot to Livebook all you need to do is Mix.install/2: 14 | 15 | ```elixir 16 | Mix.install([ 17 | {:kino_copilot, "~> 0.1.2"} 18 | ]) 19 | ``` 20 | 21 | By default we'll use the `LB_OPENAI_API_KEY` for the API key. 22 | Optionally, however, you can explicitly pass in your API key and specify which model to use. 23 | 24 | ```elixir 25 | Mix.install( 26 | [ 27 | {:kino_copilot, "~> 0.1.2"} 28 | ], 29 | config: [ 30 | kino_copilot: [ 31 | api_key: System.fetch_env!("LB_OPENAI_API_KEY"), 32 | model: "gpt-3.5-turbo" 33 | ] 34 | ] 35 | ) 36 | ``` 37 | 38 | ## Development 39 | 40 | KinoCopilot is still an active development. 41 | If you want to contribute, here are some instructions that will help get you up and running. 42 | 43 | First, you're going to want to install the package from source. 44 | 45 | ```elixir 46 | Mix.install([ 47 | {:kino_copilot, path: "/Users/thomas/code/kino_copilot"}, 48 | ]) 49 | ``` 50 | 51 | Then, if you're modifying any of the front-end bits, you'll want to make sure you have tailwind running in the background, recompiling the CSS. 52 | 53 | ```bash 54 | $ npx tailwindcss -o lib/assets/code_writer_cell/main.css --content lib/assets/code_writer_cell/main.js --watch 55 | ``` 56 | 57 | In the future when we have specialized code writer cells for various languages you'll want to run this command for the specific smart cell you are working on. 58 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "certifi": {:hex, :certifi, "2.12.0", "2d1cca2ec95f59643862af91f001478c9863c2ac9cb6e2f89780bfd8de987329", [:rebar3], [], "hexpm", "ee68d85df22e554040cdb4be100f33873ac6051387baf6a8f6ce82272340ff1c"}, 3 | "earmark_parser": {:hex, :earmark_parser, "1.4.36", "487ea8ef9bdc659f085e6e654f3c3feea1d36ac3943edf9d2ef6c98de9174c13", [:mix], [], "hexpm", "a524e395634bdcf60a616efe77fd79561bec2e930d8b82745df06ab4e844400a"}, 4 | "ex_doc": {:hex, :ex_doc, "0.30.6", "5f8b54854b240a2b55c9734c4b1d0dd7bdd41f71a095d42a70445c03cf05a281", [:mix], [{:earmark_parser, "~> 1.4.31", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "bd48f2ddacf4e482c727f9293d9498e0881597eae6ddc3d9562bd7923375109f"}, 5 | "hackney": {:hex, :hackney, "1.19.1", "59de4716e985dd2b5cbd4954fa1ae187e2b610a9c4520ffcb0b1653c3d6e5559", [:rebar3], [{:certifi, "~> 2.12.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "8aa08234bdefc269995c63c2282cf3cd0e36febe3a6bfab11b610572fdd1cad0"}, 6 | "httpoison": {:hex, :httpoison, "2.1.0", "655fd9a7b0b95ee3e9a3b535cf7ac8e08ef5229bab187fa86ac4208b122d934b", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "fc455cb4306b43827def4f57299b2d5ac8ac331cb23f517e734a4b78210a160c"}, 7 | "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, 8 | "jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"}, 9 | "kino": {:hex, :kino, "0.10.0", "ae598b5ddabc4834585c895a1ee36dcad9d771d86188637c3e28a3f589f17fa1", [:mix], [{:nx, "~> 0.1", [hex: :nx, repo: "hexpm", optional: true]}, {:table, "~> 0.1.2", [hex: :table, repo: "hexpm", optional: false]}], "hexpm", "2239ec384fe527f173ceab3d290b45272f095250164f9794c4a65a714683d228"}, 10 | "makeup": {:hex, :makeup, "1.1.0", "6b67c8bc2882a6b6a445859952a602afc1a41c2e08379ca057c0f525366fc3ca", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0a45ed501f4a8897f580eabf99a2e5234ea3e75a4373c8a52824f6e873be57a6"}, 11 | "makeup_elixir": {:hex, :makeup_elixir, "0.16.1", "cc9e3ca312f1cfeccc572b37a09980287e243648108384b97ff2b76e505c3555", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "e127a341ad1b209bd80f7bd1620a15693a9908ed780c3b763bccf7d200c767c6"}, 12 | "makeup_erlang": {:hex, :makeup_erlang, "0.1.2", "ad87296a092a46e03b7e9b0be7631ddcf64c790fa68a9ef5323b6cbb36affc72", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "f3f5a1ca93ce6e092d92b6d9c049bcda58a3b617a8d888f8e7231c85630e8108"}, 13 | "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, 14 | "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, 15 | "nimble_parsec": {:hex, :nimble_parsec, "1.3.1", "2c54013ecf170e249e9291ed0a62e5832f70a476c61da16f6aac6dca0189f2af", [:mix], [], "hexpm", "2682e3c0b2eb58d90c6375fc0cc30bc7be06f365bf72608804fb9cffa5e1b167"}, 16 | "openai": {:hex, :openai, "0.5.4", "2abc7bc6a72ad1732c16d3f0914aa54f4de14b174a4c70c1b2d7934f0fe2646f", [:mix], [{:httpoison, "~> 2.0", [hex: :httpoison, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "72add1d3dcbf3ed7d24ce3acf51e8b2f374b23305b0fc1d5f6acff35c567b267"}, 17 | "parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"}, 18 | "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"}, 19 | "table": {:hex, :table, "0.1.2", "87ad1125f5b70c5dea0307aa633194083eb5182ec537efc94e96af08937e14a8", [:mix], [], "hexpm", "7e99bc7efef806315c7e65640724bf165c3061cdc5d854060f74468367065029"}, 20 | "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, 21 | } 22 | -------------------------------------------------------------------------------- /lib/assets/code_writer_cell/main.js: -------------------------------------------------------------------------------- 1 | import * as Vue from "https://cdn.jsdelivr.net/npm/vue@3.2.26/dist/vue.esm-browser.prod.js"; 2 | 3 | export function init(ctx, payload) { 4 | ctx.importCSS("main.css"); 5 | ctx.importCSS("https://fonts.googleapis.com/css2?family=Inter:wght@400;500&display=swap"); 6 | 7 | const app = Vue.createApp({ 8 | components: {}, 9 | 10 | template: ` 11 |
13 | 14 |
15 |
16 |
17 | 20 |
21 |
22 |
{{ payload.errors[0] }}
23 |
24 |
25 |
26 | 27 |
28 | 29 | 30 | 31 | 32 |
33 |
35 |