@@ -3,7 +3,9 @@ WORKDIR /src
|
||||
COPY go.mod ./
|
||||
COPY cmd ./cmd
|
||||
COPY internal ./internal
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags="-s -w" -o /out/glpi-ai-agent ./cmd/agent
|
||||
RUN go test ./... && \
|
||||
go vet ./... && \
|
||||
CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags="-s -w" -o /out/glpi-ai-agent ./cmd/agent
|
||||
|
||||
# One-shot helper used by docker compose to prepare the persistent volume for
|
||||
# the distroless non-root runtime user (UID/GID 65532).
|
||||
@@ -16,5 +18,6 @@ COPY --from=build /out/glpi-ai-agent /app/glpi-ai-agent
|
||||
COPY knowledge /app/knowledge
|
||||
VOLUME ["/app/data"]
|
||||
EXPOSE 8080
|
||||
HEALTHCHECK --interval=15s --timeout=3s --start-period=10s --retries=4 CMD ["/app/glpi-ai-agent", "healthcheck"]
|
||||
USER 65532:65532
|
||||
ENTRYPOINT ["/app/glpi-ai-agent"]
|
||||
|
||||
@@ -27,6 +27,10 @@ import (
|
||||
)
|
||||
|
||||
func main() {
|
||||
if len(os.Args) == 2 && os.Args[1] == "healthcheck" {
|
||||
runHealthcheck()
|
||||
return
|
||||
}
|
||||
cfg, err := config.Load()
|
||||
if err != nil {
|
||||
slog.Error("configuration invalid", "error", err)
|
||||
@@ -204,6 +208,22 @@ func main() {
|
||||
slog.Info("shutdown complete")
|
||||
}
|
||||
|
||||
func runHealthcheck() {
|
||||
client := &http.Client{Timeout: 2 * time.Second}
|
||||
req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8080/healthz", nil)
|
||||
if err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func waitForOllamaPool(ctx context.Context, client *ollama.Client, retryInterval time.Duration) error {
|
||||
if retryInterval < 2*time.Second {
|
||||
retryInterval = 5 * time.Second
|
||||
|
||||
@@ -325,6 +325,7 @@ func Load() (Config, error) {
|
||||
CommunicationSalutation: env("COMMUNICATION_SALUTATION", "Guten Tag,"),
|
||||
CommunicationClosing: env("COMMUNICATION_CLOSING", "Mit freundlichen Grüßen"),
|
||||
CommunicationSignature: env("COMMUNICATION_SIGNATURE", "IT-Service"),
|
||||
AIContentLabelEnabled: envBool("AI_CONTENT_LABEL_ENABLED", true),
|
||||
AutoCategory: envBool("AUTO_CATEGORY", true),
|
||||
AutoReply: envBool("AUTO_REPLY", false),
|
||||
PriorityEnabled: envBool("PRIORITY_ENABLED", true),
|
||||
@@ -651,6 +652,12 @@ func (c Config) Validate() error {
|
||||
return fmt.Errorf("KNOWLEDGE_VECTOR_BACKEND must be one of: local, dual, neuroforge (got %q)", c.KnowledgeVectorBackend)
|
||||
}
|
||||
if c.KnowledgeVectorBackend == "dual" || c.KnowledgeVectorBackend == "neuroforge" {
|
||||
if len(c.NeuroForgeAPIKey) < 24 {
|
||||
return errors.New("NEUROFORGE_API_KEY must contain at least 24 characters for dual/neuroforge mode")
|
||||
}
|
||||
if isPlaceholder(c.NeuroForgeAPIKey) {
|
||||
return errors.New("NEUROFORGE_API_KEY still contains a CHANGE_ME placeholder")
|
||||
}
|
||||
u, err := url.Parse(c.NeuroForgeURL)
|
||||
if err != nil || u.Scheme == "" || u.Host == "" || (u.Scheme != "http" && u.Scheme != "https") {
|
||||
return errors.New("NEUROFORGE_URL must be an absolute http(s) URL")
|
||||
|
||||
@@ -515,3 +515,40 @@ func TestValidateGLPIKBAutoReplyUncategorizedRequiresArticleAllowlist(t *testing
|
||||
t.Fatalf("expected uncategorized auto reply with explicit article allowlist to validate: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadReadsAIContentLabelEnabled(t *testing.T) {
|
||||
for key, value := range map[string]string{
|
||||
"GLPI_URL": "https://glpi.internal.example",
|
||||
"GLPI_CLIENT_ID": "client-id",
|
||||
"GLPI_CLIENT_SECRET": "real-secret-value",
|
||||
"GLPI_USERNAME": "svc-agent",
|
||||
"GLPI_PASSWORD": "real-password-value",
|
||||
"WEB_ALLOW_ANONYMOUS": "true",
|
||||
"AI_CONTENT_LABEL_ENABLED": "false",
|
||||
} {
|
||||
t.Setenv(key, value)
|
||||
}
|
||||
c, err := Load()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if c.AIContentLabelEnabled {
|
||||
t.Fatal("AI_CONTENT_LABEL_ENABLED=false was not loaded")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateNeuroForgeAPIKeyForRemoteVectorBackend(t *testing.T) {
|
||||
c := validConfig()
|
||||
c.KnowledgeVectorBackend = "dual"
|
||||
c.NeuroForgeURL = "http://neuroforge:8090"
|
||||
c.NeuroForgeSearchK = 128
|
||||
c.NeuroForgeTimeout = 5 * time.Second
|
||||
c.NeuroForgeAPIKey = "CHANGE_ME_NEUROFORGE_API_KEY"
|
||||
if err := c.Validate(); err == nil {
|
||||
t.Fatal("expected placeholder NEUROFORGE_API_KEY to be rejected")
|
||||
}
|
||||
c.NeuroForgeAPIKey = "integration-token-0123456789abcdef"
|
||||
if err := c.Validate(); err != nil {
|
||||
t.Fatalf("expected strong NEUROFORGE_API_KEY to validate: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user