Skip to content

Commit

Permalink
Incremental AI improvements (#1577)
Browse files Browse the repository at this point in the history
  • Loading branch information
michaeljguarino authored Nov 10, 2024
1 parent 5aedafd commit 2366e18
Show file tree
Hide file tree
Showing 10 changed files with 110 additions and 31 deletions.
2 changes: 1 addition & 1 deletion go/controller/api/v1alpha1/deploymentsettings_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ func (in *AISettings) Attributes(ctx context.Context, c client.Client, namespace

attr.Anthropic = &console.AnthropicSettingsAttributes{
AccessToken: lo.ToPtr(token),
Model: in.OpenAI.Model,
Model: in.Anthropic.Model,
}
case console.AiProviderAzure:
if in.Azure == nil {
Expand Down
6 changes: 2 additions & 4 deletions lib/console/ai/evidence/base.ex
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ defmodule Console.AI.Evidence.Base do
end

def list_pods(ns, selector) do
&CoreV1.list_namespaced_pod!(ns, [label_selector: construct_label_selector(selector)] ++ k8s_page(&1, 500))
(fn c -> CoreV1.list_namespaced_pod!(ns, [label_selector: construct_label_selector(selector)] ++ k8s_page(c, 500)) end)
|> k8s_paginator(fn p -> !ready_condition?(p.status.conditions) end, nil, [])
|> ok()
end
Expand All @@ -77,9 +77,7 @@ defmodule Console.AI.Evidence.Base do
def k8s_page(_, limit), do: [limit: limit]

def k8s_paginator(query_fun, filter_fun, continue \\ nil, res \\ []) do
query_fun.(continue)
|> Kube.Utils.run()
|> case do
case Kube.Utils.run(query_fun.(continue)) do
{:ok, %{metadata: %MetaV1.ListMeta{continue: c}, items: items}} when is_binary(c) and is_list(items) ->
k8s_paginator(query_fun, filter_fun, c, res ++ Enum.filter(items, filter_fun))
{:ok, %{items: items}} when is_list(items) ->
Expand Down
2 changes: 1 addition & 1 deletion lib/console/ai/evidence/stack_run.ex
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ defimpl Console.AI.Evidence, for: Console.Schema.StackRun do

defp fetch_code(%StackRun{} = run) do
with {:ok, f} <- Stacks.tarstream(run),
{:ok, msgs} <- code_prompt(f, "I'll also include the relevant terraform code below, listed in the format #{file_fmt()}") do
{:ok, msgs} <- code_prompt(f, run.git.folder, "I'll also include the relevant terraform code below, listed in the format #{file_fmt()}") do
msgs
else
_ -> []
Expand Down
2 changes: 1 addition & 1 deletion lib/console/ai/fixer.ex
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,5 @@ defmodule Console.AI.Fixer do
|> when_ok(&fix/1)
end

defp ask(prompt), do: prompt ++ [{:user, "please provide the most cogent code or configuration change available based on the information I've already provided above to fix this issue. Be sure the Git repository and full file names that are needed to change."}]
defp ask(prompt), do: prompt ++ [{:user, "please provide the most straightforward code or configuration change available based on the information I've already provided above to fix this issue. Be sure the Git repository and full file names that are needed to change."}]
end
18 changes: 15 additions & 3 deletions lib/console/ai/fixer/base.ex
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
defmodule Console.AI.Fixer.Base do
use Console.AI.Evidence.Base
alias Console.Deployments.Tar
alias Console.Schema.Service

@extension_blacklist ~w(.tgz .png .jpeg .jpg .gz .tar)

Expand All @@ -9,12 +10,23 @@ defmodule Console.AI.Fixer.Base do

def file_fmt(), do: @format

def code_prompt(f, preface \\ @preface) do
def folder(%Service{git: %Service.Git{folder: folder}}) when is_binary(folder), do: folder
def folder(_), do: ""

def code_prompt(f, subfolder, preface \\ @preface) do
with {:ok, contents} <- Tar.tar_stream(f) do
Enum.filter(contents, fn {p, _} -> Path.extname(p) not in @extension_blacklist end)
|> Enum.map(fn {p, content} -> {:user, Jason.encode!(%{file: p, content: content})} end)
Enum.filter(contents, & !blacklist(elem(&1, 0)))
|> Enum.map(fn {p, content} -> {:user, Jason.encode!(%{file: Path.join(subfolder, p), content: content})} end)
|> prepend({:user, preface})
|> ok()
end
end

defp blacklist(filename) do
cond do
String.ends_with?(filename, "values.yaml.static") -> true
Path.extname(filename) in @extension_blacklist -> true
true -> false
end
end
end
76 changes: 71 additions & 5 deletions lib/console/ai/fixer/parent.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,91 @@ defmodule Console.AI.Fixer.Parent do
import Console.AI.Fixer.Base
alias Console.AI.Fixer.Service, as: SvcFixer
alias Console.Deployments.{Services}
alias Console.Schema.Service
alias Console.Schema.{Service, GlobalService, Stack, ServiceTemplate}

def parent_prompt(%Service{} = svc, info) do
def parent_prompt(%Stack{parent: %Service{} = svc}, info), do: do_parent_prompt(svc, info)
def parent_prompt(%Service{parent: %Service{} = svc}, info), do: do_parent_prompt(svc, info)
def parent_prompt(%Service{owner: %GlobalService{parent: %Service{}} = global}, info),
do: do_parent_prompt(global, info)
def parent_prompt(_, _), do: []

defp do_parent_prompt(%Service{} = svc, info) do
svc = Console.Repo.preload(svc, [:cluster, :repository, :parent])
with {:ok, f} <- Services.tarstream(svc),
{:ok, code} <- code_prompt(f) do
{:ok, code} <- code_prompt(f, folder(svc)) do
[
{:user, """
The #{info[:child]} is being instantiated using a Plural service-of-services structure, and will be represented as a #{info[:cr]} kubernetes
custom resource#{info[:cr_additional] || ""}. It is possible this is where the fix needs to happen, especially in the wiring of helm values or
terraform variables.
I will do my best to describe the service itself and show you the manifests that's sourcing that service below:
I will do my best to describe the service itself and show you the manifests that's defining that service below:
"""},
{:user, SvcFixer.svc_details(svc)} | code
]
else
_ -> []
end
end
def parent_prompt(_, _), do: []

defp do_parent_prompt(%GlobalService{} = global, info) do
%{parent: svc} = global = Console.Repo.preload(global, [:project, :service, :template, parent: [:cluster, :repository, :parent]])
with {:ok, f} <- Services.tarstream(svc),
{:ok, code} <- code_prompt(f, folder(svc)) do
[
{:user, """
The #{info[:child]} is being instantiated by a GlobalService named #{global.name} which is itself defined using a Plural service-of-services structure,
and will be represented as a GlobalService kubernetes custom resource. It is possible this is where the fix needs to happen, especially in the wiring of helm values.
I will do my best to describe the global service itself and show you the manifests that's defining that global service below:
"""},
{:user, global_details(global)} | code
]
else
_ -> []
end
end

defp do_parent_prompt(_, _), do: []

defp global_details(%GlobalService{service: %Service{}} = global) do
"""
The global service has name #{global.name}. It will define services on a set of targeted clusters using precise replication rules.
#{targeting(global)}
#{global_source(global)}
"""
end

defp global_source(%GlobalService{service: %Service{name: name}}) do
"""
The global service operates by cloning the #{name} service on each targeted cluster.
It's possible cluster metadata will dynamically template yaml from there to customize per cluster
"""
end

defp global_source(%GlobalService{template: %ServiceTemplate{} = template}) do
"""
The global service defines new services using the service template with json spec below:\n
#{Jason.encode!(Console.mapify(template))}
"""
end

defp global_source(_), do: ""

defp targeting(%GlobalService{} = global) do
"""
The global service replicates the service onto clusters matching the following criteria:
#{compress_and_join([
(if global.distro, do: "* matches kubernetes distribution: #{global.distro}", else: nil),
(if global.project, do: "* is a cluster within the project: #{global.project.name}", else: nil),
(if is_list(global.tags) and !Enum.empty?(global.tags),
do: "* matches clusters with tags: #{Enum.map(global.tags, &"#{&1.name}=#{&1.value}") |> Enum.join(",")}",
else: nil)
])}
"""
end
end
8 changes: 4 additions & 4 deletions lib/console/ai/fixer/service.ex
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,21 @@ defmodule Console.AI.Fixer.Service do
alias Console.Deployments.{Services}

def prompt(%Service{} = svc, insight) do
svc = Repo.preload(svc, [:cluster, :repository, :parent])
svc = Repo.preload(svc, [:cluster, :repository, :parent, owner: :parent])
with {:ok, f} <- Services.tarstream(svc),
{:ok, code} <- code_prompt(f) do
{:ok, code} <- code_prompt(f, folder(svc)) do
Enum.concat([
{:user, """
We've found the following insight about a Plural service that is currently in #{svc.status} state:
#{insight}
We'd like you to suggest a simple code or configuration change that can fix the issues identified in that insight.
I'll do my best to list all the needed resources below.
I'll do my best to list all the needed resources below. Additional useful context is that Plural templates any file with a `.liquid` extension with the metadata of the cluster, or secrets attached to the service itself.
"""},
{:user, svc_details(svc)} | code
], Parent.parent_prompt(
svc.parent,
svc,
child: "#{svc.name} service",
cr: "ServiceDeployment",
cr_additional: " specifying the name #{svc.name} and namespace #{svc.namespace}"
Expand Down
4 changes: 2 additions & 2 deletions lib/console/ai/fixer/stack.ex
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ defmodule Console.AI.Fixer.Stack do
def prompt(%Stack{} = stack, insight) do
stack = Repo.preload(stack, [:repository, :parent])
with {:ok, f} <- Stacks.tarstream(last_run(stack)),
{:ok, code} <- code_prompt(f) do
{:ok, code} <- code_prompt(f, stack.git.folder) do
Enum.concat([
{:user, """
We've found the following insight about a Plural Stack that is currently in #{stack.status} state:
Expand All @@ -22,7 +22,7 @@ defmodule Console.AI.Fixer.Stack do
"""},
{:user, stack_details(stack)} | code
], Parent.parent_prompt(
stack.parent,
stack,
child: "#{stack.name} stack",
cr: "InfrastructureStack",
cr_additional: " specifying the name #{stack.name}"
Expand Down
3 changes: 1 addition & 2 deletions lib/console/ai/provider.ex
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@ defmodule Console.AI.Provider do
give a concise but clear explanation of issues in your companies kubernetes infrastructure. The user is not necessarily
an expert in the domain, so please provide as much documentation and evidence as is necessary to explain what issue they're
facing. Please provide a clear summary and any details to debug what's going on with the case provided. You should guide users
to implement GitOps best practices, so avoid telling them to manually modify resources via kubectl or helm commands directly, although
kubectl commands can be used for gathering further info about the problem.
to implement GitOps best practices, so avoid telling them to manually modify resources via kubectl, helm or terraform commands directly.
"""}

@summary """
Expand Down
20 changes: 12 additions & 8 deletions lib/console/ai/provider/anthropic.ex
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,7 @@ defmodule Console.AI.Anthropic do
"""
@spec completion(t(), Console.AI.Provider.history) :: {:ok, binary} | Console.error
def completion(%__MODULE__{} = anthropic, messages) do
history = Enum.map(messages, fn {role, msg} -> %{role: anth_role(role), content: msg} end)
case chat(anthropic, history) do
case chat(anthropic, messages) do
{:ok, %MessageResponse{content: content}} ->
{:ok, format_content(content)}
{:ok, _} -> {:error, "could not generate an ai completion for this context"}
Expand All @@ -50,17 +49,22 @@ defmodule Console.AI.Anthropic do
end

defp chat(%__MODULE__{access_key: token, model: model}, history) do
body = Jason.encode!(%{
model: model || "claude-3-5-sonnet-20240620",
{system, history} = split(history)
url("/messages")
|> HTTPoison.post(Jason.encode!(%{
model: model || "claude-3-5-haiku-latest",
system: system,
messages: history,
max_tokens: @max_tokens
})

url("/messages")
|> HTTPoison.post(body, json_headers(token), @options)
}), json_headers(token), @options)
|> handle_response(MessageResponse.spec())
end

defp split([{:system, msg} | rest]), do: {msg, fmt_msgs(rest)}
defp split(hist), do: {nil, fmt_msgs(hist)}

defp fmt_msgs(msgs), do: Enum.map(msgs, fn {role, msg} -> %{role: anth_role(role), content: msg} end)

defp format_content(content) do
Enum.map(content, fn
%Content{type: "text", text: t} -> t
Expand Down

0 comments on commit 2366e18

Please sign in to comment.