diff --git a/MANIFEST.sha256 b/MANIFEST.sha256 index 94c9f34..0ed1e3f 100644 --- a/MANIFEST.sha256 +++ b/MANIFEST.sha256 @@ -2,6 +2,7 @@ 079e174cd38e79421469cb5fc79ff4d886015cd5ec97c538466f97d70ce5331a ./.env.example 813c6a53b389380997fc71835ef72d3b593e1808c4fcc16dc0c309608a389f40 ./.env.subagent.example 2d49d2ee955bf352cc7e76068ec842808c0bf7bffaacb6a129b641a78bbde0e5 ./.gitea/workflows/ci.yml +ed22fda7661db8203563611dc144998161cd024e161b0471d615eaf0defeb7db ./.gitea/workflows/release-tag-gitea.yml cefe3681ecaca9bbbf7fcc18e668c0208e12a2260b8527561d50818916cbf77a ./.gitea/workflows/release.yml e1ff71187cc3411a85067b964264011db7bd109a585ef7b9ea5b08bda039d813 ./.gitignore 0451d06da4395dff53c5f5085bd5a78eb54a5a22ff2dbf9a717dddd13babf91a ./BUILD-AND-RELEASE.md @@ -26,30 +27,35 @@ a2d5e268fe70979caccd895156b3d33bd61a3cc7e660ab7a5b07066849b3e248 ./VERSION e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 ./backups/.gitkeep 8127e9db5e5e0af1d88770dc8fa60b381de45dbcc843262698cf9501409b4d58 ./deploy/searxng/settings.yml 316c40e929f7341b9a639d9351438d8da9d62e40783c1f05ade80ee2540ee08f ./deployments/README.md -30e63c048256d33c46931b9d9746652fd078845f446b8e395e7eb362f88b67eb ./deployments/agent/.env 0df71e0cca461b82f4991368c6f53c11bcaf62fdbdb3fda0f3aad1555e1bad36 ./deployments/agent/docker-compose.build.yml c6c255cb0f19421618a8cc8fce3fdcefd707d39e8a371f4136b1445e3ecb7ca3 ./deployments/agent/docker-compose.yml -8a8ed00d937a8d2620e8a9713754e2b0e9c048fd2bb9b0d014edf8978bf7a961 ./deployments/combined/.env +c0f27c27b2d12919070493198d58f1efa90a0c8fa898b156400d7548d07971a7 ./deployments/combined/compose.yml +1474093f126f8cd140fcb5f45505fad114ebe37cdbc19526e1532b4d2c52b1ba ./deployments/combined/compose.yml.old 1cdd0d8f851d7f7d8533443d02496a9f368e4a4194faf39408ea67487b5f77d7 ./deployments/combined/docker-compose.build.yml 67697cc388446999f21528e19df491d86aaf1141c2cbfbc3182d0149a4958458 ./deployments/combined/docker-compose.yml -2284148fdb09db7cabba462eadfa9ba10f88a2c458279d774c5c28cb5ea31b94 ./deployments/cpu-subagent/.env +c14fe9d60e1e9c216fef80d3b596b670c56231dae72f94f9461c809ac4f24504 ./deployments/cpu-subagent/README.md fa1f4ebcf753f7d3ba7b49a1bf9b9738d2f9f3cf1b88b3ed4403a819de196b4a ./deployments/cpu-subagent/docker-compose.yml dead361b179b6f761b160bd302f304b62941781cd542aba29eb5cfca8ec0497c ./deployments/cpu-subagent/preflight.sh -28180b0dc3cc22d90b84ef3f2dffa30f05e4255b8dd15824d3d200d6628fbd5e ./deployments/gpu-subagent/.env +8bd16c76f2da34f50e827e131eefb260b1c56b7263f8b92b42795fdb719e9f40 ./deployments/gpu-subagent/README.md d38f8433fb34c0d9b88b788cc95219c8b8ca96e6de93243c9a9eb8fc64946e40 ./deployments/gpu-subagent/docker-compose.yml 36d44f3bde10d228881273f04a309cbddba394c8bc559cb48f7d37ee8f2f4e3a ./deployments/gpu-subagent/preflight.sh -aee3a1b054e2f84b4f3d148067960d7d19b2edaf9eeb5dd8b02fbee4703481af ./deployments/knowledge/.env 81013b617610a4903ac7e237c231d62e0aa5caa083d73a5a838316db31d9dc55 ./deployments/knowledge/docker-compose.build.yml 6686e18e07453dcfdb5e57c0d456833ddf1e7a921fdd5f05abf324719fc69d5a ./deployments/knowledge/docker-compose.yml -966a910f90a25f5b434f386b0ea9828c5b4e270166f7101efcd582a37f9d13fe ./deployments/master/.env +16f8e8224c0c46e5c79f35b322695046edb13e608d176cb0b9f25590f11b6b01 ./deployments/master/README.md +e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 ./deployments/master/backups/.gitkeep +8127e9db5e5e0af1d88770dc8fa60b381de45dbcc843262698cf9501409b4d58 ./deployments/master/deploy/searxng/settings.yml a7d49be8630c475230585db69ad977339d6b3e95c36379e75682f6ff124ab588 ./deployments/master/docker-compose.yml +e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 ./deployments/master/knowledge/.gitkeep +6cd2e5bdf4c2a213f548e4f7a139faeeb56ff74f54e6da72e430fbd71342d64d ./deployments/master/monitoring/README.md 0c2fc23f8f6deced618e025cc66fa5711349439814d8a2c6d0f347990b4632e1 ./deployments/master/monitoring/grafana/dashboards/neuroforge-master-subagents.json +2c7c42913333222438dc3dd9baaab21aa28bd9aff033a75f85c6981da00325be ./deployments/master/monitoring/grafana/provisioning/dashboards/dashboards.yml 083e25e212b70a4c0fb5cd8d2cd5a5735f8ae230809c7fac40187f785a8eb43d ./deployments/master/monitoring/grafana/provisioning/dashboards/neuroforge.yml 72305a9fded6b40fe740ec82dac495f006990e6e37de754d1fcd01d93da4834a ./deployments/master/monitoring/grafana/provisioning/datasources/prometheus.yml 3e42499e55aa2d5c211728809e22d713162c88a493610c2107da17891c3567ad ./deployments/master/monitoring/prometheus/alerts.yml aac90d7e75a79256f77d730ca80fc1824e86f4299192526910b6f0226cedcbb9 ./deployments/master/monitoring/prometheus/prometheus.yml.template +5e3459ce74ef9f754e4a8439977f7da21563dc6863f760f49ef8699fe56ff603 ./deployments/master/monitoring/validate.sh 57fa3c3be7f476c791da0fd7e9985b38be82f1d9a816d13d75f645f29de57e86 ./deployments/master/preflight.sh -8dc996e535cdb7867421a02536f8b4ed0eb5d0dfdad0959ddd9db90fc04c7d4e ./deployments/ollama/.env +e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 ./deployments/master/staging/.gitkeep 15a8680873d1dbde2b7deca0a30b786f49f4498f499269e9cd24c33e0b243333 ./deployments/ollama/docker-compose.yml 8d4943a3e023ed3b24312ff0b406298026b4d32aac6873b0c8488812b20dba4b ./deployments/ollama/pull-models.sh 5d2ca8625ccb84cb868c3f7450a0c63fc128967aab5677363dfeab8f214f92c3 ./docker-bake.hcl @@ -437,7 +443,7 @@ b74f901cca99eae4bed1b168f1f077a28bf1134a71c867d263c7834f6d896684 ./services/age cfc570de023b80f9a775d82a8ad5398521a4f377a85bb86642e60d0f22333d94 ./services/agent/docker-compose.yml a0105475dc054977223fac36618b8cd8137c55be1d11fddcd24e9a4d3074c170 ./services/agent/glpi-ai-agent-neural-brain.patch 8a7805be0da45875d45a5e5b26780fe659f671edd2d6b72c0d7da9606bf15755 ./services/agent/go.mod -e34ac0033c53effcf654689641a15a92deae1a987e3d92e9921b7b030efab3d0 ./services/agent/internal/agent/agent.go +beab3aa1f73f3b3f78f76ff43b60d2ea3d9c85353288b53e83b01bf3fd59db22 ./services/agent/internal/agent/agent.go 789e55accdbfbb99ef8179f5680aa340e67a055c71686533302558389dabfb8a ./services/agent/internal/agent/agent_test.go a8eedc80f57b174fbad3850d787346f7b370236b4c2eef67905ef6b60a1ced64 ./services/agent/internal/agent/analysis_runs.go 46a8dfe5e80569aac48f9a098acb2c1089ea4ab9521ed6c92f53b020e0c76ccd ./services/agent/internal/agent/analysis_runs_test.go @@ -456,8 +462,8 @@ c8da5784acc4f28d632f357dadbfc61209a37b698525dae40473d27625d9c8e2 ./services/age fa1717032ad536bff867a07d3fdf0ae2e0a9d5d7637a27b43274832d51322eb6 ./services/agent/internal/config/config_test.go 6fe7335b0e66b60d5f6f27583b1ac11a0948719cfa657d51e77de0b337aab5d5 ./services/agent/internal/contextdata/collector.go 12e9eb6fd567b5d4173a63fa2b89be913b6d1c97e17b69048f46d119e88dce26 ./services/agent/internal/contextdata/collector_test.go -5eee0e130a9cb8452a07e9f5d4c76048ad2eb62aa411ea61c9dc6066fadff934 ./services/agent/internal/glpi/client.go -6499b2e19547d7d874dbaa64f0c905643ffe4d822329c6cd710df0a30a3a9755 ./services/agent/internal/glpi/client_test.go +cffe6bc72ac01f0eb96599a40af43941c0719bd8cb698eddb4441fbf11703c7a ./services/agent/internal/glpi/client.go +6d4846d6f4e327c8c55ebf7a83c9206bcc60364e739976c282873e5508f1789a ./services/agent/internal/glpi/client_test.go 247324ba41f3cd991a3582ed270d0b0d6c4c92c36f11e7742c20d3e59b982928 ./services/agent/internal/glpikb/sync.go 9836e2bf9153df638ead57d92c152b87f831e5a3696bcda4b1f650ff38e9ac70 ./services/agent/internal/glpikb/sync_test.go 787ab6b53623aa36ef6628f4eada11dcb4ed3011b34effd691bf07ba5d8f214d ./services/agent/internal/knowledge/category_mapping.go @@ -467,8 +473,8 @@ df1ac214ebb88bf3ce7f68112129cf9ffc300dd725380c38661b85b04908ecdf ./services/age 63e47d86bcc89178fdc0f91e07ecb30bd01ee4b84bf29bfc660debbf26f8a40b ./services/agent/internal/knowledge/store_test.go 807996228b38e1cd97ec2516cb1ecfc4bd945b638fcde465a0991051df2f1a00 ./services/agent/internal/learning/outcomes.go 2917b3dab63783af4c92c48f269f097dc94b989008a5451f46d6c1b309ccdc9b ./services/agent/internal/learning/outcomes_test.go -cd8c7379d6ca50fed77a7eafb6ab34b12b9ce2fe56025da534f54e6db246430d ./services/agent/internal/learning/store.go -c540f2e554d66f36d79d2ce32c74a18b27b2940e96a40e89eae10abccd0bd2a5 ./services/agent/internal/learning/store_test.go +482ca79552b02a8a53bbd547318a04e01a0b41e64d4a347b00f850c0218febda ./services/agent/internal/learning/store.go +07505924d3a15046836e7e6d278d1583c6591268f2216c3be78c58e35eafc71a ./services/agent/internal/learning/store_test.go 5030ec3f51830f7decfb24c27f9b243cb64f97302a6a51df096bc957f24b3fae ./services/agent/internal/metrics/metrics.go 809c3f2833ed2093bf1f9e4466eb3d855f2a84d827cb21deb698dd0e7b63b403 ./services/agent/internal/model/model.go 975d5846fa38bf2478fa3ab9696660740c8d9a0549ce8d4f3443d4ca917c4aba ./services/agent/internal/model/reason_codes.go @@ -489,10 +495,10 @@ ab29c5808a06f6b21a0776355eeaa030f69be5eee251e7b1dbd7e00c71045c61 ./services/age c849df7e346e1bd1e122037a4eafc30fe461add9b1876d7b9d63a02e33d831ad ./services/agent/internal/uptimekuma/client_test.go 5f0375f33c4c3f0804e58bb7b3a8699a450a05100274fbb19762d59f4d5d0034 ./services/agent/internal/web/control_graph.go cb68a23bef549186d251766a3d3820b611defd55e8f7386b8b87490a78b9e11d ./services/agent/internal/web/control_graph_test.go -b4f7d72022d5009fce43b5879483de92dacfee2b3bf527306902187c3d26486b ./services/agent/internal/web/server.go -1f517bcb30f95831a06dc2fe0fd9cd633a0c42fc795f67e5ce53de7a2eebd2b1 ./services/agent/internal/web/server_test.go +6b457a720d8c8c6b07b590ce827d5a51de611e50b6905ff9668ec8f3f4046fdc ./services/agent/internal/web/server.go +1edf74892fa1da9f614fee8ec47e6b900132677167f2642a625effc028f2dfa0 ./services/agent/internal/web/server_test.go 21b32b0b863ed62ae352011d7dd85f0134bf9909c23ee80b1c7e3c3605ef8cc4 ./services/agent/internal/web/templates/category-mappings.html -d0561b87acd8fe8c0ab7f1f0bb82e1df840db8342f91798e8cb0a0408064abcc ./services/agent/internal/web/templates/dashboard.html +158f91963bbcd39e19fedd6d8a3ced2b26ffc92a51f00a84d883f24557e03cb0 ./services/agent/internal/web/templates/dashboard.html fb5a4e2e1846046032bb878f1e878eb792497e321df6a8ce16209c07a925fdcd ./services/agent/internal/web/templates/diagnostics.html 7418ad39aa0899a85e69a9910d99ff1cb01757d5ae2edaa885c5ff7a815cb580 ./services/agent/knowledge-category-map.example.json 5a81ef73b2de4ff60c440a7a5e42da68e8ca30fab7bc62de63757157d498d72a ./services/agent/knowledge/01_active-directory.json @@ -601,7 +607,7 @@ f1eab883370e0a40ef52a6b6d785a510d8ed48a19d25e0a2bc95f4f2cc8e329e ./services/age 895f8400aaad550ff2b96262eaa54e954d603129682e97aa4964539eb19bd7c2 ./services/agent/run.ps1 d1d4ab8145a7131c36df51d3a9fbdcdeb0cda3de134fe24c0e8d52370862f3b2 ./services/control/Dockerfile f30ddf9251860d92717276483f7a2c2d7405f0516940ca62abc21d97ff3f3ad2 ./services/control/cmd/engineering-graph/main.go -10a365d00d56b812d3300fe08f33d1176d6819909235bc8e275606005e1b371f ./services/control/engineering-graph.json +c61a014f75ceb8c2d9365d5c053d952f80cb60faa1fbc4f3049999450b49adae ./services/control/engineering-graph.json ba44c599b9faf861eca647614c4abd18548e2708233cf462b11eb277e96449b8 ./services/control/go.mod da37393e58ff53847f26b1051f6d3a5270571d290b33d79360bae0dcb3513829 ./services/control/graph.go e6a8021a44219219fe37ab5edc45d1908fc6579f653aa80c9966810d891ad27d ./services/control/graph_test.go diff --git a/services/agent/internal/agent/agent.go b/services/agent/internal/agent/agent.go index f3901d5..12a67de 100644 --- a/services/agent/internal/agent/agent.go +++ b/services/agent/internal/agent/agent.go @@ -1293,7 +1293,7 @@ func (s *Service) RecordCategoryFeedback(ctx context.Context, runID string, cate } func (s *Service) LearningExamples() []model.LearningExample { if s.learning == nil { - return nil + return []model.LearningExample{} } return s.learning.List() } diff --git a/services/agent/internal/glpi/client.go b/services/agent/internal/glpi/client.go index 909e536..7ff21c1 100644 --- a/services/agent/internal/glpi/client.go +++ b/services/agent/internal/glpi/client.go @@ -28,6 +28,8 @@ type Client struct { tokenExpiry time.Time } +const maxGLPIResponseBytes = 32 << 20 + func New(baseURL, version, clientID, clientSecret, username, password string, timeout time.Duration) *Client { return &Client{baseURL: strings.TrimRight(baseURL, "/"), version: version, clientID: clientID, clientSecret: clientSecret, username: username, password: password, http: &http.Client{Timeout: timeout}} } @@ -104,7 +106,7 @@ func (c *Client) do(ctx context.Context, method, path string, query url.Values, if err != nil { return nil, nil, err } - b, _ := io.ReadAll(io.LimitReader(resp.Body, 4<<20)) + b, _ := io.ReadAll(io.LimitReader(resp.Body, maxGLPIResponseBytes)) resp.Body.Close() if resp.StatusCode == http.StatusUnauthorized && attempt == 0 { continue @@ -426,70 +428,99 @@ func (c *Client) DiscoverKnowledgeBasePath(ctx context.Context, configured strin } return candidates[i].score > candidates[j].score }) - path := candidates[0].path - // The OpenAPI document may include /api.php/vX.Y in documented paths while - // c.do already prepends the configured API base. Keep only the route suffix. - if i := strings.Index(path, "/api.php/"); i >= 0 { - rest := path[i+len("/api.php/"):] - if slash := strings.Index(rest, "/"); slash >= 0 { - path = rest[slash:] - } - } - versionPrefix := "/" + strings.Trim(c.version, "/") - if strings.HasPrefix(path, versionPrefix+"/") { - path = strings.TrimPrefix(path, versionPrefix) - } - if !strings.HasPrefix(path, "/") { - path = "/" + path - } - return path, nil + return normalizeDocumentedAPIPath(candidates[0].path, c.version), nil } // ListKnowledgeBaseItems reads only items visible to the authenticated GLPI // service account. Visibility is therefore enforced by GLPI itself; an // optional filter can further restrict the collection on a per-instance basis. func (c *Client) ListKnowledgeBaseItems(ctx context.Context, path string, limit int, filter string) ([]model.GLPIKnowledgeItem, error) { - q := url.Values{"limit": {strconv.Itoa(limit)}, "sort": {"date_mod"}, "order": {"DESC"}} - if strings.TrimSpace(filter) != "" { - q.Set("filter", filter) + if limit <= 0 { + return []model.GLPIKnowledgeItem{}, nil } - b, _, err := c.do(ctx, http.MethodGet, path, q, nil) - if err != nil { - return nil, err - } - arr, err := extractArray(b) - if err != nil { - return nil, err - } - out := make([]model.GLPIKnowledgeItem, 0, len(arr)) - for _, r := range arr { - id := int64Val(r["id"]) - if id <= 0 { - continue + const pageSize = 100 + out := make([]model.GLPIKnowledgeItem, 0, limit) + seen := make(map[int64]struct{}, limit) + start := 0 + for len(out) < limit { + requestLimit := pageSize + if remaining := limit - len(out); remaining < requestLimit { + requestLimit = remaining } - if firstString(r, "answer", "content", "text", "description") == "" { - if detail, _, e := c.do(ctx, http.MethodGet, strings.TrimRight(path, "/")+"/"+strconv.FormatInt(id, 10), nil, nil); e == nil { + q := url.Values{ + "start": {strconv.Itoa(start)}, + "limit": {strconv.Itoa(requestLimit)}, + "sort": {"date_mod"}, + "order": {"DESC"}, + } + if strings.TrimSpace(filter) != "" { + q.Set("filter", filter) + } + b, headers, err := c.do(ctx, http.MethodGet, path, q, nil) + if err != nil { + return nil, err + } + arr, err := extractArray(b) + if err != nil { + return nil, err + } + if len(arr) == 0 { + break + } + + newIDs := 0 + for _, r := range arr { + id := int64Val(r["id"]) + if id <= 0 { + continue + } + if _, duplicate := seen[id]; duplicate { + continue + } + seen[id] = struct{}{} + newIDs++ + if firstString(r, "answer", "content", "text", "description") == "" { + detail, _, detailErr := c.do(ctx, http.MethodGet, strings.TrimRight(path, "/")+"/"+strconv.FormatInt(id, 10), nil, nil) + if detailErr != nil { + return nil, fmt.Errorf("fetch GLPI knowledge item %d detail: %w", id, detailErr) + } var full map[string]any - if json.Unmarshal(detail, &full) == nil { - for k, v := range full { - r[k] = v - } + if err := json.Unmarshal(detail, &full); err != nil { + return nil, fmt.Errorf("decode GLPI knowledge item %d detail: %w", id, err) + } + for k, v := range full { + r[k] = v } } + title := firstString(r, "name", "title", "subject") + content := firstString(r, "answer", "content", "text", "description") + if title == "" || content == "" { + continue + } + out = append(out, model.GLPIKnowledgeItem{ + ID: id, + Title: title, + Content: content, + CategoryIDs: knowledgeCategoryIDs(r), + Language: firstString(r, "language", "locale"), + ModifiedAt: firstString(r, "date_mod", "modified_at", "date_creation"), + }) + if len(out) >= limit { + break + } } - title := firstString(r, "name", "title", "subject") - content := firstString(r, "answer", "content", "text", "description") - if title == "" || content == "" { - continue + pageStart := start + start += len(arr) + moreKnown, hasMore := collectionHasMore(b, headers, pageStart, len(arr)) + if len(arr) < requestLimit && (!moreKnown || !hasMore) { + break } - out = append(out, model.GLPIKnowledgeItem{ - ID: id, - Title: title, - Content: content, - CategoryIDs: knowledgeCategoryIDs(r), - Language: firstString(r, "language", "locale"), - ModifiedAt: firstString(r, "date_mod", "modified_at", "date_creation"), - }) + if newIDs == 0 { + return nil, fmt.Errorf("GLPI knowledge pagination made no progress at start=%d; server may be ignoring the start parameter", pageStart) + } + } + if len(seen) > 0 && len(out) == 0 { + return nil, fmt.Errorf("GLPI returned %d knowledge records but none contained a usable title and article body", len(seen)) } return out, nil } @@ -543,23 +574,7 @@ func (c *Client) ListKnowledgeBaseLinkedItems(ctx context.Context, articleIDs [] return candidates[i].score > candidates[j].score }) chosen := candidates[0] - cleanPath := func(p string) string { - if i := strings.Index(p, "/api.php/"); i >= 0 { - rest := p[i+len("/api.php/"):] - if slash := strings.Index(rest, "/"); slash >= 0 { - p = rest[slash:] - } - } - versionPrefix := "/" + strings.Trim(c.version, "/") - if strings.HasPrefix(p, versionPrefix+"/") { - p = strings.TrimPrefix(p, versionPrefix) - } - if !strings.HasPrefix(p, "/") { - p = "/" + p - } - return p - } - chosen.path = cleanPath(chosen.path) + chosen.path = normalizeDocumentedAPIPath(chosen.path, c.version) if limit <= 0 { limit = 10000 } @@ -690,6 +705,50 @@ func extractArray(b []byte) ([]map[string]any, error) { } return nil, fmt.Errorf("unexpected GLPI collection response: %.200s", string(b)) } + +func collectionHasMore(body []byte, headers http.Header, start, count int) (known bool, more bool) { + if raw := strings.TrimSpace(headers.Get("Content-Range")); raw != "" { + if slash := strings.LastIndex(raw, "/"); slash >= 0 && slash+1 < len(raw) { + totalText := strings.TrimSpace(raw[slash+1:]) + if totalText != "*" { + if total, err := strconv.Atoi(totalText); err == nil && total >= 0 { + return true, start+count < total + } + } + } + } + var obj map[string]any + if json.Unmarshal(body, &obj) == nil { + for _, key := range []string{"total", "totalcount", "total_count"} { + if total := int(int64Val(obj[key])); total > 0 { + return true, start+count < total + } + } + } + return false, false +} + +var documentedVersionPrefix = regexp.MustCompile(`^/v[0-9]+(?:\.[0-9]+){0,2}/`) + +func normalizeDocumentedAPIPath(path, configuredVersion string) string { + path = strings.TrimSpace(path) + if i := strings.Index(path, "/api.php/"); i >= 0 { + rest := path[i+len("/api.php/"):] + if slash := strings.Index(rest, "/"); slash >= 0 { + path = rest[slash:] + } + } + versionPrefix := "/" + strings.Trim(configuredVersion, "/") + if strings.HasPrefix(path, versionPrefix+"/") { + path = strings.TrimPrefix(path, versionPrefix) + } else if documentedVersionPrefix.MatchString(path) { + path = documentedVersionPrefix.ReplaceAllString(path, "/") + } + if !strings.HasPrefix(path, "/") { + path = "/" + path + } + return path +} func decodeTicket(r map[string]any) model.Ticket { t := model.Ticket{ ID: int64Val(r["id"]), diff --git a/services/agent/internal/glpi/client_test.go b/services/agent/internal/glpi/client_test.go index b364ad9..ba9c872 100644 --- a/services/agent/internal/glpi/client_test.go +++ b/services/agent/internal/glpi/client_test.go @@ -3,8 +3,11 @@ package glpi import ( "context" "encoding/json" + "fmt" "net/http" "net/http/httptest" + "reflect" + "strconv" "strings" "testing" "time" @@ -91,6 +94,142 @@ func TestDiscoverAndListKnowledgeBase(t *testing.T) { } } +func TestDiscoverKnowledgeBaseStripsPatchVersionPrefix(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/api.php/token": + _ = json.NewEncoder(w).Encode(map[string]any{"access_token": "x", "expires_in": 3600}) + case "/api.php/doc.json": + _ = json.NewEncoder(w).Encode(map[string]any{"paths": map[string]any{ + "/v2.3.0/Knowledge/KnowbaseItem": map[string]any{"get": map[string]any{}}, + }}) + default: + http.NotFound(w, r) + } + })) + defer srv.Close() + c := New(srv.URL, "v2.3", "cid", "sec", "u", "p", time.Second) + path, err := c.DiscoverKnowledgeBasePath(context.Background(), "auto") + if err != nil { + t.Fatal(err) + } + if path != "/Knowledge/KnowbaseItem" { + t.Fatalf("path=%q, want /Knowledge/KnowbaseItem", path) + } +} + +func TestListKnowledgeBasePaginatesLargeCollections(t *testing.T) { + const total = 235 + var starts []int + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/api.php/token": + _ = json.NewEncoder(w).Encode(map[string]any{"access_token": "x", "expires_in": 3600}) + case "/api.php/v2.3/Knowledge/KnowbaseItem": + start, _ := strconv.Atoi(r.URL.Query().Get("start")) + limit, _ := strconv.Atoi(r.URL.Query().Get("limit")) + starts = append(starts, start) + if limit <= 0 || limit > 100 { + t.Fatalf("unexpected page limit %d", limit) + } + end := start + limit + if end > total { + end = total + } + rows := make([]map[string]any, 0, max(0, end-start)) + for i := start; i < end; i++ { + rows = append(rows, map[string]any{ + "id": i + 1, "name": fmt.Sprintf("Artikel %d", i+1), "answer": "Inhalt", "date_mod": "2026-09-09 10:00:00", + }) + } + _ = json.NewEncoder(w).Encode(rows) + default: + http.NotFound(w, r) + } + })) + defer srv.Close() + c := New(srv.URL, "v2.3", "cid", "sec", "u", "p", time.Second) + items, err := c.ListKnowledgeBaseItems(context.Background(), "/Knowledge/KnowbaseItem", total, "") + if err != nil { + t.Fatal(err) + } + if len(items) != total { + t.Fatalf("items=%d, want %d", len(items), total) + } + wantStarts := []int{0, 100, 200} + if !reflect.DeepEqual(starts, wantStarts) { + t.Fatalf("starts=%v, want %v", starts, wantStarts) + } +} + +func TestListKnowledgeBaseContinuesWhenServerCapsPageSize(t *testing.T) { + const total = 121 + const serverCap = 37 + var starts []int + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/api.php/token": + _ = json.NewEncoder(w).Encode(map[string]any{"access_token": "x", "expires_in": 3600}) + case "/api.php/v2.3/Knowledge/KnowbaseItem": + start, _ := strconv.Atoi(r.URL.Query().Get("start")) + requested, _ := strconv.Atoi(r.URL.Query().Get("limit")) + starts = append(starts, start) + pageLimit := requested + if pageLimit > serverCap { + pageLimit = serverCap + } + end := start + pageLimit + if end > total { + end = total + } + rows := make([]map[string]any, 0, max(0, end-start)) + for i := start; i < end; i++ { + rows = append(rows, map[string]any{"id": i + 1, "name": fmt.Sprintf("Artikel %d", i+1), "answer": "Inhalt"}) + } + if len(rows) > 0 { + w.Header().Set("Content-Range", fmt.Sprintf("items %d-%d/%d", start, end-1, total)) + } + _ = json.NewEncoder(w).Encode(rows) + default: + http.NotFound(w, r) + } + })) + defer srv.Close() + c := New(srv.URL, "v2.3", "cid", "sec", "u", "p", time.Second) + items, err := c.ListKnowledgeBaseItems(context.Background(), "/Knowledge/KnowbaseItem", total, "") + if err != nil { + t.Fatal(err) + } + if len(items) != total { + t.Fatalf("items=%d, want %d", len(items), total) + } + wantStarts := []int{0, 37, 74, 111} + if !reflect.DeepEqual(starts, wantStarts) { + t.Fatalf("starts=%v, want %v", starts, wantStarts) + } +} + +func TestListKnowledgeBaseFailsWhenRequiredDetailCannotBeRead(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/api.php/token": + _ = json.NewEncoder(w).Encode(map[string]any{"access_token": "x", "expires_in": 3600}) + case "/api.php/v2.3/Knowledge/KnowbaseItem": + _ = json.NewEncoder(w).Encode([]map[string]any{{"id": 7, "name": "Konto gesperrt"}}) + case "/api.php/v2.3/Knowledge/KnowbaseItem/7": + http.Error(w, "missing right", http.StatusForbidden) + default: + http.NotFound(w, r) + } + })) + defer srv.Close() + c := New(srv.URL, "v2.3", "cid", "sec", "u", "p", time.Second) + _, err := c.ListKnowledgeBaseItems(context.Background(), "/Knowledge/KnowbaseItem", 50, "") + if err == nil || !strings.Contains(err.Error(), "knowledge item 7 detail") { + t.Fatalf("expected explicit detail-read error, got %v", err) + } +} + func TestAddFollowupPreservesRichHTML(t *testing.T) { var gotContent string srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { diff --git a/services/agent/internal/learning/store.go b/services/agent/internal/learning/store.go index 89dd762..08bed81 100644 --- a/services/agent/internal/learning/store.go +++ b/services/agent/internal/learning/store.go @@ -98,11 +98,11 @@ func (s *Store) Delete(id string) error { func (s *Store) List() []model.LearningExample { if s == nil { - return nil + return []model.LearningExample{} } s.mu.RLock() defer s.mu.RUnlock() - out := append([]model.LearningExample(nil), s.examples...) + out := append([]model.LearningExample{}, s.examples...) sort.SliceStable(out, func(i, j int) bool { return out[i].CreatedAt.After(out[j].CreatedAt) }) return out } diff --git a/services/agent/internal/learning/store_test.go b/services/agent/internal/learning/store_test.go index 2ceca03..d456bbb 100644 --- a/services/agent/internal/learning/store_test.go +++ b/services/agent/internal/learning/store_test.go @@ -28,3 +28,17 @@ func TestAddReplaceAndDelete(t *testing.T) { t.Fatal("old replaced id should not exist") } } + +func TestEmptyListIsNonNil(t *testing.T) { + s, err := Open(t.TempDir(), 10) + if err != nil { + t.Fatal(err) + } + rows := s.List() + if rows == nil { + t.Fatal("empty learning list must serialize as [] instead of null") + } + if len(rows) != 0 { + t.Fatalf("rows=%d, want 0", len(rows)) + } +} diff --git a/services/agent/internal/web/server.go b/services/agent/internal/web/server.go index 9730c3c..1dd2215 100644 --- a/services/agent/internal/web/server.go +++ b/services/agent/internal/web/server.go @@ -640,7 +640,11 @@ func (s *Server) knowledgeDelete(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNoContent) } func (s *Server) learningList(w http.ResponseWriter, r *http.Request) { - respondJSON(w, s.feedback.LearningExamples()) + rows := s.feedback.LearningExamples() + if rows == nil { + rows = []model.LearningExample{} + } + respondJSON(w, rows) } func (s *Server) learningAdd(w http.ResponseWriter, r *http.Request) { if !s.cfg.LearningEnabled { diff --git a/services/agent/internal/web/server_test.go b/services/agent/internal/web/server_test.go index 7929297..4a7d81b 100644 --- a/services/agent/internal/web/server_test.go +++ b/services/agent/internal/web/server_test.go @@ -51,6 +51,18 @@ func TestDashboardTemplateParses(t *testing.T) { } } +func TestLearningListSerializesEmptyArray(t *testing.T) { + h, _ := newKnowledgeTestServer(t) + rr := httptest.NewRecorder() + h.ServeHTTP(rr, httptest.NewRequest(http.MethodGet, "/api/learning", nil)) + if rr.Code != http.StatusOK { + t.Fatalf("learning list = %d: %s", rr.Code, rr.Body.String()) + } + if got := strings.TrimSpace(rr.Body.String()); got != "[]" { + t.Fatalf("learning list body=%q, want []", got) + } +} + type fakeFeedback struct { cats []model.Category outcomeRows []model.ValidatedOutcomeEvidence diff --git a/services/agent/internal/web/templates/dashboard.html b/services/agent/internal/web/templates/dashboard.html index b749820..af05d12 100644 --- a/services/agent/internal/web/templates/dashboard.html +++ b/services/agent/internal/web/templates/dashboard.html @@ -107,6 +107,7 @@ const esc=s=>String(s??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'& const pct=v=>`${Math.round(Number(v||0)*100)} %`; const fmtNum=v=>new Intl.NumberFormat('de-DE').format(Number(v||0)); const fmtDate=v=>{if(!v)return '–';const d=new Date(v);return Number.isNaN(d.getTime())?'–':d.toLocaleString('de-DE')}; const clamp=v=>Math.max(0,Math.min(1,Number(v||0))); const scoreClass=v=>Number(v)>=.8?'good':Number(v)>=.6?'warn':'bad'; +const asArray=v=>Array.isArray(v)?v:[]; let statusData={},runsData=[],categories=[],kbDocs=[],learningRows=[],outcomeRows=[],currentRun=null,currentKbId='',currentLearnRun='',kbCategorySelection=new Set(); const viewMeta={overview:['Übersicht','Gesundheit, Verarbeitung und die wichtigsten Stellschrauben auf einen Blick.'],runs:['Verarbeitungen','Audit-Trail mit KI-, Policy-, RAG- und Kontextdetails.'],knowledge:['Knowledge Base','Interne und synchronisierte Wissensquellen verwalten und kontrollieren.'],learning:['Bestätigtes Lernen','Menschlich bestätigte Beispiele, die die Kategorisierung schrittweise verbessern.'],settings:['Effektive Konfiguration','Alle nicht-geheimen Laufzeitwerte zum Tuning und Troubleshooting.']}; function toast(text,kind='good'){const n=document.createElement('div');n.className=`toast ${kind}`;n.textContent=text;$('#toasts').appendChild(n);setTimeout(()=>n.remove(),5000)} @@ -164,11 +165,11 @@ async function deleteLearning(id){if(!confirm('Lernbeispiel wirklich löschen?') async function recordOutcome(runID,decision){const r=runsData.find(x=>x.run_id===runID);if(!r)return;let corrected='';if(decision==='corrected'){corrected=prompt('Korrigierte, fachlich bestätigte Antwort:',r.reply_proposed_text||'')||'';if(!corrected.trim())return}const note=prompt('Optionaler Prüfvermerk / Grund (kann leer bleiben):','')||'';if(!confirm(decision==='accepted'?'Diese KI-Antwort als technisch bestätigt lernen?':'Diese korrigierte Antwort als technisch bestätigt lernen?'))return;try{await api('/api/outcomes',{method:'POST',body:JSON.stringify({run_id:runID,decision,corrected_reply:corrected,note})});toast('Bestätigtes Ticket-Outcome wurde an NeuroForge übergeben.');await loadOutcomes();await loadRuns();const fresh=runsData.find(x=>x.run_id===runID);if(fresh)renderRunDrawer(fresh)}catch(e){toast(e.message,'error')}} async function reprocessTicket(){const id=Number($('#reprocessTicketID').value||0);if(!Number.isInteger(id)||id<=0){toast('Bitte eine gültige Ticket-ID eingeben.','error');return}const live=!statusData.dry_run;if(live&&!confirm(`Ticket #${id} im LIVE-Modus neu analysieren? Konfigurierte Auto-Aktionen können ausgeführt werden.`))return;const b=$('#reprocessTicketBtn');b.disabled=true;try{await api(`/api/tickets/${id}/reprocess`,{method:'POST',body:'{}'});toast(`Ticket #${id} wurde zur Neuanalyse eingereiht.`);setTimeout(()=>refreshAll(),800)}catch(e){toast(e.message,'error')}finally{b.disabled=false}} async function loadStatus(){statusData=await api('/api/status');renderStatusChrome();renderOverview();renderConfig();renderSourceOptions()} -async function loadRuns(){const limit=Number($('#runLimit').value||50);runsData=await api(`/api/runs?limit=${limit}`);renderRuns();renderOverview()} -async function loadKnowledge(){kbDocs=await api('/api/knowledge');renderSourceOptions();renderKB();renderOverview()} -async function loadLearning(){learningRows=await api('/api/learning');renderLearning();renderOverview()} -async function loadOutcomes(){outcomeRows=await api('/api/outcomes')} -async function loadCategories(){categories=await api('/api/categories');renderCategoryPicker($('#kbCategorySearch').value||'')} +async function loadRuns(){const limit=Number($('#runLimit').value||50);runsData=asArray(await api(`/api/runs?limit=${limit}`));renderRuns();renderOverview()} +async function loadKnowledge(){kbDocs=asArray(await api('/api/knowledge'));renderSourceOptions();renderKB();renderOverview()} +async function loadLearning(){learningRows=asArray(await api('/api/learning'));renderLearning();renderOverview()} +async function loadOutcomes(){outcomeRows=asArray(await api('/api/outcomes'))} +async function loadCategories(){categories=asArray(await api('/api/categories'));renderCategoryPicker($('#kbCategorySearch').value||'')} async function refreshAll(showToast=false){$('#refreshBtn').disabled=true;try{await Promise.all([loadStatus(),loadRuns(),loadKnowledge(),loadLearning(),loadOutcomes(),loadCategories()]);renderSourceOptions();renderOverview();renderRuns();renderKB();renderLearning();renderConfig();if(showToast)toast('Dashboard aktualisiert.')}catch(e){toast(e.message,'error')}finally{$('#refreshBtn').disabled=false;$('#lastRefresh').textContent=new Date().toLocaleTimeString('de-DE')}} $('#nav').addEventListener('click',e=>{const b=e.target.closest('[data-view]');if(b)setView(b.dataset.view)});document.addEventListener('click',e=>{const g=e.target.closest('[data-goto]');if(g)setView(g.dataset.goto);const row=e.target.closest('[data-run-id]');if(row){const x=runsData.find(r=>r.run_id===row.dataset.runId);if(x)renderRunDrawer(x)}const ed=e.target.closest('[data-kb-edit]');if(ed)editKB(ed.dataset.kbEdit);const del=e.target.closest('[data-kb-delete]');if(del)deleteKB(del.dataset.kbDelete);const ld=e.target.closest('[data-learning-delete]');if(ld)deleteLearning(ld.dataset.learningDelete);const lr=e.target.closest('[data-learn-run]');if(lr)openLearn(lr.dataset.learnRun,Number(lr.dataset.learnCat||0));const oa=e.target.closest('[data-outcome-accept]');if(oa)recordOutcome(oa.dataset.outcomeAccept,'accepted');const oc=e.target.closest('[data-outcome-correct]');if(oc)recordOutcome(oc.dataset.outcomeCorrect,'corrected')}); diff --git a/services/control/engineering-graph.json b/services/control/engineering-graph.json index 77c70d6..1646b8d 100644 --- a/services/control/engineering-graph.json +++ b/services/control/engineering-graph.json @@ -2836,7 +2836,7 @@ "community": "github.com/example/glpi-ai-agent/internal/glpi", "meta": { "exported": true, - "line": 35, + "line": 37, "path": "services/agent/internal/glpi/client.go" } }, @@ -2848,7 +2848,7 @@ "community": "github.com/example/glpi-ai-agent/internal/glpi", "meta": { "exported": true, - "line": 314, + "line": 316, "path": "services/agent/internal/glpi/client.go" } }, @@ -2860,7 +2860,7 @@ "community": "github.com/example/glpi-ai-agent/internal/glpi", "meta": { "exported": true, - "line": 310, + "line": 312, "path": "services/agent/internal/glpi/client.go" } }, @@ -2872,7 +2872,7 @@ "community": "github.com/example/glpi-ai-agent/internal/glpi", "meta": { "exported": true, - "line": 373, + "line": 375, "path": "services/agent/internal/glpi/client.go" } }, @@ -2884,7 +2884,7 @@ "community": "github.com/example/glpi-ai-agent/internal/glpi", "meta": { "exported": true, - "line": 124, + "line": 126, "path": "services/agent/internal/glpi/client.go" } }, @@ -2896,7 +2896,7 @@ "community": "github.com/example/glpi-ai-agent/internal/glpi", "meta": { "exported": true, - "line": 354, + "line": 356, "path": "services/agent/internal/glpi/client.go" } }, @@ -2908,7 +2908,7 @@ "community": "github.com/example/glpi-ai-agent/internal/glpi", "meta": { "exported": true, - "line": 238, + "line": 240, "path": "services/agent/internal/glpi/client.go" } }, @@ -2920,7 +2920,7 @@ "community": "github.com/example/glpi-ai-agent/internal/glpi", "meta": { "exported": true, - "line": 227, + "line": 229, "path": "services/agent/internal/glpi/client.go" } }, @@ -2932,7 +2932,7 @@ "community": "github.com/example/glpi-ai-agent/internal/glpi", "meta": { "exported": true, - "line": 327, + "line": 329, "path": "services/agent/internal/glpi/client.go" } }, @@ -2944,7 +2944,7 @@ "community": "github.com/example/glpi-ai-agent/internal/glpi", "meta": { "exported": true, - "line": 949, + "line": 1008, "path": "services/agent/internal/glpi/client.go" } }, @@ -2956,7 +2956,7 @@ "community": "github.com/example/glpi-ai-agent/internal/glpi", "meta": { "exported": true, - "line": 207, + "line": 209, "path": "services/agent/internal/glpi/client.go" } }, @@ -2968,7 +2968,7 @@ "community": "github.com/example/glpi-ai-agent/internal/glpi", "meta": { "exported": true, - "line": 451, + "line": 437, "path": "services/agent/internal/glpi/client.go" } }, @@ -2980,7 +2980,7 @@ "community": "github.com/example/glpi-ai-agent/internal/glpi", "meta": { "exported": true, - "line": 501, + "line": 532, "path": "services/agent/internal/glpi/client.go" } }, @@ -2992,7 +2992,7 @@ "community": "github.com/example/glpi-ai-agent/internal/glpi", "meta": { "exported": true, - "line": 979, + "line": 1038, "path": "services/agent/internal/glpi/client.go" } }, @@ -3004,7 +3004,7 @@ "community": "github.com/example/glpi-ai-agent/internal/glpi", "meta": { "exported": true, - "line": 187, + "line": 189, "path": "services/agent/internal/glpi/client.go" } }, @@ -3016,7 +3016,7 @@ "community": "github.com/example/glpi-ai-agent/internal/glpi", "meta": { "exported": true, - "line": 1013, + "line": 1072, "path": "services/agent/internal/glpi/client.go" } }, @@ -3028,7 +3028,7 @@ "community": "github.com/example/glpi-ai-agent/internal/glpi", "meta": { "exported": true, - "line": 120, + "line": 122, "path": "services/agent/internal/glpi/client.go" } }, @@ -3040,7 +3040,7 @@ "community": "github.com/example/glpi-ai-agent/internal/glpi", "meta": { "exported": true, - "line": 262, + "line": 264, "path": "services/agent/internal/glpi/client.go" } }, @@ -3052,7 +3052,7 @@ "community": "github.com/example/glpi-ai-agent/internal/glpi", "meta": { "exported": true, - "line": 266, + "line": 268, "path": "services/agent/internal/glpi/client.go" } }, @@ -3064,7 +3064,7 @@ "community": "github.com/example/glpi-ai-agent/internal/glpi", "meta": { "exported": true, - "line": 253, + "line": 255, "path": "services/agent/internal/glpi/client.go" } }, @@ -3076,7 +3076,7 @@ "community": "github.com/example/glpi-ai-agent/internal/glpi", "meta": { "exported": true, - "line": 257, + "line": 259, "path": "services/agent/internal/glpi/client.go" } }, @@ -3088,7 +3088,7 @@ "community": "github.com/example/glpi-ai-agent/internal/glpi", "meta": { "exported": true, - "line": 146, + "line": 148, "path": "services/agent/internal/glpi/client.go" } }, @@ -3100,7 +3100,7 @@ "community": "github.com/example/glpi-ai-agent/internal/glpi", "meta": { "exported": true, - "line": 925, + "line": 984, "path": "services/agent/internal/glpi/client.go" } }, @@ -3112,7 +3112,7 @@ "community": "github.com/example/glpi-ai-agent/internal/glpi", "meta": { "exported": false, - "line": 318, + "line": 320, "path": "services/agent/internal/glpi/client.go" } }, @@ -3124,7 +3124,7 @@ "community": "github.com/example/glpi-ai-agent/internal/glpi", "meta": { "exported": false, - "line": 36, + "line": 38, "path": "services/agent/internal/glpi/client.go" } }, @@ -3136,7 +3136,7 @@ "community": "github.com/example/glpi-ai-agent/internal/glpi", "meta": { "exported": false, - "line": 76, + "line": 78, "path": "services/agent/internal/glpi/client.go" } }, @@ -3148,7 +3148,7 @@ "community": "github.com/example/glpi-ai-agent/internal/glpi", "meta": { "exported": false, - "line": 270, + "line": 272, "path": "services/agent/internal/glpi/client.go" } }, @@ -3160,7 +3160,7 @@ "community": "github.com/example/glpi-ai-agent/internal/glpi", "meta": { "exported": true, - "line": 31, + "line": 33, "path": "services/agent/internal/glpi/client.go" } }, @@ -3172,7 +3172,7 @@ "community": "github.com/example/glpi-ai-agent/internal/glpi", "meta": { "exported": false, - "line": 792, + "line": 851, "path": "services/agent/internal/glpi/client.go" } }, @@ -3184,7 +3184,19 @@ "community": "github.com/example/glpi-ai-agent/internal/glpi", "meta": { "exported": false, - "line": 910, + "line": 969, + "path": "services/agent/internal/glpi/client.go" + } + }, + { + "id": "function:github.com/example/glpi-ai-agent/internal/glpi:collectionHasMore", + "kind": "function", + "label": "collectionHasMore", + "group": "engineering", + "community": "github.com/example/glpi-ai-agent/internal/glpi", + "meta": { + "exported": false, + "line": 709, "path": "services/agent/internal/glpi/client.go" } }, @@ -3196,7 +3208,7 @@ "community": "github.com/example/glpi-ai-agent/internal/glpi", "meta": { "exported": false, - "line": 865, + "line": 924, "path": "services/agent/internal/glpi/client.go" } }, @@ -3208,7 +3220,7 @@ "community": "github.com/example/glpi-ai-agent/internal/glpi", "meta": { "exported": false, - "line": 693, + "line": 752, "path": "services/agent/internal/glpi/client.go" } }, @@ -3220,7 +3232,7 @@ "community": "github.com/example/glpi-ai-agent/internal/glpi", "meta": { "exported": false, - "line": 716, + "line": 775, "path": "services/agent/internal/glpi/client.go" } }, @@ -3232,7 +3244,7 @@ "community": "github.com/example/glpi-ai-agent/internal/glpi", "meta": { "exported": false, - "line": 677, + "line": 692, "path": "services/agent/internal/glpi/client.go" } }, @@ -3244,7 +3256,7 @@ "community": "github.com/example/glpi-ai-agent/internal/glpi", "meta": { "exported": false, - "line": 812, + "line": 871, "path": "services/agent/internal/glpi/client.go" } }, @@ -3256,7 +3268,7 @@ "community": "github.com/example/glpi-ai-agent/internal/glpi", "meta": { "exported": false, - "line": 762, + "line": 821, "path": "services/agent/internal/glpi/client.go" } }, @@ -3268,7 +3280,7 @@ "community": "github.com/example/glpi-ai-agent/internal/glpi", "meta": { "exported": false, - "line": 629, + "line": 644, "path": "services/agent/internal/glpi/client.go" } }, @@ -3280,7 +3292,7 @@ "community": "github.com/example/glpi-ai-agent/internal/glpi", "meta": { "exported": false, - "line": 868, + "line": 927, "path": "services/agent/internal/glpi/client.go" } }, @@ -3292,7 +3304,7 @@ "community": "github.com/example/glpi-ai-agent/internal/glpi", "meta": { "exported": false, - "line": 1054, + "line": 1113, "path": "services/agent/internal/glpi/client.go" } }, @@ -3304,7 +3316,7 @@ "community": "github.com/example/glpi-ai-agent/internal/glpi", "meta": { "exported": false, - "line": 884, + "line": 943, "path": "services/agent/internal/glpi/client.go" } }, @@ -3316,7 +3328,19 @@ "community": "github.com/example/glpi-ai-agent/internal/glpi", "meta": { "exported": false, - "line": 638, + "line": 653, + "path": "services/agent/internal/glpi/client.go" + } + }, + { + "id": "function:github.com/example/glpi-ai-agent/internal/glpi:normalizeDocumentedAPIPath", + "kind": "function", + "label": "normalizeDocumentedAPIPath", + "group": "engineering", + "community": "github.com/example/glpi-ai-agent/internal/glpi", + "meta": { + "exported": false, + "line": 733, "path": "services/agent/internal/glpi/client.go" } }, @@ -3328,7 +3352,7 @@ "community": "github.com/example/glpi-ai-agent/internal/glpi", "meta": { "exported": false, - "line": 175, + "line": 177, "path": "services/agent/internal/glpi/client.go" } }, @@ -3340,7 +3364,7 @@ "community": "github.com/example/glpi-ai-agent/internal/glpi", "meta": { "exported": false, - "line": 878, + "line": 937, "path": "services/agent/internal/glpi/client.go" } }, @@ -3352,7 +3376,7 @@ "community": "github.com/example/glpi-ai-agent/internal/glpi", "meta": { "exported": false, - "line": 1066, + "line": 1125, "path": "services/agent/internal/glpi/client.go" } }, @@ -3364,7 +3388,7 @@ "community": "github.com/example/glpi-ai-agent/internal/glpi", "meta": { "exported": false, - "line": 901, + "line": 960, "path": "services/agent/internal/glpi/client.go" } }, @@ -3376,7 +3400,7 @@ "community": "github.com/example/glpi-ai-agent/internal/glpi", "meta": { "exported": false, - "line": 294, + "line": 296, "path": "services/agent/internal/glpi/client.go" } }, @@ -6748,7 +6772,7 @@ "community": "github.com/example/glpi-ai-agent/internal/web", "meta": { "exported": true, - "line": 867, + "line": 871, "path": "services/agent/internal/web/server.go" } }, @@ -6784,7 +6808,7 @@ "community": "github.com/example/glpi-ai-agent/internal/web", "meta": { "exported": true, - "line": 870, + "line": 874, "path": "services/agent/internal/web/server.go" } }, @@ -6796,7 +6820,7 @@ "community": "github.com/example/glpi-ai-agent/internal/web", "meta": { "exported": false, - "line": 835, + "line": 839, "path": "services/agent/internal/web/server.go" } }, @@ -7072,7 +7096,7 @@ "community": "github.com/example/glpi-ai-agent/internal/web", "meta": { "exported": false, - "line": 645, + "line": 649, "path": "services/agent/internal/web/server.go" } }, @@ -7084,7 +7108,7 @@ "community": "github.com/example/glpi-ai-agent/internal/web", "meta": { "exported": false, - "line": 665, + "line": 669, "path": "services/agent/internal/web/server.go" } }, @@ -7108,7 +7132,7 @@ "community": "github.com/example/glpi-ai-agent/internal/web", "meta": { "exported": false, - "line": 706, + "line": 710, "path": "services/agent/internal/web/server.go" } }, @@ -7120,7 +7144,7 @@ "community": "github.com/example/glpi-ai-agent/internal/web", "meta": { "exported": false, - "line": 679, + "line": 683, "path": "services/agent/internal/web/server.go" } }, @@ -7132,7 +7156,7 @@ "community": "github.com/example/glpi-ai-agent/internal/web", "meta": { "exported": false, - "line": 676, + "line": 680, "path": "services/agent/internal/web/server.go" } }, @@ -7156,7 +7180,7 @@ "community": "github.com/example/glpi-ai-agent/internal/web", "meta": { "exported": false, - "line": 884, + "line": 888, "path": "services/agent/internal/web/server.go" } }, @@ -7180,7 +7204,7 @@ "community": "github.com/example/glpi-ai-agent/internal/web", "meta": { "exported": false, - "line": 720, + "line": 724, "path": "services/agent/internal/web/server.go" } }, @@ -7228,7 +7252,7 @@ "community": "github.com/example/glpi-ai-agent/internal/web", "meta": { "exported": false, - "line": 740, + "line": 744, "path": "services/agent/internal/web/server.go" } }, @@ -7324,7 +7348,7 @@ "community": "github.com/example/glpi-ai-agent/internal/web", "meta": { "exported": false, - "line": 768, + "line": 772, "path": "services/agent/internal/web/server.go" } }, @@ -7336,7 +7360,7 @@ "community": "github.com/example/glpi-ai-agent/internal/web", "meta": { "exported": false, - "line": 819, + "line": 823, "path": "services/agent/internal/web/server.go" } }, @@ -7360,7 +7384,7 @@ "community": "github.com/example/glpi-ai-agent/internal/web", "meta": { "exported": false, - "line": 858, + "line": 862, "path": "services/agent/internal/web/server.go" } }, @@ -7372,7 +7396,7 @@ "community": "github.com/example/glpi-ai-agent/internal/web", "meta": { "exported": false, - "line": 829, + "line": 833, "path": "services/agent/internal/web/server.go" } }, @@ -7384,7 +7408,7 @@ "community": "github.com/example/glpi-ai-agent/internal/web", "meta": { "exported": false, - "line": 830, + "line": 834, "path": "services/agent/internal/web/server.go" } }, @@ -7396,7 +7420,7 @@ "community": "github.com/example/glpi-ai-agent/internal/web", "meta": { "exported": false, - "line": 849, + "line": 853, "path": "services/agent/internal/web/server.go" } }, @@ -7408,7 +7432,7 @@ "community": "github.com/example/glpi-ai-agent/internal/web", "meta": { "exported": false, - "line": 781, + "line": 785, "path": "services/agent/internal/web/server.go" } }, @@ -30669,6 +30693,12 @@ "to": "function:github.com/example/glpi-ai-agent/internal/glpi:boolVal", "kind": "defines" }, + { + "id": "file:services/agent/internal/glpi/client.go-\u003efunction:github.com/example/glpi-ai-agent/internal/glpi:collectionHasMore:defines", + "from": "file:services/agent/internal/glpi/client.go", + "to": "function:github.com/example/glpi-ai-agent/internal/glpi:collectionHasMore", + "kind": "defines" + }, { "id": "file:services/agent/internal/glpi/client.go-\u003efunction:github.com/example/glpi-ai-agent/internal/glpi:decodeFollowup:defines", "from": "file:services/agent/internal/glpi/client.go", @@ -30735,6 +30765,12 @@ "to": "function:github.com/example/glpi-ai-agent/internal/glpi:knowledgeCategoryIDs", "kind": "defines" }, + { + "id": "file:services/agent/internal/glpi/client.go-\u003efunction:github.com/example/glpi-ai-agent/internal/glpi:normalizeDocumentedAPIPath:defines", + "from": "file:services/agent/internal/glpi/client.go", + "to": "function:github.com/example/glpi-ai-agent/internal/glpi:normalizeDocumentedAPIPath", + "kind": "defines" + }, { "id": "file:services/agent/internal/glpi/client.go-\u003efunction:github.com/example/glpi-ai-agent/internal/glpi:openAPIOperations:defines", "from": "file:services/agent/internal/glpi/client.go", @@ -38779,6 +38815,12 @@ "to": "function:github.com/example/glpi-ai-agent/internal/glpi:New", "kind": "calls" }, + { + "id": "function:github.com/example/glpi-ai-agent/internal/glpi:Client.DiscoverKnowledgeBasePath-\u003efunction:github.com/example/glpi-ai-agent/internal/glpi:normalizeDocumentedAPIPath:calls", + "from": "function:github.com/example/glpi-ai-agent/internal/glpi:Client.DiscoverKnowledgeBasePath", + "to": "function:github.com/example/glpi-ai-agent/internal/glpi:normalizeDocumentedAPIPath", + "kind": "calls" + }, { "id": "function:github.com/example/glpi-ai-agent/internal/glpi:Client.DiscoverKnowledgeBasePath-\u003epackage:fmt:calls_package", "from": "function:github.com/example/glpi-ai-agent/internal/glpi:Client.DiscoverKnowledgeBasePath", @@ -39043,6 +39085,12 @@ "to": "function:github.com/example/glpi-ai-agent/internal/glpi:Client.do", "kind": "calls" }, + { + "id": "function:github.com/example/glpi-ai-agent/internal/glpi:Client.ListKnowledgeBaseItems-\u003efunction:github.com/example/glpi-ai-agent/internal/glpi:collectionHasMore:calls", + "from": "function:github.com/example/glpi-ai-agent/internal/glpi:Client.ListKnowledgeBaseItems", + "to": "function:github.com/example/glpi-ai-agent/internal/glpi:collectionHasMore", + "kind": "calls" + }, { "id": "function:github.com/example/glpi-ai-agent/internal/glpi:Client.ListKnowledgeBaseItems-\u003efunction:github.com/example/glpi-ai-agent/internal/glpi:extractArray:calls", "from": "function:github.com/example/glpi-ai-agent/internal/glpi:Client.ListKnowledgeBaseItems", @@ -39074,6 +39122,13 @@ "kind": "calls_package", "label": "Unmarshal" }, + { + "id": "function:github.com/example/glpi-ai-agent/internal/glpi:Client.ListKnowledgeBaseItems-\u003epackage:fmt:calls_package", + "from": "function:github.com/example/glpi-ai-agent/internal/glpi:Client.ListKnowledgeBaseItems", + "to": "package:fmt", + "kind": "calls_package", + "label": "Errorf" + }, { "id": "function:github.com/example/glpi-ai-agent/internal/glpi:Client.ListKnowledgeBaseItems-\u003epackage:strconv:calls_package", "from": "function:github.com/example/glpi-ai-agent/internal/glpi:Client.ListKnowledgeBaseItems", @@ -39124,6 +39179,12 @@ "to": "function:github.com/example/glpi-ai-agent/internal/glpi:firstString", "kind": "calls" }, + { + "id": "function:github.com/example/glpi-ai-agent/internal/glpi:Client.ListKnowledgeBaseLinkedItems-\u003efunction:github.com/example/glpi-ai-agent/internal/glpi:normalizeDocumentedAPIPath:calls", + "from": "function:github.com/example/glpi-ai-agent/internal/glpi:Client.ListKnowledgeBaseLinkedItems", + "to": "function:github.com/example/glpi-ai-agent/internal/glpi:normalizeDocumentedAPIPath", + "kind": "calls" + }, { "id": "function:github.com/example/glpi-ai-agent/internal/glpi:Client.ListKnowledgeBaseLinkedItems-\u003epackage:fmt:calls_package", "from": "function:github.com/example/glpi-ai-agent/internal/glpi:Client.ListKnowledgeBaseLinkedItems", @@ -39605,6 +39666,33 @@ "kind": "calls_package", "label": "EqualFold" }, + { + "id": "function:github.com/example/glpi-ai-agent/internal/glpi:collectionHasMore-\u003efunction:github.com/example/glpi-ai-agent/internal/glpi:int64Val:calls", + "from": "function:github.com/example/glpi-ai-agent/internal/glpi:collectionHasMore", + "to": "function:github.com/example/glpi-ai-agent/internal/glpi:int64Val", + "kind": "calls" + }, + { + "id": "function:github.com/example/glpi-ai-agent/internal/glpi:collectionHasMore-\u003epackage:encoding/json:calls_package", + "from": "function:github.com/example/glpi-ai-agent/internal/glpi:collectionHasMore", + "to": "package:encoding/json", + "kind": "calls_package", + "label": "Unmarshal" + }, + { + "id": "function:github.com/example/glpi-ai-agent/internal/glpi:collectionHasMore-\u003epackage:strconv:calls_package", + "from": "function:github.com/example/glpi-ai-agent/internal/glpi:collectionHasMore", + "to": "package:strconv", + "kind": "calls_package", + "label": "Atoi" + }, + { + "id": "function:github.com/example/glpi-ai-agent/internal/glpi:collectionHasMore-\u003epackage:strings:calls_package", + "from": "function:github.com/example/glpi-ai-agent/internal/glpi:collectionHasMore", + "to": "package:strings", + "kind": "calls_package", + "label": "TrimSpace" + }, { "id": "function:github.com/example/glpi-ai-agent/internal/glpi:decodeFollowup-\u003efunction:github.com/example/glpi-ai-agent/internal/glpi:boolVal:calls", "from": "function:github.com/example/glpi-ai-agent/internal/glpi:decodeFollowup", @@ -39813,6 +39901,13 @@ "kind": "calls_package", "label": "Slice" }, + { + "id": "function:github.com/example/glpi-ai-agent/internal/glpi:normalizeDocumentedAPIPath-\u003epackage:strings:calls_package", + "from": "function:github.com/example/glpi-ai-agent/internal/glpi:normalizeDocumentedAPIPath", + "to": "package:strings", + "kind": "calls_package", + "label": "TrimSpace" + }, { "id": "function:github.com/example/glpi-ai-agent/internal/glpi:openAPIOperations-\u003epackage:strings:calls_package", "from": "function:github.com/example/glpi-ai-agent/internal/glpi:openAPIOperations", @@ -66907,10 +67002,10 @@ } ], "meta": { - "edges": 7278, + "edges": 7289, "format_version": 1, "generator": "go-ast+compose", "modules": 4, - "nodes": 1846 + "nodes": 1848 } }