update-1.6.2a
ci / docker-build (push) Failing after 25s
ci / test (services/control) (push) Successful in 50s
ci / test (services/knowledge) (push) Successful in 52s
ci / test (platform/neuroforge) (push) Successful in 56s
ci / test (services/agent) (push) Successful in 1m1s
release-tag / release-image (push) Successful in 6m47s

This commit is contained in:
2026-09-09 14:14:05 +02:00
parent d18405ed2e
commit 9cf24dad23
10 changed files with 490 additions and 160 deletions
+1 -1
View File
@@ -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()
}
+128 -69
View File
@@ -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"]),
+139
View File
@@ -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) {
+2 -2
View File
@@ -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
}
@@ -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))
}
}
+5 -1
View File
@@ -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 {
@@ -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
@@ -107,6 +107,7 @@ const esc=s=>String(s??'').replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&
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')});
+161 -66
View File
@@ -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
}
}