Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 61 additions & 23 deletions cmd/server-bot/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,14 @@ import (

"hairy-botter/internal/ai/adapters"
"hairy-botter/internal/ai/agent"
"hairy-botter/internal/ai/gemini"
"hairy-botter/internal/ai/providers"
"hairy-botter/internal/config"
"hairy-botter/internal/history"
"hairy-botter/internal/mcpserver"
"hairy-botter/internal/rag"
"hairy-botter/internal/server"

"github.com/firebase/genkit/go/core/api"
"github.com/firebase/genkit/go/genkit"
)

Expand Down Expand Up @@ -75,11 +76,37 @@ func main() {
return
}

if cfg.APIKeys.Gemini == "" {
logger.Error("GEMINI_API_KEY is not set in config or env")
// Build the main AI provider.
mainProvider, err := providers.New(cfg.Provider, providerCfgFor(cfg.Provider, cfg))
if err != nil {
logger.Error("failed to create AI provider", slog.String("provider", cfg.Provider), slog.String("err", err.Error()))
return
}

// Build the embedder provider (may differ from the main provider).
embedderProviderName := cfg.Capabilities.Rag.EmbedderProvider
embedderCfg := providerCfgFor(embedderProviderName, cfg)
if cfg.Capabilities.Rag.EmbedderBaseURL != "" {
embedderCfg.BaseURL = cfg.Capabilities.Rag.EmbedderBaseURL
}
embedderProvider, err := providers.New(embedderProviderName, embedderCfg)
if err != nil {
logger.Error("failed to create embedder provider", slog.String("provider", embedderProviderName), slog.String("err", err.Error()))
return
}

// Collect distinct plugins for genkit.Init.
g := genkit.Init(context.Background(), genkit.WithPlugins(distinctPlugins(mainProvider.Plugin(), embedderProvider.Plugin())...))

model, err := mainProvider.Model(g, cfg.Model)
if err != nil {
logger.Error("failed to define model", slog.String("err", err.Error()))
return
}

searchEnable := !cfg.GeminiSearchDisabled
customModelConfig := mainProvider.GenerateOptions(cfg.Model, searchEnable, cfg.GeminiThinkingLevel)

historySummary := 0
if cfg.Capabilities.HistorySummary.Enabled {
historySummary = 20
Expand All @@ -101,30 +128,14 @@ func main() {
}
logger.Info("MCP servers from config", slog.Int("count", len(mcpServers)))

searchEnable := !cfg.GeminiSearchDisabled

// Initialize the Gemini AI logic
ga := gemini.ConfigPlugin(cfg.APIKeys.Gemini)
g := genkit.Init(context.Background(), genkit.WithPlugins(ga))

model, err := gemini.ConfigModel(g, ga, cfg.Model)
if err != nil {
logger.Error("failed to define model", slog.String("err", err.Error()))
return
}
customModelConfig := gemini.GenerateOptions(cfg.Model, searchEnable, cfg.GeminiThinkingLevel)

var ragL *rag.Logic
if cfg.Capabilities.Rag.Enabled && cfg.Capabilities.Rag.Directory != "" {
// First load/created embedder config
embedder, err := gemini.ConfigEmbedder(g, ga, cfg.Capabilities.Rag.EmbeddingModel)
embedder, err := embedderProvider.Embedder(g, cfg.Capabilities.Rag.EmbeddingModel)
if err != nil {
logger.Error("failed to define embedder", slog.String("err", err.Error()))

return
}

// Init the RAG
ragL, err = rag.New(logger, cfg.Capabilities.Rag.Directory, adapters.NewEmbedder(g, embedder))
if err != nil {
logger.Error("failed to create RAG logic", slog.String("err", err.Error()))
Expand All @@ -141,7 +152,6 @@ func main() {
aiLogic, err := agent.New(logger, g, model, hist, mcpServers, ragL, systemPrompt, customModelConfig, cfg.Context)
if err != nil {
logger.Error("failed to create AI logic", slog.String("err", err.Error()))

return
}

Expand Down Expand Up @@ -228,7 +238,7 @@ func main() {

stopCh := make(chan os.Signal, 1)
signal.Notify(stopCh, os.Interrupt, syscall.SIGTERM)
finishedCh := make(chan struct{}) // Signal the end of the graceful shutdown
finishedCh := make(chan struct{})
go func() {
<-stopCh
logger.Info("shutting down server")
Expand Down Expand Up @@ -262,8 +272,36 @@ func main() {
logger.Error("server failed", slog.String("err", err.Error()))
}
} else {
// Wait if no chat proxy to keep agent alive
logger.Info("agent running without chat proxy")
}
<-finishedCh
}

// providerCfgFor returns the providers.Config for the named provider from cfg.
func providerCfgFor(name string, cfg *config.Config) providers.Config {
switch name {
case "openai":
return providers.Config{
APIKey: cfg.Providers.OpenAI.APIKey,
BaseURL: cfg.Providers.OpenAI.BaseURL,
}
default: // gemini
return providers.Config{
APIKey: cfg.Providers.Gemini.APIKey,
BaseURL: cfg.Providers.Gemini.BaseURL,
}
}
}

// distinctPlugins returns a deduplicated slice of api.Plugin by name.
func distinctPlugins(ps ...api.Plugin) []api.Plugin {
seen := make(map[string]bool, len(ps))
out := make([]api.Plugin, 0, len(ps))
for _, p := range ps {
if !seen[p.Name()] {
seen[p.Name()] = true
out = append(out, p)
}
}
return out
}
23 changes: 19 additions & 4 deletions config.yaml.example
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,12 @@ agent_config:
# ---------------------------------------------------------
# 3. Universal Settings (Evaluated by all modes)
# ---------------------------------------------------------
model: "gemini-flash-latest"
gemini_search_disabled: true # Gemini specific config

# Provider for the main AI model: "gemini" (default) or "openai"
provider: "gemini"

model: "gemini-flash-latest" # gemini: e.g. "gemini-2.5-flash"; openai: e.g. "gpt-4o"
gemini_search_disabled: true # Gemini-specific; ignored for other providers
log_level: warning


Expand All @@ -37,7 +41,9 @@ capabilities:
rag:
enabled: true
directory: "./knowledge_base"
embedding_model: "gemini-embedding-001"
# embedder_provider: "gemini" # defaults to top-level provider; can be different
# embedder_base_url: "" # overrides the provider's base_url for the embedder only
embedding_model: "gemini-embedding-001" # openai default: "text-embedding-3-small"
history_summary:
enabled: true
message_count: 20
Expand Down Expand Up @@ -80,7 +86,16 @@ context:
# args: ["--city", "New York"]

# ---------------------------------------------------------
# API Keys (Fallback config, can be overridden by env vars)
# 5. Provider Credentials
# API keys can also be set via env vars: GEMINI_API_KEY, OPENAI_API_KEY
# ---------------------------------------------------------
# providers:
# gemini:
# api_key: "your_gemini_api_key_here"
# openai:
# api_key: "your_openai_api_key_here"
# base_url: "" # optional; override for any OpenAI-compatible endpoint

# Legacy (still supported, merged into providers.gemini.api_key automatically):
# api_keys:
# gemini: "your_gemini_api_key_here"
5 changes: 5 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,12 @@ require (
github.com/mattn/go-colorable v0.1.2 // indirect
github.com/mattn/go-isatty v0.0.8 // indirect
github.com/mbleigh/raymond v0.0.0-20250414171441-6b3a58ab9e0a // indirect
github.com/openai/openai-go v1.8.2 // indirect
github.com/spf13/cast v1.7.1 // indirect
github.com/tidwall/gjson v1.18.0 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
github.com/tidwall/sjson v1.2.5 // indirect
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
Expand Down
12 changes: 12 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mbleigh/raymond v0.0.0-20250414171441-6b3a58ab9e0a h1:v2cBA3xWKv2cIOVhnzX/gNgkNXqiHfUgJtA3r61Hf7A=
github.com/mbleigh/raymond v0.0.0-20250414171441-6b3a58ab9e0a/go.mod h1:Y6ghKH+ZijXn5d9E7qGGZBmjitx7iitZdQiIW97EpTU=
github.com/openai/openai-go v1.8.2 h1:UqSkJ1vCOPUpz9Ka5tS0324EJFEuOvMc+lA/EarJWP8=
github.com/openai/openai-go v1.8.2/go.mod h1:g461MYGXEXBVdV5SaR/5tNzNbSfwTBBefwc+LlDCK0Y=
github.com/philippgille/chromem-go v0.7.0 h1:4jfvfyKymjKNfGxBUhHUcj1kp7B17NL/I1P+vGh1RvY=
github.com/philippgille/chromem-go v0.7.0/go.mod h1:hTd+wGEm/fFPQl7ilfCwQXkgEUxceYh86iIdoKMolPo=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
Expand All @@ -80,6 +82,16 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
Expand Down
113 changes: 0 additions & 113 deletions internal/ai/gemini/config.go

This file was deleted.

Loading
Loading