feat: add database tables and API routes for agentic chat feature (#17570)

Backend portion of experimental `AgenticChat` feature:
- Adds database tables for chats and chat messages
- Adds functionality to stream messages from LLM providers using
`kylecarbs/aisdk-go`
- Adds API routes with relevant functionality (list, create, update
chats, insert chat message)
- Adds experiment `codersdk.AgenticChat`

---------

Co-authored-by: Kyle Carberry <kyle@carberry.com>
This commit is contained in:
Cian Johnston
2025-05-02 17:29:57 +01:00
committed by GitHub
parent 64b9bc1ca4
commit 544259b809
45 changed files with 4264 additions and 16 deletions

View File

@ -61,6 +61,7 @@ import (
"github.com/coder/serpent"
"github.com/coder/wgtunnel/tunnelsdk"
"github.com/coder/coder/v2/coderd/ai"
"github.com/coder/coder/v2/coderd/entitlements"
"github.com/coder/coder/v2/coderd/notifications/reports"
"github.com/coder/coder/v2/coderd/runtimeconfig"
@ -610,6 +611,22 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
)
}
aiProviders, err := ReadAIProvidersFromEnv(os.Environ())
if err != nil {
return xerrors.Errorf("read ai providers from env: %w", err)
}
vals.AI.Value.Providers = append(vals.AI.Value.Providers, aiProviders...)
for _, provider := range aiProviders {
logger.Debug(
ctx, "loaded ai provider",
slog.F("type", provider.Type),
)
}
languageModels, err := ai.ModelsFromConfig(ctx, vals.AI.Value.Providers)
if err != nil {
return xerrors.Errorf("create language models: %w", err)
}
realIPConfig, err := httpmw.ParseRealIPConfig(vals.ProxyTrustedHeaders, vals.ProxyTrustedOrigins)
if err != nil {
return xerrors.Errorf("parse real ip config: %w", err)
@ -640,6 +657,7 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
CacheDir: cacheDir,
GoogleTokenValidator: googleTokenValidator,
ExternalAuthConfigs: externalAuthConfigs,
LanguageModels: languageModels,
RealIPConfig: realIPConfig,
SSHKeygenAlgorithm: sshKeygenAlgorithm,
TracerProvider: tracerProvider,
@ -2621,6 +2639,77 @@ func redirectHTTPToHTTPSDeprecation(ctx context.Context, logger slog.Logger, inv
}
}
func ReadAIProvidersFromEnv(environ []string) ([]codersdk.AIProviderConfig, error) {
// The index numbers must be in-order.
sort.Strings(environ)
var providers []codersdk.AIProviderConfig
for _, v := range serpent.ParseEnviron(environ, "CODER_AI_PROVIDER_") {
tokens := strings.SplitN(v.Name, "_", 2)
if len(tokens) != 2 {
return nil, xerrors.Errorf("invalid env var: %s", v.Name)
}
providerNum, err := strconv.Atoi(tokens[0])
if err != nil {
return nil, xerrors.Errorf("parse number: %s", v.Name)
}
var provider codersdk.AIProviderConfig
switch {
case len(providers) < providerNum:
return nil, xerrors.Errorf(
"provider num %v skipped: %s",
len(providers),
v.Name,
)
case len(providers) == providerNum:
// At the next next provider.
providers = append(providers, provider)
case len(providers) == providerNum+1:
// At the current provider.
provider = providers[providerNum]
}
key := tokens[1]
switch key {
case "TYPE":
provider.Type = v.Value
case "API_KEY":
provider.APIKey = v.Value
case "BASE_URL":
provider.BaseURL = v.Value
case "MODELS":
provider.Models = strings.Split(v.Value, ",")
}
providers[providerNum] = provider
}
for _, envVar := range environ {
tokens := strings.SplitN(envVar, "=", 2)
if len(tokens) != 2 {
continue
}
switch tokens[0] {
case "OPENAI_API_KEY":
providers = append(providers, codersdk.AIProviderConfig{
Type: "openai",
APIKey: tokens[1],
})
case "ANTHROPIC_API_KEY":
providers = append(providers, codersdk.AIProviderConfig{
Type: "anthropic",
APIKey: tokens[1],
})
case "GOOGLE_API_KEY":
providers = append(providers, codersdk.AIProviderConfig{
Type: "google",
APIKey: tokens[1],
})
}
}
return providers, nil
}
// ReadExternalAuthProvidersFromEnv is provided for compatibility purposes with
// the viper CLI.
func ReadExternalAuthProvidersFromEnv(environ []string) ([]codersdk.ExternalAuthConfig, error) {