From 2366e182710c6f361bb1716ca96ef410694cd555 Mon Sep 17 00:00:00 2001 From: michaeljguarino Date: Sun, 10 Nov 2024 14:11:01 -0500 Subject: [PATCH] Incremental AI improvements (#1577) --- .../api/v1alpha1/deploymentsettings_types.go | 2 +- lib/console/ai/evidence/base.ex | 6 +- lib/console/ai/evidence/stack_run.ex | 2 +- lib/console/ai/fixer.ex | 2 +- lib/console/ai/fixer/base.ex | 18 ++++- lib/console/ai/fixer/parent.ex | 76 +++++++++++++++++-- lib/console/ai/fixer/service.ex | 8 +- lib/console/ai/fixer/stack.ex | 4 +- lib/console/ai/provider.ex | 3 +- lib/console/ai/provider/anthropic.ex | 20 +++-- 10 files changed, 110 insertions(+), 31 deletions(-) diff --git a/go/controller/api/v1alpha1/deploymentsettings_types.go b/go/controller/api/v1alpha1/deploymentsettings_types.go index 3e2ffea5d3..1735cfcae3 100644 --- a/go/controller/api/v1alpha1/deploymentsettings_types.go +++ b/go/controller/api/v1alpha1/deploymentsettings_types.go @@ -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 { diff --git a/lib/console/ai/evidence/base.ex b/lib/console/ai/evidence/base.ex index 963ebf1f08..8c53db2149 100644 --- a/lib/console/ai/evidence/base.ex +++ b/lib/console/ai/evidence/base.ex @@ -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 @@ -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) -> diff --git a/lib/console/ai/evidence/stack_run.ex b/lib/console/ai/evidence/stack_run.ex index af883eaf69..5f3cb45b5a 100644 --- a/lib/console/ai/evidence/stack_run.ex +++ b/lib/console/ai/evidence/stack_run.ex @@ -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 _ -> [] diff --git a/lib/console/ai/fixer.ex b/lib/console/ai/fixer.ex index ede2d08db1..24bdbcaea5 100644 --- a/lib/console/ai/fixer.ex +++ b/lib/console/ai/fixer.ex @@ -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 diff --git a/lib/console/ai/fixer/base.ex b/lib/console/ai/fixer/base.ex index 67f233e493..ddb103c8a7 100644 --- a/lib/console/ai/fixer/base.ex +++ b/lib/console/ai/fixer/base.ex @@ -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) @@ -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 diff --git a/lib/console/ai/fixer/parent.ex b/lib/console/ai/fixer/parent.ex index b59195fc1f..8df391e25f 100644 --- a/lib/console/ai/fixer/parent.ex +++ b/lib/console/ai/fixer/parent.ex @@ -3,19 +3,25 @@ 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 ] @@ -23,5 +29,65 @@ defmodule Console.AI.Fixer.Parent do _ -> [] 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 diff --git a/lib/console/ai/fixer/service.ex b/lib/console/ai/fixer/service.ex index 332ad11a85..38cf130138 100644 --- a/lib/console/ai/fixer/service.ex +++ b/lib/console/ai/fixer/service.ex @@ -8,9 +8,9 @@ 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: @@ -18,11 +18,11 @@ defmodule Console.AI.Fixer.Service do #{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}" diff --git a/lib/console/ai/fixer/stack.ex b/lib/console/ai/fixer/stack.ex index 435b3d0a87..149bfe509f 100644 --- a/lib/console/ai/fixer/stack.ex +++ b/lib/console/ai/fixer/stack.ex @@ -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: @@ -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}" diff --git a/lib/console/ai/provider.ex b/lib/console/ai/provider.ex index 1c00d2d44e..8e76787aae 100644 --- a/lib/console/ai/provider.ex +++ b/lib/console/ai/provider.ex @@ -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 """ diff --git a/lib/console/ai/provider/anthropic.ex b/lib/console/ai/provider/anthropic.ex index ecf0bd7acb..2763cc23d8 100644 --- a/lib/console/ai/provider/anthropic.ex +++ b/lib/console/ai/provider/anthropic.ex @@ -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"} @@ -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