diff --git a/.env.example b/.env.example index d3c6f7d..8ad45c1 100644 --- a/.env.example +++ b/.env.example @@ -655,6 +655,8 @@ GLPI_KB_FILTER= GLPI_KB_LIMIT=500 # Synchronisationsintervall. GLPI_KB_SYNC_INTERVAL=10m +# Gesamtzeitbudget fuer einen kompletten GLPI-KB-Refresh inklusive Embeddings. +GLPI_KB_SYNC_TIMEOUT=30m # source-Wert importierter GLPI-KB-Artikel. GLPI_KB_SOURCE=glpi-kb # true: diff --git a/MANIFEST.sha256 b/MANIFEST.sha256 index 0ed1e3f..b514761 100644 --- a/MANIFEST.sha256 +++ b/MANIFEST.sha256 @@ -1,5 +1,5 @@ 27dc46be5cbb1b171deff7fbd2f28bff1be802dff403797535fd8968bb98c8eb ./.cbmignore -079e174cd38e79421469cb5fc79ff4d886015cd5ec97c538466f97d70ce5331a ./.env.example +6b603e52548748b2e9717e53543905bc323d25ce3e541643e6bb0d4f4bc8bbce ./.env.example 813c6a53b389380997fc71835ef72d3b593e1808c4fcc16dc0c309608a389f40 ./.env.subagent.example 2d49d2ee955bf352cc7e76068ec842808c0bf7bffaacb6a129b641a78bbde0e5 ./.gitea/workflows/ci.yml ed22fda7661db8203563611dc144998161cd024e161b0471d615eaf0defeb7db ./.gitea/workflows/release-tag-gitea.yml @@ -61,7 +61,7 @@ e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 ./deployments/ 5d2ca8625ccb84cb868c3f7450a0c63fc128967aab5677363dfeab8f214f92c3 ./docker-bake.hcl 58db2a87b703a5bbddc20b140907d187f8388e7db87b079ae9f6ffaac9c121fc ./docker-compose.dev.yml 1360e44c867fdb9b754fe4585d57a02daf0f4820a7602a4e588c94c45eeb28e8 ./docker-compose.subagent.yml -0b5325d5e86ff75292031316d8ea2837e8c3eecbf77dcb67db39407edc1189fe ./docker-compose.yml +7883c941cbe0be323601661567168aebfd7ec2f441063c453cfce56fb7af1083 ./docker-compose.yml e4c516375e450c43b5193b1257880c02f2132b05a16e19ab1c96d57182fa070c ./docs/ARCHITECTURE.md 34e2c7ac5992389cc51b40f740c1e206f20bffc67bdf19cb59d94b5df67a7082 ./docs/BACKUP-RESTORE-v1.5.0.md 9467a3c0796a87bada0eab6ad191913e628b4abc05520cbb4b73fac76d334481 ./docs/CODEBASE-MEMORY-MCP.md @@ -413,11 +413,11 @@ a2c56eb668137e058cb78bb592ac359ef7e608cb9bedc7eaa930b3f4f66411ca ./scripts/stat 2970d5f5049323b14a3f3983c2f4eb5392d745b4fea34742e3cf56e52d740f17 ./scripts/validate.sh cc0fc4433db0964ed172a82f2910e55c0f3dd6d59b1ec04fe0541b8fe5a912bb ./scripts/verify-package.sh 17d7149607bf40ccc6f64a3c4cf3e4023a8325675918c18df0be309ea9731a36 ./services/agent/.dockerignore -68f757ffe33c55b622f0f2f3d907530419ac649e8510c47eaa8a05e08788e602 ./services/agent/.env.example +4db4de9cac0f05961cfdc20b8c7e741cdb64d71432f4b5162fa005cf2c19a4d3 ./services/agent/.env.example 236713daf159ff0a8067e80a442ae3404fa28a5251ae6f24782f263bcfc17005 ./services/agent/.gitea/workflows/registry.yml 9ee46f2156ee805c4ff40e418b004ead5cfbc3a9b6df3968a1b772fe1b313cdc ./services/agent/.gitignore -4c893d6049499eb70ca755a333e97f0a726dd212e4ded4a9d4acabf0a25d9bd8 ./services/agent/BETRIEBSANLEITUNG_GLPI_AI_AGENT.md -9339ce57dfc58d4731ea02c8d0ab9f2835e5966a29c071062fc6a2effd17da2c ./services/agent/BETRIEBSANLEITUNG_GLPI_AI_AGENT_OLLAMA_POOL.md +e947cd34f7188f09581e5d04dd45ceed6fcef530d88ca4138cc4299cec08b43e ./services/agent/BETRIEBSANLEITUNG_GLPI_AI_AGENT.md +5343fe8bde3261ac5cb8e60759a43e7ea44ca647287d1fad7dfc350672f799ac ./services/agent/BETRIEBSANLEITUNG_GLPI_AI_AGENT_OLLAMA_POOL.md 81c91a44c5d3b7eca3051d8cee35f231ef283563da7a636c56c98b61da60218b ./services/agent/Dockerfile 2ec93f81187ebf92665a6a15a6672b2f7dfce23bfea1546f2d4188f49de5dce0 ./services/agent/EMERGENCY-HOTFIX-TICKETVERARBEITUNG.md 2bce6e10780004bc12042a416b8a05b417130db2d730d01a95d9d86a564e01cd ./services/agent/ESCALATION.md @@ -432,10 +432,10 @@ c0e13c6703cf3feda57a9eaaa718e501280fbb526f466040eb685a27700b5797 ./services/age 0ed7ee0e4846be09994dd5f3b49ffdca6b59177b9018b1947388949f0dd49a14 ./services/agent/Makefile 80b20d2c2c78badc0dcc47e485e46bfd647877445c35faf261fb677ad58e8359 ./services/agent/OLLAMA-POOL.md 80b20d2c2c78badc0dcc47e485e46bfd647877445c35faf261fb677ad58e8359 ./services/agent/OLLAMA_POOL_BETRIEB.md -a7a3f969150005ef062e7a77cf852371931bc2fa5ca4be01cb0e9eb07d35335c ./services/agent/README.md +b0e1f907b26b31f3df4f9fae9ceaa59e468f3aa30487695fec89213912b685c8 ./services/agent/README.md aeac85d1adc92a8b34ffa5a95f7f19c2c6fb94742a7359a7fd452529d59ad87b ./services/agent/SECURITY.md -edb92cdf2d1863df53ea8045657f64e415f206d503c8da3b45ea96b7f7f0960d ./services/agent/UPGRADE.md -a499188fd29230a52949f2d5252d64d859b727e9d94a00889a025f30a1fa8981 ./services/agent/cmd/agent/main.go +ee0b752092cd437e215c05064560b455e844c4a190780afe70900b9a275a2c2b ./services/agent/UPGRADE.md +5f12f3841c224f7a1658ec34fcb93d7ae8d91030f80469ecba25bfeae0c91c61 ./services/agent/cmd/agent/main.go 98be0fd63dd4fde52342c5385da113c926ac29195aedb657c0ca0f98e1593fff ./services/agent/compose_local.yml e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 ./services/agent/data/.gitkeep b74f901cca99eae4bed1b168f1f077a28bf1134a71c867d263c7834f6d896684 ./services/agent/deploy/glpi-ai-agent.service @@ -458,19 +458,19 @@ c4bf8fcb4ba6eb7962ec55cacff806a8a99561d134ab5238abd012763a2f9c74 ./services/age 6b70a29176602e499935a85ac8831dbe36bf75fddb130a56cda376e124ecb7db ./services/agent/internal/agent/status_reply.go 6f95bd3c85b42f1b2b098f05828c0b9e3dbb2e57f1f89b3dc4d1806453e0c4ce ./services/agent/internal/agent/status_reply_test.go 9057a742c3fe8e0fa76efd6dd9bcdf9fcdf7ab2c37ed823822808946e8649566 ./services/agent/internal/brainactivity/client.go -c8da5784acc4f28d632f357dadbfc61209a37b698525dae40473d27625d9c8e2 ./services/agent/internal/config/config.go +3ce010749e2d378249e2da9ebd0471673a6c558db9273940788ddcc096d7103d ./services/agent/internal/config/config.go fa1717032ad536bff867a07d3fdf0ae2e0a9d5d7637a27b43274832d51322eb6 ./services/agent/internal/config/config_test.go 6fe7335b0e66b60d5f6f27583b1ac11a0948719cfa657d51e77de0b337aab5d5 ./services/agent/internal/contextdata/collector.go 12e9eb6fd567b5d4173a63fa2b89be913b6d1c97e17b69048f46d119e88dce26 ./services/agent/internal/contextdata/collector_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 +f48e63d605790b9b6e3307accf206034c98ec06e7b8c7900d33b6e44b4f97e6c ./services/agent/internal/glpikb/sync.go +6411c727c1b28da1748bd2ab08da3ea4621dfd32bdf36c02c3ea4253bac50532 ./services/agent/internal/glpikb/sync_test.go 787ab6b53623aa36ef6628f4eada11dcb4ed3011b34effd691bf07ba5d8f214d ./services/agent/internal/knowledge/category_mapping.go d3a9909672ea420db87be29ce669f3cd973125ff15bb46ca90a54c3aa98d9e73 ./services/agent/internal/knowledge/neuroforge_backend.go -7bcd33bfcfb99fb06762fe75f4646308428957ed7eca26a266891ab580c3a929 ./services/agent/internal/knowledge/persistent_index.go -df1ac214ebb88bf3ce7f68112129cf9ffc300dd725380c38661b85b04908ecdf ./services/agent/internal/knowledge/store.go -63e47d86bcc89178fdc0f91e07ecb30bd01ee4b84bf29bfc660debbf26f8a40b ./services/agent/internal/knowledge/store_test.go +151a88dcb7e649473acc68765058d660d7877e6cf2b111b301b41d6950d59c43 ./services/agent/internal/knowledge/persistent_index.go +50342192dca4b4b06c560f37f6f6d139b51d8def89923a02243f5db29f41fe2a ./services/agent/internal/knowledge/store.go +2b462c8e4eaae31420a1bc0b77d41ac9d1b2f55cab616d1e206fd622fc37b6ef ./services/agent/internal/knowledge/store_test.go 807996228b38e1cd97ec2516cb1ecfc4bd945b638fcde465a0991051df2f1a00 ./services/agent/internal/learning/outcomes.go 2917b3dab63783af4c92c48f269f097dc94b989008a5451f46d6c1b309ccdc9b ./services/agent/internal/learning/outcomes_test.go 482ca79552b02a8a53bbd547318a04e01a0b41e64d4a347b00f850c0218febda ./services/agent/internal/learning/store.go @@ -481,8 +481,8 @@ df1ac214ebb88bf3ce7f68112129cf9ffc300dd725380c38661b85b04908ecdf ./services/age 821850d649c7661f1665dc6be030ebeeacb60e4e007e1e84891d4b779b9a5d0d ./services/agent/internal/model/reason_codes_test.go c89248224adf72720289215684b916eb209387c1d38b757d64804a8d3148011c ./services/agent/internal/obsidian/export.go b75ffec53e9a94505ee57ecb9361bffbb6805cfd06ea05a7bf68b6b28b268c09 ./services/agent/internal/obsidian/export_test.go -f4e32c5653b5fc3e046f1536513d376a7c0ac11b392210a107e12bd0ee63e179 ./services/agent/internal/ollama/client.go -3522e7687e38db771dcf7e05b8dc52ad99a2fbc53d98dbdf07ded781d68b248e ./services/agent/internal/ollama/client_test.go +73ab85693941c2af0ab6999a366e4d5baab629405233869699001123110bed32 ./services/agent/internal/ollama/client.go +32949d24dbfef098426ed1891d27860b295a8d7f312972020beb6976360705e3 ./services/agent/internal/ollama/client_test.go 0e4e98a3db5ad781a4f7022bd74bbc4b434e64187afee077daf9174165509ea6 ./services/agent/internal/ollama/pool.go 4da6a7f953bfce654dbd8ab2267d1d519057fb230773e37538ce81aa049f9751 ./services/agent/internal/ollama/pool_test.go ab29c5808a06f6b21a0776355eeaa030f69be5eee251e7b1dbd7e00c71045c61 ./services/agent/internal/prioritysignals/signals.go @@ -607,7 +607,7 @@ f1eab883370e0a40ef52a6b6d785a510d8ed48a19d25e0a2bc95f4f2cc8e329e ./services/age 895f8400aaad550ff2b96262eaa54e954d603129682e97aa4964539eb19bd7c2 ./services/agent/run.ps1 d1d4ab8145a7131c36df51d3a9fbdcdeb0cda3de134fe24c0e8d52370862f3b2 ./services/control/Dockerfile f30ddf9251860d92717276483f7a2c2d7405f0516940ca62abc21d97ff3f3ad2 ./services/control/cmd/engineering-graph/main.go -c61a014f75ceb8c2d9365d5c053d952f80cb60faa1fbc4f3049999450b49adae ./services/control/engineering-graph.json +5b2b6ec4790c89d07834c4e52bbde9a75d67f72dd0ec1b5b4ad033958043fac3 ./services/control/engineering-graph.json ba44c599b9faf861eca647614c4abd18548e2708233cf462b11eb277e96449b8 ./services/control/go.mod da37393e58ff53847f26b1051f6d3a5270571d290b33d79360bae0dcb3513829 ./services/control/graph.go e6a8021a44219219fe37ab5edc45d1908fc6579f653aa80c9966810d891ad27d ./services/control/graph_test.go diff --git a/docker-compose.yml b/docker-compose.yml index 9e3936b..4ffe63b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -306,6 +306,7 @@ services: GLPI_KB_PATH: ${GLPI_KB_PATH:-} GLPI_KB_SOURCE: ${GLPI_KB_SOURCE:-} GLPI_KB_SYNC_INTERVAL: ${GLPI_KB_SYNC_INTERVAL:-} + GLPI_KB_SYNC_TIMEOUT: ${GLPI_KB_SYNC_TIMEOUT:-} GLPI_MAJOR_INCIDENT_FILTER: ${GLPI_MAJOR_INCIDENT_FILTER:-} GLPI_MAJOR_INCIDENT_LIMIT: ${GLPI_MAJOR_INCIDENT_LIMIT:-} GLPI_PASSWORD: ${GLPI_PASSWORD:-} diff --git a/services/agent/.env.example b/services/agent/.env.example index 4738654..e6cb35a 100644 --- a/services/agent/.env.example +++ b/services/agent/.env.example @@ -518,6 +518,8 @@ GLPI_KB_FILTER= GLPI_KB_LIMIT=500 # Synchronisationsintervall. GLPI_KB_SYNC_INTERVAL=10m +# Gesamtzeitbudget fuer einen kompletten GLPI-KB-Refresh inklusive Embeddings. +GLPI_KB_SYNC_TIMEOUT=30m # source-Wert importierter GLPI-KB-Artikel. GLPI_KB_SOURCE=glpi-kb # true: diff --git a/services/agent/BETRIEBSANLEITUNG_GLPI_AI_AGENT.md b/services/agent/BETRIEBSANLEITUNG_GLPI_AI_AGENT.md index 8198c0a..2cce8c0 100644 --- a/services/agent/BETRIEBSANLEITUNG_GLPI_AI_AGENT.md +++ b/services/agent/BETRIEBSANLEITUNG_GLPI_AI_AGENT.md @@ -816,6 +816,7 @@ Für gemeinsam genutzte Knowledge-Verzeichnisse ist `unscoped` der kompatibelste | `GLPI_KB_FILTER` | Agent | Optionaler serverseitiger GLPI-Filter. alle für den Service Account sichtbaren Artikel, begrenzt durch LIMIT. | Freier Text beziehungsweise installationsspezifischer Wert. | leer | leer | Optional; Wirkung abhängig von aktivierten Funktionen. | | `GLPI_KB_LIMIT` | Agent | Maximale Anzahl GLPI-KB-Artikel. | 1–5000. | 500 | 500 | Optional; Wirkung abhängig von aktivierten Funktionen. | | `GLPI_KB_SYNC_INTERVAL` | Agent | Synchronisationsintervall. | Dauer >= 1m. | 10m | 10m | Optional; Wirkung abhängig von aktivierten Funktionen. | +| `GLPI_KB_SYNC_TIMEOUT` | Agent | Gesamtzeitbudget eines GLPI-Wissens-Syncs inklusive Embedding/Reindexierung. `GLPI_TIMEOUT` bleibt das Timeout je GLPI-HTTP-Request. | 0 oder Dauer >= 1m. | 30m | 30m | Für große KBs ausreichend hoch wählen. | | `GLPI_KB_SOURCE` | Agent | source-Wert importierter GLPI-KB-Artikel. | Freier Text beziehungsweise installationsspezifischer Wert. | glpi-kb | glpi-kb | Optional; Wirkung abhängig von aktivierten Funktionen. | | `GLPI_KB_AUTO_REPLY` | Agent | GLPI-KB-Artikel können grundsätzlich Auto-Replies auslösen. Zusätzlich gelten weiterhin alle anderen Policy-Gates. | true \| false | false | true | Bei true: `GLPI_KB_SOURCE` muss in normalen und Auto-Reply-Quellen stehen; außerdem ist mindestens eine KB-Kategorie oder eine explizite Allowlist unkategorisierter Artikel erforderlich. | | `GLPI_KB_AUTO_REPLY_CATEGORY_IDS` | Agent | Whitelist der GLPI-Knowledge-Base-Kategorie-IDs. Ein kategorisierter Artikel ist nur dann grundsätzlich für Auto-Reply freigegeben, wenn mindestens eine seiner KB-Kategorien enthalten ist. Dies sind nicht die ITIL-/Ticketkategorie-IDs. | Kommagetrennte positive GLPI-KB-Kategorie-IDs; leer/none = keine. | leer | 1 | Kann allein oder zusammen mit der Allowlist unkategorisierter Artikel verwendet werden. | diff --git a/services/agent/BETRIEBSANLEITUNG_GLPI_AI_AGENT_OLLAMA_POOL.md b/services/agent/BETRIEBSANLEITUNG_GLPI_AI_AGENT_OLLAMA_POOL.md index aa11b4f..babbde0 100644 --- a/services/agent/BETRIEBSANLEITUNG_GLPI_AI_AGENT_OLLAMA_POOL.md +++ b/services/agent/BETRIEBSANLEITUNG_GLPI_AI_AGENT_OLLAMA_POOL.md @@ -816,6 +816,7 @@ Für gemeinsam genutzte Knowledge-Verzeichnisse ist `unscoped` der kompatibelste | `GLPI_KB_FILTER` | Agent | Optionaler serverseitiger GLPI-Filter. alle für den Service Account sichtbaren Artikel, begrenzt durch LIMIT. | Freier Text beziehungsweise installationsspezifischer Wert. | leer | leer | Optional; Wirkung abhängig von aktivierten Funktionen. | | `GLPI_KB_LIMIT` | Agent | Maximale Anzahl GLPI-KB-Artikel. | 1–5000. | 500 | 500 | Optional; Wirkung abhängig von aktivierten Funktionen. | | `GLPI_KB_SYNC_INTERVAL` | Agent | Synchronisationsintervall. | Dauer >= 1m. | 10m | 10m | Optional; Wirkung abhängig von aktivierten Funktionen. | +| `GLPI_KB_SYNC_TIMEOUT` | Agent | Gesamtzeitbudget eines GLPI-Wissens-Syncs inklusive Embedding/Reindexierung. `GLPI_TIMEOUT` bleibt das Timeout je GLPI-HTTP-Request. | 0 oder Dauer >= 1m. | 30m | 30m | Für große KBs ausreichend hoch wählen. | | `GLPI_KB_SOURCE` | Agent | source-Wert importierter GLPI-KB-Artikel. | Freier Text beziehungsweise installationsspezifischer Wert. | glpi-kb | glpi-kb | Optional; Wirkung abhängig von aktivierten Funktionen. | | `GLPI_KB_AUTO_REPLY` | Agent | GLPI-KB-Artikel können grundsätzlich Auto-Replies auslösen. Zusätzlich gelten weiterhin alle anderen Policy-Gates. | true \| false | false | true | Bei true: GLPI_KB_SOURCE muss in normalen und Auto-Reply-Quellen stehen; Kategorie-ID-Whitelist darf nicht leer sein. | | `GLPI_KB_AUTO_REPLY_CATEGORY_IDS` | Agent | Whitelist der GLPI KNOWLEDGE-BASE-Kategorie-IDs. WICHTIG: Dies sind NICHT die ITIL-/Ticketkategorie-IDs. Mehrere Werte: 1,2,7 | Kommagetrennte positive GLPI-KB-Kategorie-IDs; leer/none = keine. | nicht ermittelt | 1 | Optional; Wirkung abhängig von aktivierten Funktionen. | diff --git a/services/agent/README.md b/services/agent/README.md index 9beba41..5a4ca49 100644 --- a/services/agent/README.md +++ b/services/agent/README.md @@ -244,6 +244,7 @@ KNOWLEDGE_ALLOWED_SOURCES=internal-kb,glpi-kb GLPI_KB_ENABLED=true GLPI_KB_PATH=auto GLPI_KB_SYNC_INTERVAL=10m +GLPI_KB_SYNC_TIMEOUT=30m GLPI_KB_LIMIT=500 GLPI_KB_SOURCE=glpi-kb ``` diff --git a/services/agent/UPGRADE.md b/services/agent/UPGRADE.md index e6fb5a6..b6b160b 100644 --- a/services/agent/UPGRADE.md +++ b/services/agent/UPGRADE.md @@ -196,6 +196,7 @@ KNOWLEDGE_ALLOWED_SOURCES=internal-kb,glpi-kb GLPI_KB_ENABLED=true GLPI_KB_PATH=auto GLPI_KB_SYNC_INTERVAL=10m +GLPI_KB_SYNC_TIMEOUT=30m GLPI_KB_LIMIT=500 GLPI_KB_SOURCE=glpi-kb GLPI_KB_AUTO_REPLY=false diff --git a/services/agent/cmd/agent/main.go b/services/agent/cmd/agent/main.go index 8b57d04..88339c1 100644 --- a/services/agent/cmd/agent/main.go +++ b/services/agent/cmd/agent/main.go @@ -189,7 +189,7 @@ func main() { if err := kbSync.LoadCache(ctx); err != nil { slog.Warn("GLPI knowledge cache unavailable", "error", err) } - syncCtx, syncCancel := context.WithTimeout(ctx, maxDuration(cfg.GLPITimeout*3, 30*time.Second)) + syncCtx, syncCancel := context.WithTimeout(ctx, glpikb.SyncTimeout(cfg)) if err := kbSync.Sync(syncCtx); err != nil { slog.Error("initial GLPI knowledge base sync failed; continuing with local/cache knowledge", "error", err) } diff --git a/services/agent/internal/config/config.go b/services/agent/internal/config/config.go index e274989..12f9463 100644 --- a/services/agent/internal/config/config.go +++ b/services/agent/internal/config/config.go @@ -99,6 +99,7 @@ type Config struct { GLPIKBFilter string GLPIKBLimit int GLPIKBSyncInterval time.Duration + GLPIKBSyncTimeout time.Duration GLPIKBSource string GLPIKBAutoReply bool GLPIKBAutoReplyCategoryIDs []int64 @@ -306,6 +307,7 @@ func Load() (Config, error) { GLPIKBFilter: strings.TrimSpace(os.Getenv("GLPI_KB_FILTER")), GLPIKBLimit: envInt("GLPI_KB_LIMIT", 500), GLPIKBSyncInterval: envDuration("GLPI_KB_SYNC_INTERVAL", 10*time.Minute), + GLPIKBSyncTimeout: envDuration("GLPI_KB_SYNC_TIMEOUT", 30*time.Minute), GLPIKBSource: strings.ToLower(env("GLPI_KB_SOURCE", "glpi-kb")), GLPIKBAutoReply: envBool("GLPI_KB_AUTO_REPLY", false), GLPIKBAutoReplyCategoryIDs: envInt64ListAllowEmpty("GLPI_KB_AUTO_REPLY_CATEGORY_IDS"), @@ -738,6 +740,9 @@ func (c Config) Validate() error { if c.GLPIKBSyncInterval < time.Minute { return errors.New("GLPI_KB_SYNC_INTERVAL must be at least 1m") } + if c.GLPIKBSyncTimeout < 0 || (c.GLPIKBSyncTimeout > 0 && c.GLPIKBSyncTimeout < time.Minute) { + return errors.New("GLPI_KB_SYNC_TIMEOUT must be 0 or at least 1m") + } if c.GLPIKBPath != "auto" && !validAPIPath(c.GLPIKBPath) { return errors.New("GLPI_KB_PATH must be 'auto' or an absolute API path") } diff --git a/services/agent/internal/glpikb/sync.go b/services/agent/internal/glpikb/sync.go index 3417382..5a36fda 100644 --- a/services/agent/internal/glpikb/sync.go +++ b/services/agent/internal/glpikb/sync.go @@ -51,7 +51,21 @@ type Syncer struct { count int } -const cachePolicyVersion = 2 +const ( + cachePolicyVersion = 2 + defaultKnowledgeSyncLimit = 30 * time.Minute +) + +// SyncTimeout is the total budget for one complete GLPI knowledge refresh, +// including embedding/re-indexing. GLPI_TIMEOUT remains the per-request HTTP +// timeout; using it as the total sync budget aborts larger knowledge bases long +// before their embeddings can finish. +func SyncTimeout(cfg config.Config) time.Duration { + if cfg.GLPIKBSyncTimeout > 0 { + return cfg.GLPIKBSyncTimeout + } + return defaultKnowledgeSyncLimit +} type cacheFile struct { PolicyVersion int `json:"policy_version"` @@ -246,7 +260,7 @@ func (s *Syncer) Start(ctx context.Context) { case <-ctx.Done(): return case <-t.C: - c, cancel := context.WithTimeout(ctx, maxDuration(s.cfg.GLPITimeout*3, 30*time.Second)) + c, cancel := context.WithTimeout(ctx, SyncTimeout(s.cfg)) if err := s.Sync(c); err != nil { slog.Error("GLPI knowledge base sync failed", "error", err) } diff --git a/services/agent/internal/glpikb/sync_test.go b/services/agent/internal/glpikb/sync_test.go index f366e84..6226299 100644 --- a/services/agent/internal/glpikb/sync_test.go +++ b/services/agent/internal/glpikb/sync_test.go @@ -269,3 +269,12 @@ func TestSyncPreservesGLPIKnowledgeLinkedItems(t *testing.T) { t.Fatalf("unexpected linked item: %#v", got) } } + +func TestSyncTimeoutUsesDedicatedKnowledgeBudget(t *testing.T) { + if got := SyncTimeout(config.Config{}); got != 30*time.Minute { + t.Fatalf("default sync timeout=%s, want 30m", got) + } + if got := SyncTimeout(config.Config{GLPIKBSyncTimeout: 7 * time.Minute}); got != 7*time.Minute { + t.Fatalf("configured sync timeout=%s, want 7m", got) + } +} diff --git a/services/agent/internal/knowledge/persistent_index.go b/services/agent/internal/knowledge/persistent_index.go index 83cc084..d493048 100644 --- a/services/agent/internal/knowledge/persistent_index.go +++ b/services/agent/internal/knowledge/persistent_index.go @@ -133,12 +133,17 @@ func (s *Store) loadPersistentSnapshot() (bool, error) { // them to the current knowledge roots using the stable manifest key. reboundFiles := map[string]string{} for key, rec := range snap.Manifest { - name := filepath.Base(key) + rel := strings.TrimPrefix(key, "static/") + root := s.dir if strings.HasPrefix(key, "managed/") { - rec.Path = filepath.Join(s.managedDir, name) - } else { - rec.Path = filepath.Join(s.dir, name) + rel = strings.TrimPrefix(key, "managed/") + root = s.managedDir } + rel = filepath.Clean(filepath.FromSlash(rel)) + if rel == "." || filepath.IsAbs(rel) || rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) { + return false, fmt.Errorf("persistent knowledge index contains invalid manifest path %q", key) + } + rec.Path = filepath.Join(root, rel) snap.Manifest[key] = rec if rec.Included && rec.ID != "" { if rec.Managed || reboundFiles[rec.ID] == "" { @@ -511,7 +516,7 @@ type deltaScanResult struct { func (s *Store) scanDeltaDir(ctx context.Context, dir, origin string, managed bool, opts LoadOptions, old map[string]fileRecord, oldDocs map[string]model.KnowledgeDoc) (deltaScanResult, error) { res := deltaScanResult{docs: map[string]model.KnowledgeDoc{}, manifest: map[string]fileRecord{}} - entries, err := os.ReadDir(dir) + entries, err := listKnowledgeJSONFiles(dir) if err != nil { return res, fmt.Errorf("read knowledge directory %q: %w", dir, err) } @@ -519,16 +524,10 @@ func (s *Store) scanDeltaDir(ctx context.Context, dir, origin string, managed bo if err := ctx.Err(); err != nil { return res, err } - if e.IsDir() || !strings.HasSuffix(strings.ToLower(e.Name()), ".json") { - continue - } - key := origin + "/" + e.Name() - path := filepath.Join(dir, e.Name()) - info, err := e.Info() - if err != nil { - return res, err - } - if matchesAnyGlob(e.Name(), opts.IgnoreGlobs) { + key := origin + "/" + e.Rel + path := e.Path + info := e.Info + if matchesAnyGlob(e.Rel, opts.IgnoreGlobs) { res.manifest[key] = fileRecord{Key: key, Path: path, Size: info.Size(), ModTimeUnixNano: info.ModTime().UnixNano(), Managed: managed, Ignored: true} res.stats.IgnoredFiles++ continue @@ -572,7 +571,7 @@ func (s *Store) scanDeltaDir(ctx context.Context, dir, origin string, managed bo } d, unmapped, skip, err := decodeKnowledgeDoc(b, opts.CategoryMode, s.categoryMap) if err != nil { - return res, fmt.Errorf("%s: %w", e.Name(), err) + return res, fmt.Errorf("%s: %w", e.Rel, err) } rec := fileRecord{Key: key, Path: path, Size: info.Size(), ModTimeUnixNano: info.ModTime().UnixNano(), RawHash: rawHash, Managed: managed, Unmapped: append([]string(nil), unmapped...)} if len(unmapped) > 0 { @@ -587,14 +586,14 @@ func (s *Store) scanDeltaDir(ctx context.Context, dir, origin string, managed bo continue } if d.ID == "" || d.Title == "" { - return res, fmt.Errorf("%s: id/title required", e.Name()) + return res, fmt.Errorf("%s: id/title required", e.Rel) } if !safeID(d.ID) { - return res, fmt.Errorf("%s: invalid id %q", e.Name(), d.ID) + return res, fmt.Errorf("%s: invalid id %q", e.Rel, d.ID) } d.Source = strings.ToLower(strings.TrimSpace(d.Source)) if d.Source == "" { - return res, fmt.Errorf("%s: source required", e.Name()) + return res, fmt.Errorf("%s: source required", e.Rel) } if _, allowed := s.allowedSources[d.Source]; !allowed { res.manifest[key] = rec @@ -629,7 +628,7 @@ func mergeLoadStats(a, b LoadStats) LoadStats { return LoadStats{IgnoredFiles: a.IgnoredFiles + b.IgnoredFiles, UnmappedCategoryFiles: a.UnmappedCategoryFiles + b.UnmappedCategoryFiles, UnmappedCategories: mergeStrings(a.UnmappedCategories, b.UnmappedCategories)} } -func buildManifestForDocs(docs []model.KnowledgeDoc, files []string, origin string, managed bool) (map[string]fileRecord, error) { +func buildManifestForDocs(docs []model.KnowledgeDoc, files []string, root, origin string, managed bool) (map[string]fileRecord, error) { out := make(map[string]fileRecord, len(docs)) for i, d := range docs { if i >= len(files) { @@ -645,7 +644,11 @@ func buildManifestForDocs(docs []model.KnowledgeDoc, files []string, origin stri return nil, err } h := sha256.Sum256(b) - key := origin + "/" + filepath.Base(path) + rel, err := filepath.Rel(root, path) + if err != nil { + return nil, err + } + key := origin + "/" + filepath.ToSlash(rel) out[key] = fileRecord{Key: key, Path: path, ID: d.ID, Size: info.Size(), ModTimeUnixNano: info.ModTime().UnixNano(), RawHash: hex.EncodeToString(h[:]), Managed: managed, Included: true, Unmapped: append([]string(nil), d.UnmappedExternalCategories...)} } return out, nil @@ -688,24 +691,18 @@ func (s *Store) persistExternalVectorCache(source string) error { // delta scans do not repeatedly open files that are intentionally not part of // the active corpus. func augmentManifestAllFiles(dir, origin string, managed bool, opts LoadOptions, manifest map[string]fileRecord) error { - entries, err := os.ReadDir(dir) + entries, err := listKnowledgeJSONFiles(dir) if err != nil { return err } for _, e := range entries { - if e.IsDir() || !strings.HasSuffix(strings.ToLower(e.Name()), ".json") { - continue - } - key := origin + "/" + e.Name() + key := origin + "/" + e.Rel if _, ok := manifest[key]; ok { continue } - info, err := e.Info() - if err != nil { - return err - } - path := filepath.Join(dir, e.Name()) - rec := fileRecord{Key: key, Path: path, Size: info.Size(), ModTimeUnixNano: info.ModTime().UnixNano(), Managed: managed, Included: false, Ignored: matchesAnyGlob(e.Name(), opts.IgnoreGlobs)} + info := e.Info + path := e.Path + rec := fileRecord{Key: key, Path: path, Size: info.Size(), ModTimeUnixNano: info.ModTime().UnixNano(), Managed: managed, Included: false, Ignored: matchesAnyGlob(e.Rel, opts.IgnoreGlobs)} if !rec.Ignored { b, err := os.ReadFile(path) if err != nil { diff --git a/services/agent/internal/knowledge/store.go b/services/agent/internal/knowledge/store.go index 9757b31..49a99af 100644 --- a/services/agent/internal/knowledge/store.go +++ b/services/agent/internal/knowledge/store.go @@ -237,6 +237,15 @@ func (s *Store) Initialize(ctx context.Context) (err error) { if _, err := s.syncLoadedSemanticBackend(ctx); err != nil { return err } + // A persistent snapshot is a warm start, not proof that the mounted + // knowledge directory is unchanged. Reconcile it before ticket workers + // start so files copied while the agent was stopped are available on the + // first ticket instead of only after the background scan eventually ends. + if mode == "incremental" { + if err := s.SyncLocal(ctx); err != nil { + return fmt.Errorf("reconcile persistent knowledge index: %w", err) + } + } return nil } else if mode == "readonly" { return fmt.Errorf("KNOWLEDGE_INDEX_MODE=readonly requires a compatible persistent index at %s", s.snapshotPath) @@ -294,11 +303,11 @@ func (s *Store) fullRebuild(ctx context.Context) (err error) { stats.UnmappedCategoryFiles += managedStats.UnmappedCategoryFiles stats.UnmappedCategories = mergeStrings(stats.UnmappedCategories, managedStats.UnmappedCategories) - staticManifest, err := buildManifestForDocs(static, staticFiles, "static", false) + staticManifest, err := buildManifestForDocs(static, staticFiles, s.dir, "static", false) if err != nil { return fmt.Errorf("build static knowledge manifest: %w", err) } - managedManifest, err := buildManifestForDocs(managed, managedFiles, "managed", true) + managedManifest, err := buildManifestForDocs(managed, managedFiles, s.managedDir, "managed", true) if err != nil { return fmt.Errorf("build managed knowledge manifest: %w", err) } @@ -405,44 +414,71 @@ func (s *Store) InitStatus() InitStatus { func (s *Store) Ready() bool { return s != nil && s.InitStatus().State == "ready" } +type knowledgeJSONFile struct { + Rel string + Path string + Info os.FileInfo +} + +// listKnowledgeJSONFiles recursively discovers JSON knowledge documents. Large +// exports are commonly grouped into category/subcategory folders; limiting the +// scan to the top-level directory makes such a mounted corpus appear empty. +func listKnowledgeJSONFiles(dir string) ([]knowledgeJSONFile, error) { + files := make([]knowledgeJSONFile, 0) + err := filepath.WalkDir(dir, func(path string, entry os.DirEntry, walkErr error) error { + if walkErr != nil { + return walkErr + } + if entry.IsDir() || !strings.HasSuffix(strings.ToLower(entry.Name()), ".json") { + return nil + } + rel, err := filepath.Rel(dir, path) + if err != nil { + return err + } + info, err := entry.Info() + if err != nil { + return err + } + files = append(files, knowledgeJSONFile{Rel: filepath.ToSlash(rel), Path: path, Info: info}) + return nil + }) + if err != nil { + return nil, err + } + sort.Slice(files, func(i, j int) bool { return files[i].Rel < files[j].Rel }) + return files, nil +} + func readDocs(dir string, allowed map[string]struct{}, opts LoadOptions, categoryMap map[string][]int64, progress func(total, processed, loaded int)) ([]model.KnowledgeDoc, []string, LoadStats, error) { - entries, err := os.ReadDir(dir) + entries, err := listKnowledgeJSONFiles(dir) if err != nil { return nil, nil, LoadStats{}, fmt.Errorf("read knowledge directory %q: %w", dir, err) } var docs []model.KnowledgeDoc var files []string stats := LoadStats{} - total := 0 - for _, e := range entries { - if !e.IsDir() && strings.HasSuffix(strings.ToLower(e.Name()), ".json") { - total++ - } - } + total := len(entries) processed := 0 if progress != nil { progress(total, 0, 0) } for _, e := range entries { - if e.IsDir() || !strings.HasSuffix(strings.ToLower(e.Name()), ".json") { - continue - } processed++ - if matchesAnyGlob(e.Name(), opts.IgnoreGlobs) { + if matchesAnyGlob(e.Rel, opts.IgnoreGlobs) { stats.IgnoredFiles++ if progress != nil { progress(total, processed, len(docs)) } continue } - path := filepath.Join(dir, e.Name()) - b, err := os.ReadFile(path) + b, err := os.ReadFile(e.Path) if err != nil { return nil, nil, stats, err } d, unmapped, skip, err := decodeKnowledgeDoc(b, opts.CategoryMode, categoryMap) if err != nil { - return nil, nil, stats, fmt.Errorf("%s: %w", e.Name(), err) + return nil, nil, stats, fmt.Errorf("%s: %w", e.Rel, err) } if len(unmapped) > 0 { stats.UnmappedCategoryFiles++ @@ -456,14 +492,14 @@ func readDocs(dir string, allowed map[string]struct{}, opts LoadOptions, categor continue } if d.ID == "" || d.Title == "" { - return nil, nil, stats, fmt.Errorf("%s: id/title required", e.Name()) + return nil, nil, stats, fmt.Errorf("%s: id/title required", e.Rel) } if !safeID(d.ID) { - return nil, nil, stats, fmt.Errorf("%s: invalid id %q", e.Name(), d.ID) + return nil, nil, stats, fmt.Errorf("%s: invalid id %q", e.Rel, d.ID) } d.Source = strings.ToLower(strings.TrimSpace(d.Source)) if d.Source == "" { - return nil, nil, stats, fmt.Errorf("%s: source required", e.Name()) + return nil, nil, stats, fmt.Errorf("%s: source required", e.Rel) } if _, ok := allowed[d.Source]; !ok { if progress != nil { @@ -474,7 +510,7 @@ func readDocs(dir string, allowed map[string]struct{}, opts LoadOptions, categor d.Language = strings.TrimSpace(d.Language) d.CommunicationStyle = strings.ToLower(strings.TrimSpace(d.CommunicationStyle)) docs = append(docs, d) - files = append(files, path) + files = append(files, e.Path) if progress != nil { progress(total, processed, len(docs)) } @@ -654,10 +690,23 @@ func parseMappingIDs(raw json.RawMessage) ([]int64, error) { } func matchesAnyGlob(name string, patterns []string) bool { + name = filepath.ToSlash(name) + base := filepath.Base(filepath.FromSlash(name)) for _, pattern := range patterns { - if ok, _ := filepath.Match(pattern, name); ok { + pattern = filepath.ToSlash(strings.TrimSpace(pattern)) + if pattern == "" { + continue + } + if ok, _ := filepath.Match(filepath.FromSlash(pattern), filepath.FromSlash(name)); ok { return true } + // Backwards compatibility: filename-only patterns continue to match files + // inside newly supported nested knowledge directories. + if !strings.Contains(pattern, "/") { + if ok, _ := filepath.Match(pattern, base); ok { + return true + } + } } return false } diff --git a/services/agent/internal/knowledge/store_test.go b/services/agent/internal/knowledge/store_test.go index 34c9e1b..cda31b1 100644 --- a/services/agent/internal/knowledge/store_test.go +++ b/services/agent/internal/knowledge/store_test.go @@ -716,3 +716,109 @@ func TestNeuroForgeSearchFailureHonorsFailOpenPolicy(t *testing.T) { t.Fatal("fail-closed search must surface backend failure") } } + +func TestKnowledgeLoadsAndIncrementallyTracksNestedJSONFiles(t *testing.T) { + dir := t.TempDir() + data := t.TempDir() + nested := filepath.Join(dir, "department", "network") + if err := os.MkdirAll(nested, 0o755); err != nil { + t.Fatal(err) + } + doc1 := `{"id":"NESTED-1","title":"VPN nested","text":"gateway vpn","answer":"x","source":"internal-kb","language":"de-DE","communication_style":"formal"}` + if err := os.WriteFile(filepath.Join(nested, "vpn.json"), []byte(doc1), 0o644); err != nil { + t.Fatal(err) + } + + s, err := Load(context.Background(), dir, data, nil, false, []string{"internal-kb"}) + if err != nil { + t.Fatal(err) + } + if s.Count() != 1 { + t.Fatalf("nested initial count=%d, want 1", s.Count()) + } + if _, ok := s.manifest["static/department/network/vpn.json"]; !ok { + t.Fatalf("nested manifest key missing: %#v", s.manifest) + } + + doc2 := `{"id":"NESTED-2","title":"DNS nested","text":"resolver dns","answer":"x","source":"internal-kb","language":"de-DE","communication_style":"formal"}` + deeper := filepath.Join(dir, "department", "dns", "prod") + if err := os.MkdirAll(deeper, 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(deeper, "dns.json"), []byte(doc2), 0o644); err != nil { + t.Fatal(err) + } + if err := s.SyncLocal(context.Background()); err != nil { + t.Fatal(err) + } + if s.Count() != 2 { + t.Fatalf("nested incremental count=%d, want 2", s.Count()) + } + if _, ok := s.manifest["static/department/dns/prod/dns.json"]; !ok { + t.Fatalf("nested incremental manifest key missing: %#v", s.manifest) + } +} + +func TestNestedKnowledgeSnapshotRebindsFullRelativePath(t *testing.T) { + dir := t.TempDir() + data := t.TempDir() + nested := filepath.Join(dir, "a", "b") + if err := os.MkdirAll(nested, 0o755); err != nil { + t.Fatal(err) + } + doc := `{"id":"SNAP-NESTED","title":"Nested snapshot","text":"nested knowledge","answer":"x","source":"internal-kb","language":"de-DE","communication_style":"formal"}` + path := filepath.Join(nested, "item.json") + if err := os.WriteFile(path, []byte(doc), 0o644); err != nil { + t.Fatal(err) + } + first, err := Load(context.Background(), dir, data, nil, false, []string{"internal-kb"}) + if err != nil { + t.Fatal(err) + } + if first.files["SNAP-NESTED"] != path { + t.Fatalf("initial nested path=%q, want %q", first.files["SNAP-NESTED"], path) + } + + second, err := Load(context.Background(), dir, data, nil, false, []string{"internal-kb"}) + if err != nil { + t.Fatal(err) + } + if second.files["SNAP-NESTED"] != path { + t.Fatalf("rebound nested path=%q, want %q", second.files["SNAP-NESTED"], path) + } + if !second.InitStatus().SnapshotLoaded { + t.Fatal("expected second load to use persistent snapshot") + } +} + +func TestInitializeReconcilesFilesAddedAfterEmptySnapshot(t *testing.T) { + dir := t.TempDir() + data := t.TempDir() + first, err := Load(context.Background(), dir, data, nil, false, []string{"internal-kb"}) + if err != nil { + t.Fatal(err) + } + if first.Count() != 0 { + t.Fatalf("initial count=%d, want 0", first.Count()) + } + + nested := filepath.Join(dir, "import", "batch-1") + if err := os.MkdirAll(nested, 0o755); err != nil { + t.Fatal(err) + } + doc := `{"id":"AFTER-SNAPSHOT","title":"Imported after snapshot","text":"new knowledge","answer":"x","source":"internal-kb","language":"de-DE","communication_style":"formal"}` + if err := os.WriteFile(filepath.Join(nested, "new.json"), []byte(doc), 0o644); err != nil { + t.Fatal(err) + } + + second, err := Load(context.Background(), dir, data, nil, false, []string{"internal-kb"}) + if err != nil { + t.Fatal(err) + } + if second.Count() != 1 { + t.Fatalf("reconciled count=%d, want 1", second.Count()) + } + if _, ok := second.ByID("AFTER-SNAPSHOT"); !ok { + t.Fatal("file added after snapshot was not reconciled during Initialize") + } +} diff --git a/services/agent/internal/ollama/client.go b/services/agent/internal/ollama/client.go index a477884..c3f37a3 100644 --- a/services/agent/internal/ollama/client.go +++ b/services/agent/internal/ollama/client.go @@ -65,9 +65,65 @@ func (c *Client) NodeStatuses() []model.OllamaNodeStatus { return c.pool.NodeSta func (c *Client) RoutingMode() string { return c.routingMode } func (c *Client) Embed(ctx context.Context, texts []string) ([][]float64, error) { ctx = withStage(ctx, "embedding") + return c.embedAdaptive(ctx, texts, 0) +} + +const maxEmbeddingSplitDepth = 24 + +// embedAdaptive keeps truncate=false so knowledge is never silently discarded. +// Some Ollama embedding models enforce their context window across the complete +// input array, not only per individual string. When such a request is rejected, +// split the batch and retry. If one pathological input still exceeds the model +// context (for example a very long URL/base64-like token), split that text and +// combine the partial embeddings into one length-weighted vector. +func (c *Client) embedAdaptive(ctx context.Context, texts []string, depth int) ([][]float64, error) { if len(texts) == 0 { return nil, nil } + vectors, err := c.embedOnce(ctx, texts) + if err == nil { + return vectors, nil + } + if !isEmbeddingContextLengthError(err) || depth >= maxEmbeddingSplitDepth { + return nil, err + } + + if len(texts) > 1 { + mid := len(texts) / 2 + left, leftErr := c.embedAdaptive(ctx, texts[:mid], depth+1) + if leftErr != nil { + return nil, leftErr + } + right, rightErr := c.embedAdaptive(ctx, texts[mid:], depth+1) + if rightErr != nil { + return nil, rightErr + } + return append(left, right...), nil + } + + leftText, rightText, ok := splitEmbeddingText(texts[0]) + if !ok { + return nil, err + } + left, leftErr := c.embedAdaptive(ctx, []string{leftText}, depth+1) + if leftErr != nil { + return nil, leftErr + } + right, rightErr := c.embedAdaptive(ctx, []string{rightText}, depth+1) + if rightErr != nil { + return nil, rightErr + } + if len(left) != 1 || len(right) != 1 { + return nil, fmt.Errorf("adaptive embedding split returned invalid vector counts: left=%d right=%d", len(left), len(right)) + } + combined, combineErr := weightedEmbeddingAverage(left[0], right[0], len([]rune(leftText)), len([]rune(rightText))) + if combineErr != nil { + return nil, combineErr + } + return [][]float64{combined}, nil +} + +func (c *Client) embedOnce(ctx context.Context, texts []string) ([][]float64, error) { payload := map[string]any{"model": c.embeddingModel, "input": texts, "truncate": false} var out struct { Embeddings [][]float64 `json:"embeddings"` @@ -80,6 +136,49 @@ func (c *Client) Embed(ctx context.Context, texts []string) ([][]float64, error) } return out.Embeddings, nil } + +func isEmbeddingContextLengthError(err error) bool { + if err == nil { + return false + } + msg := strings.ToLower(err.Error()) + return strings.Contains(msg, "context length") || + strings.Contains(msg, "context window") || + (strings.Contains(msg, "context") && strings.Contains(msg, "exceed")) +} + +func splitEmbeddingText(text string) (string, string, bool) { + runes := []rune(strings.TrimSpace(text)) + if len(runes) < 2 { + return "", "", false + } + mid := len(runes) / 2 + left := strings.TrimSpace(string(runes[:mid])) + right := strings.TrimSpace(string(runes[mid:])) + if left == "" || right == "" { + left = string(runes[:mid]) + right = string(runes[mid:]) + } + if left == "" || right == "" || left == text || right == text { + return "", "", false + } + return left, right, true +} + +func weightedEmbeddingAverage(a, b []float64, aWeight, bWeight int) ([]float64, error) { + if len(a) == 0 || len(b) == 0 || len(a) != len(b) { + return nil, fmt.Errorf("cannot combine embedding vectors with dimensions %d and %d", len(a), len(b)) + } + if aWeight <= 0 || bWeight <= 0 { + return nil, fmt.Errorf("cannot combine embedding vectors with non-positive weights %d and %d", aWeight, bWeight) + } + total := float64(aWeight + bWeight) + out := make([]float64, len(a)) + for i := range a { + out[i] = (a[i]*float64(aWeight) + b[i]*float64(bWeight)) / total + } + return out, nil +} func (c *Client) AnalyseCategory(ctx context.Context, t model.Ticket, categories []model.Category, categoryHits []model.KnowledgeHit, contextData model.ContextSnapshot) (model.Decision, error) { ctx = withStage(ctx, "category") categoryIDs := []int64{0} diff --git a/services/agent/internal/ollama/client_test.go b/services/agent/internal/ollama/client_test.go index cca1557..450b54b 100644 --- a/services/agent/internal/ollama/client_test.go +++ b/services/agent/internal/ollama/client_test.go @@ -377,3 +377,106 @@ func TestAnalyseEscalationNormalizesNegativeDecision(t *testing.T) { t.Fatalf("negative escalation was not normalized: %+v", d) } } + +func TestEmbedSplitsBatchWhenOllamaContextWindowRejectsAggregateInput(t *testing.T) { + calls := 0 + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + calls++ + var body struct { + Input []string `json:"input"` + Truncate bool `json:"truncate"` + } + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + t.Error(err) + w.WriteHeader(http.StatusBadRequest) + return + } + if body.Truncate { + t.Errorf("adaptive embedding must preserve truncate=false") + } + if len(body.Input) > 1 { + w.WriteHeader(http.StatusBadRequest) + _ = json.NewEncoder(w).Encode(map[string]any{"error": "estimated request size 5933 tokens exceeds model context length 2048"}) + return + } + value := map[string]float64{"a": 1, "b": 2, "c": 3}[body.Input[0]] + _ = json.NewEncoder(w).Encode(map[string]any{"embeddings": [][]float64{{value, 1}}}) + })) + defer srv.Close() + + c := New(srv.URL, "m", "e", "de-DE", "formal", time.Second, 256, time.Minute, false, 1, 0) + vectors, err := c.Embed(context.Background(), []string{"a", "b", "c"}) + if err != nil { + t.Fatal(err) + } + if len(vectors) != 3 || vectors[0][0] != 1 || vectors[1][0] != 2 || vectors[2][0] != 3 { + t.Fatalf("unexpected vectors: %#v", vectors) + } + if calls != 5 { + t.Fatalf("calls=%d, want 5 adaptive requests", calls) + } +} + +func TestEmbedSplitsOversizedSingleInputWithoutSilentTruncation(t *testing.T) { + calls := 0 + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + calls++ + var body struct { + Input []string `json:"input"` + Truncate bool `json:"truncate"` + } + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + t.Error(err) + w.WriteHeader(http.StatusBadRequest) + return + } + if body.Truncate { + t.Errorf("adaptive embedding must preserve truncate=false") + } + if len(body.Input) != 1 { + t.Errorf("input count=%d, want 1", len(body.Input)) + } + n := len([]rune(body.Input[0])) + if n > 8 { + w.WriteHeader(http.StatusBadRequest) + _ = json.NewEncoder(w).Encode(map[string]any{"error": "input exceeds model context window"}) + return + } + _ = json.NewEncoder(w).Encode(map[string]any{"embeddings": [][]float64{{float64(n), 1}}}) + })) + defer srv.Close() + + c := New(srv.URL, "m", "e", "de-DE", "formal", time.Second, 256, time.Minute, false, 1, 0) + vectors, err := c.Embed(context.Background(), []string{"abcdefghijklmnopqrstuvwxyz123456"}) + if err != nil { + t.Fatal(err) + } + if len(vectors) != 1 || len(vectors[0]) != 2 { + t.Fatalf("unexpected vectors: %#v", vectors) + } + if vectors[0][0] != 8 || vectors[0][1] != 1 { + t.Fatalf("unexpected weighted aggregate vector: %#v", vectors[0]) + } + if calls != 7 { + t.Fatalf("calls=%d, want 7 adaptive requests", calls) + } +} + +func TestEmbedDoesNotSplitUnrelatedBadRequest(t *testing.T) { + calls := 0 + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + calls++ + w.WriteHeader(http.StatusBadRequest) + _ = json.NewEncoder(w).Encode(map[string]any{"error": "model does not support embeddings"}) + })) + defer srv.Close() + + c := New(srv.URL, "m", "e", "de-DE", "formal", time.Second, 256, time.Minute, false, 1, 0) + _, err := c.Embed(context.Background(), []string{"a", "b"}) + if err == nil { + t.Fatal("expected embedding error") + } + if calls != 1 { + t.Fatalf("calls=%d, unrelated error must not trigger adaptive splitting", calls) + } +} diff --git a/services/control/engineering-graph.json b/services/control/engineering-graph.json index 1646b8d..953bc96 100644 --- a/services/control/engineering-graph.json +++ b/services/control/engineering-graph.json @@ -2464,7 +2464,7 @@ "community": "github.com/example/glpi-ai-agent/internal/config", "meta": { "exported": true, - "line": 1065, + "line": 1070, "path": "services/agent/internal/config/config.go" } }, @@ -2476,7 +2476,7 @@ "community": "github.com/example/glpi-ai-agent/internal/config", "meta": { "exported": true, - "line": 442, + "line": 444, "path": "services/agent/internal/config/config.go" } }, @@ -2488,7 +2488,7 @@ "community": "github.com/example/glpi-ai-agent/internal/config", "meta": { "exported": true, - "line": 225, + "line": 226, "path": "services/agent/internal/config/config.go" } }, @@ -2500,7 +2500,7 @@ "community": "github.com/example/glpi-ai-agent/internal/config", "meta": { "exported": false, - "line": 1039, + "line": 1044, "path": "services/agent/internal/config/config.go" } }, @@ -2512,7 +2512,7 @@ "community": "github.com/example/glpi-ai-agent/internal/config", "meta": { "exported": false, - "line": 1210, + "line": 1215, "path": "services/agent/internal/config/config.go" } }, @@ -2524,7 +2524,7 @@ "community": "github.com/example/glpi-ai-agent/internal/config", "meta": { "exported": false, - "line": 1254, + "line": 1259, "path": "services/agent/internal/config/config.go" } }, @@ -2536,7 +2536,7 @@ "community": "github.com/example/glpi-ai-agent/internal/config", "meta": { "exported": false, - "line": 1243, + "line": 1248, "path": "services/agent/internal/config/config.go" } }, @@ -2548,7 +2548,7 @@ "community": "github.com/example/glpi-ai-agent/internal/config", "meta": { "exported": false, - "line": 1221, + "line": 1226, "path": "services/agent/internal/config/config.go" } }, @@ -2560,7 +2560,7 @@ "community": "github.com/example/glpi-ai-agent/internal/config", "meta": { "exported": false, - "line": 1232, + "line": 1237, "path": "services/agent/internal/config/config.go" } }, @@ -2572,7 +2572,7 @@ "community": "github.com/example/glpi-ai-agent/internal/config", "meta": { "exported": false, - "line": 1088, + "line": 1093, "path": "services/agent/internal/config/config.go" } }, @@ -2584,7 +2584,7 @@ "community": "github.com/example/glpi-ai-agent/internal/config", "meta": { "exported": false, - "line": 1110, + "line": 1115, "path": "services/agent/internal/config/config.go" } }, @@ -2596,7 +2596,7 @@ "community": "github.com/example/glpi-ai-agent/internal/config", "meta": { "exported": false, - "line": 1193, + "line": 1198, "path": "services/agent/internal/config/config.go" } }, @@ -2608,7 +2608,7 @@ "community": "github.com/example/glpi-ai-agent/internal/config", "meta": { "exported": false, - "line": 1054, + "line": 1059, "path": "services/agent/internal/config/config.go" } }, @@ -2620,7 +2620,7 @@ "community": "github.com/example/glpi-ai-agent/internal/config", "meta": { "exported": false, - "line": 1185, + "line": 1190, "path": "services/agent/internal/config/config.go" } }, @@ -2632,7 +2632,7 @@ "community": "github.com/example/glpi-ai-agent/internal/config", "meta": { "exported": false, - "line": 1132, + "line": 1137, "path": "services/agent/internal/config/config.go" } }, @@ -2644,7 +2644,7 @@ "community": "github.com/example/glpi-ai-agent/internal/config", "meta": { "exported": false, - "line": 1158, + "line": 1163, "path": "services/agent/internal/config/config.go" } }, @@ -2656,7 +2656,7 @@ "community": "github.com/example/glpi-ai-agent/internal/config", "meta": { "exported": false, - "line": 1048, + "line": 1053, "path": "services/agent/internal/config/config.go" } }, @@ -2668,7 +2668,7 @@ "community": "github.com/example/glpi-ai-agent/internal/config", "meta": { "exported": false, - "line": 1084, + "line": 1089, "path": "services/agent/internal/config/config.go" } }, @@ -2680,7 +2680,7 @@ "community": "github.com/example/glpi-ai-agent/internal/config", "meta": { "exported": false, - "line": 1025, + "line": 1030, "path": "services/agent/internal/config/config.go" } }, @@ -2692,7 +2692,7 @@ "community": "github.com/example/glpi-ai-agent/internal/config", "meta": { "exported": false, - "line": 1020, + "line": 1025, "path": "services/agent/internal/config/config.go" } }, @@ -2704,7 +2704,7 @@ "community": "github.com/example/glpi-ai-agent/internal/config", "meta": { "exported": false, - "line": 995, + "line": 1000, "path": "services/agent/internal/config/config.go" } }, @@ -3412,7 +3412,19 @@ "community": "github.com/example/glpi-ai-agent/internal/glpikb", "meta": { "exported": true, - "line": 72, + "line": 86, + "path": "services/agent/internal/glpikb/sync.go" + } + }, + { + "id": "function:github.com/example/glpi-ai-agent/internal/glpikb:SyncTimeout", + "kind": "function", + "label": "SyncTimeout", + "group": "engineering", + "community": "github.com/example/glpi-ai-agent/internal/glpikb", + "meta": { + "exported": true, + "line": 63, "path": "services/agent/internal/glpikb/sync.go" } }, @@ -3424,7 +3436,7 @@ "community": "github.com/example/glpi-ai-agent/internal/glpikb", "meta": { "exported": true, - "line": 89, + "line": 103, "path": "services/agent/internal/glpikb/sync.go" } }, @@ -3436,7 +3448,7 @@ "community": "github.com/example/glpi-ai-agent/internal/glpikb", "meta": { "exported": true, - "line": 237, + "line": 251, "path": "services/agent/internal/glpikb/sync.go" } }, @@ -3448,7 +3460,7 @@ "community": "github.com/example/glpi-ai-agent/internal/glpikb", "meta": { "exported": true, - "line": 83, + "line": 97, "path": "services/agent/internal/glpikb/sync.go" } }, @@ -3460,7 +3472,7 @@ "community": "github.com/example/glpi-ai-agent/internal/glpikb", "meta": { "exported": true, - "line": 132, + "line": 146, "path": "services/agent/internal/glpikb/sync.go" } }, @@ -3472,7 +3484,7 @@ "community": "github.com/example/glpi-ai-agent/internal/glpikb", "meta": { "exported": false, - "line": 405, + "line": 419, "path": "services/agent/internal/glpikb/sync.go" } }, @@ -3484,7 +3496,7 @@ "community": "github.com/example/glpi-ai-agent/internal/glpikb", "meta": { "exported": false, - "line": 406, + "line": 420, "path": "services/agent/internal/glpikb/sync.go" } }, @@ -3496,7 +3508,7 @@ "community": "github.com/example/glpi-ai-agent/internal/glpikb", "meta": { "exported": false, - "line": 259, + "line": 273, "path": "services/agent/internal/glpikb/sync.go" } }, @@ -3508,7 +3520,7 @@ "community": "github.com/example/glpi-ai-agent/internal/glpikb", "meta": { "exported": false, - "line": 204, + "line": 218, "path": "services/agent/internal/glpikb/sync.go" } }, @@ -3520,7 +3532,7 @@ "community": "github.com/example/glpi-ai-agent/internal/glpikb", "meta": { "exported": false, - "line": 329, + "line": 343, "path": "services/agent/internal/glpikb/sync.go" } }, @@ -3532,7 +3544,7 @@ "community": "github.com/example/glpi-ai-agent/internal/glpikb", "meta": { "exported": false, - "line": 226, + "line": 240, "path": "services/agent/internal/glpikb/sync.go" } }, @@ -3544,7 +3556,7 @@ "community": "github.com/example/glpi-ai-agent/internal/glpikb", "meta": { "exported": false, - "line": 418, + "line": 432, "path": "services/agent/internal/glpikb/sync.go" } }, @@ -3556,7 +3568,7 @@ "community": "github.com/example/glpi-ai-agent/internal/glpikb", "meta": { "exported": false, - "line": 359, + "line": 373, "path": "services/agent/internal/glpikb/sync.go" } }, @@ -3568,7 +3580,7 @@ "community": "github.com/example/glpi-ai-agent/internal/glpikb", "meta": { "exported": false, - "line": 459, + "line": 473, "path": "services/agent/internal/glpikb/sync.go" } }, @@ -3580,7 +3592,7 @@ "community": "github.com/example/glpi-ai-agent/internal/glpikb", "meta": { "exported": false, - "line": 368, + "line": 382, "path": "services/agent/internal/glpikb/sync.go" } }, @@ -3592,7 +3604,7 @@ "community": "github.com/example/glpi-ai-agent/internal/glpikb", "meta": { "exported": false, - "line": 428, + "line": 442, "path": "services/agent/internal/glpikb/sync.go" } }, @@ -3604,7 +3616,7 @@ "community": "github.com/example/glpi-ai-agent/internal/glpikb", "meta": { "exported": false, - "line": 377, + "line": 391, "path": "services/agent/internal/glpikb/sync.go" } }, @@ -3616,7 +3628,7 @@ "community": "github.com/example/glpi-ai-agent/internal/glpikb", "meta": { "exported": false, - "line": 445, + "line": 459, "path": "services/agent/internal/glpikb/sync.go" } }, @@ -3640,7 +3652,7 @@ "community": "github.com/example/glpi-ai-agent/internal/knowledge", "meta": { "exported": true, - "line": 1288, + "line": 1337, "path": "services/agent/internal/knowledge/store.go" } }, @@ -3772,7 +3784,7 @@ "community": "github.com/example/glpi-ai-agent/internal/knowledge", "meta": { "exported": true, - "line": 718, + "line": 767, "path": "services/agent/internal/knowledge/store.go" } }, @@ -3796,7 +3808,7 @@ "community": "github.com/example/glpi-ai-agent/internal/knowledge", "meta": { "exported": true, - "line": 710, + "line": 759, "path": "services/agent/internal/knowledge/store.go" } }, @@ -3808,7 +3820,7 @@ "community": "github.com/example/glpi-ai-agent/internal/knowledge", "meta": { "exported": true, - "line": 858, + "line": 907, "path": "services/agent/internal/knowledge/store.go" } }, @@ -3820,7 +3832,7 @@ "community": "github.com/example/glpi-ai-agent/internal/knowledge", "meta": { "exported": true, - "line": 1070, + "line": 1119, "path": "services/agent/internal/knowledge/store.go" } }, @@ -3832,7 +3844,7 @@ "community": "github.com/example/glpi-ai-agent/internal/knowledge", "meta": { "exported": true, - "line": 397, + "line": 406, "path": "services/agent/internal/knowledge/store.go" } }, @@ -3856,7 +3868,7 @@ "community": "github.com/example/glpi-ai-agent/internal/knowledge", "meta": { "exported": true, - "line": 907, + "line": 956, "path": "services/agent/internal/knowledge/store.go" } }, @@ -3868,7 +3880,7 @@ "community": "github.com/example/glpi-ai-agent/internal/knowledge", "meta": { "exported": true, - "line": 731, + "line": 780, "path": "services/agent/internal/knowledge/store.go" } }, @@ -3880,7 +3892,7 @@ "community": "github.com/example/glpi-ai-agent/internal/knowledge", "meta": { "exported": true, - "line": 699, + "line": 748, "path": "services/agent/internal/knowledge/store.go" } }, @@ -3892,7 +3904,7 @@ "community": "github.com/example/glpi-ai-agent/internal/knowledge", "meta": { "exported": true, - "line": 1094, + "line": 1143, "path": "services/agent/internal/knowledge/store.go" } }, @@ -3904,7 +3916,7 @@ "community": "github.com/example/glpi-ai-agent/internal/knowledge", "meta": { "exported": true, - "line": 915, + "line": 964, "path": "services/agent/internal/knowledge/store.go" } }, @@ -3916,7 +3928,7 @@ "community": "github.com/example/glpi-ai-agent/internal/knowledge", "meta": { "exported": true, - "line": 406, + "line": 415, "path": "services/agent/internal/knowledge/store.go" } }, @@ -3928,7 +3940,7 @@ "community": "github.com/example/glpi-ai-agent/internal/knowledge", "meta": { "exported": true, - "line": 936, + "line": 985, "path": "services/agent/internal/knowledge/store.go" } }, @@ -3940,7 +3952,7 @@ "community": "github.com/example/glpi-ai-agent/internal/knowledge", "meta": { "exported": true, - "line": 1316, + "line": 1365, "path": "services/agent/internal/knowledge/store.go" } }, @@ -3964,7 +3976,7 @@ "community": "github.com/example/glpi-ai-agent/internal/knowledge", "meta": { "exported": true, - "line": 1116, + "line": 1165, "path": "services/agent/internal/knowledge/store.go" } }, @@ -3988,7 +4000,7 @@ "community": "github.com/example/glpi-ai-agent/internal/knowledge", "meta": { "exported": true, - "line": 250, + "line": 255, "path": "services/agent/internal/knowledge/persistent_index.go" } }, @@ -4000,7 +4012,7 @@ "community": "github.com/example/glpi-ai-agent/internal/knowledge", "meta": { "exported": true, - "line": 283, + "line": 288, "path": "services/agent/internal/knowledge/persistent_index.go" } }, @@ -4012,7 +4024,7 @@ "community": "github.com/example/glpi-ai-agent/internal/knowledge", "meta": { "exported": true, - "line": 742, + "line": 791, "path": "services/agent/internal/knowledge/store.go" } }, @@ -4036,7 +4048,7 @@ "community": "github.com/example/glpi-ai-agent/internal/knowledge", "meta": { "exported": false, - "line": 1444, + "line": 1493, "path": "services/agent/internal/knowledge/store.go" } }, @@ -4048,7 +4060,7 @@ "community": "github.com/example/glpi-ai-agent/internal/knowledge", "meta": { "exported": false, - "line": 1487, + "line": 1536, "path": "services/agent/internal/knowledge/store.go" } }, @@ -4072,7 +4084,7 @@ "community": "github.com/example/glpi-ai-agent/internal/knowledge", "meta": { "exported": false, - "line": 248, + "line": 257, "path": "services/agent/internal/knowledge/store.go" } }, @@ -4096,7 +4108,7 @@ "community": "github.com/example/glpi-ai-agent/internal/knowledge", "meta": { "exported": false, - "line": 1352, + "line": 1401, "path": "services/agent/internal/knowledge/store.go" } }, @@ -4132,7 +4144,7 @@ "community": "github.com/example/glpi-ai-agent/internal/knowledge", "meta": { "exported": false, - "line": 654, + "line": 657, "path": "services/agent/internal/knowledge/persistent_index.go" } }, @@ -4144,7 +4156,7 @@ "community": "github.com/example/glpi-ai-agent/internal/knowledge", "meta": { "exported": false, - "line": 172, + "line": 177, "path": "services/agent/internal/knowledge/persistent_index.go" } }, @@ -4156,7 +4168,7 @@ "community": "github.com/example/glpi-ai-agent/internal/knowledge", "meta": { "exported": false, - "line": 1064, + "line": 1113, "path": "services/agent/internal/knowledge/store.go" } }, @@ -4168,7 +4180,7 @@ "community": "github.com/example/glpi-ai-agent/internal/knowledge", "meta": { "exported": false, - "line": 512, + "line": 517, "path": "services/agent/internal/knowledge/persistent_index.go" } }, @@ -4204,7 +4216,7 @@ "community": "github.com/example/glpi-ai-agent/internal/knowledge", "meta": { "exported": false, - "line": 391, + "line": 400, "path": "services/agent/internal/knowledge/store.go" } }, @@ -4228,7 +4240,7 @@ "community": "github.com/example/glpi-ai-agent/internal/knowledge", "meta": { "exported": false, - "line": 272, + "line": 277, "path": "services/agent/internal/knowledge/persistent_index.go" } }, @@ -4264,7 +4276,7 @@ "community": "github.com/example/glpi-ai-agent/internal/knowledge", "meta": { "exported": false, - "line": 682, + "line": 731, "path": "services/agent/internal/knowledge/store.go" } }, @@ -4276,7 +4288,7 @@ "community": "github.com/example/glpi-ai-agent/internal/knowledge", "meta": { "exported": false, - "line": 690, + "line": 693, "path": "services/agent/internal/knowledge/persistent_index.go" } }, @@ -4288,7 +4300,7 @@ "community": "github.com/example/glpi-ai-agent/internal/knowledge", "meta": { "exported": false, - "line": 632, + "line": 631, "path": "services/agent/internal/knowledge/persistent_index.go" } }, @@ -4300,7 +4312,7 @@ "community": "github.com/example/glpi-ai-agent/internal/knowledge", "meta": { "exported": false, - "line": 1660, + "line": 1709, "path": "services/agent/internal/knowledge/store.go" } }, @@ -4312,7 +4324,7 @@ "community": "github.com/example/glpi-ai-agent/internal/knowledge", "meta": { "exported": false, - "line": 1568, + "line": 1617, "path": "services/agent/internal/knowledge/store.go" } }, @@ -4324,7 +4336,7 @@ "community": "github.com/example/glpi-ai-agent/internal/knowledge", "meta": { "exported": false, - "line": 1833, + "line": 1882, "path": "services/agent/internal/knowledge/store.go" } }, @@ -4348,7 +4360,7 @@ "community": "github.com/example/glpi-ai-agent/internal/knowledge", "meta": { "exported": false, - "line": 1849, + "line": 1898, "path": "services/agent/internal/knowledge/store.go" } }, @@ -4360,7 +4372,7 @@ "community": "github.com/example/glpi-ai-agent/internal/knowledge", "meta": { "exported": false, - "line": 1856, + "line": 1905, "path": "services/agent/internal/knowledge/store.go" } }, @@ -4372,7 +4384,7 @@ "community": "github.com/example/glpi-ai-agent/internal/knowledge", "meta": { "exported": false, - "line": 1863, + "line": 1912, "path": "services/agent/internal/knowledge/store.go" } }, @@ -4384,7 +4396,7 @@ "community": "github.com/example/glpi-ai-agent/internal/knowledge", "meta": { "exported": false, - "line": 1842, + "line": 1891, "path": "services/agent/internal/knowledge/store.go" } }, @@ -4408,7 +4420,7 @@ "community": "github.com/example/glpi-ai-agent/internal/knowledge", "meta": { "exported": false, - "line": 1896, + "line": 1945, "path": "services/agent/internal/knowledge/store.go" } }, @@ -4420,7 +4432,7 @@ "community": "github.com/example/glpi-ai-agent/internal/knowledge", "meta": { "exported": false, - "line": 618, + "line": 617, "path": "services/agent/internal/knowledge/persistent_index.go" } }, @@ -4432,7 +4444,7 @@ "community": "github.com/example/glpi-ai-agent/internal/knowledge", "meta": { "exported": false, - "line": 485, + "line": 521, "path": "services/agent/internal/knowledge/store.go" } }, @@ -4444,7 +4456,7 @@ "community": "github.com/example/glpi-ai-agent/internal/knowledge", "meta": { "exported": false, - "line": 1825, + "line": 1874, "path": "services/agent/internal/knowledge/store.go" } }, @@ -4456,7 +4468,7 @@ "community": "github.com/example/glpi-ai-agent/internal/knowledge", "meta": { "exported": false, - "line": 1544, + "line": 1593, "path": "services/agent/internal/knowledge/store.go" } }, @@ -4468,7 +4480,7 @@ "community": "github.com/example/glpi-ai-agent/internal/knowledge", "meta": { "exported": false, - "line": 1532, + "line": 1581, "path": "services/agent/internal/knowledge/store.go" } }, @@ -4480,7 +4492,7 @@ "community": "github.com/example/glpi-ai-agent/internal/knowledge", "meta": { "exported": false, - "line": 1877, + "line": 1926, "path": "services/agent/internal/knowledge/store.go" } }, @@ -4492,7 +4504,7 @@ "community": "github.com/example/glpi-ai-agent/internal/knowledge", "meta": { "exported": false, - "line": 1797, + "line": 1846, "path": "services/agent/internal/knowledge/store.go" } }, @@ -4504,7 +4516,7 @@ "community": "github.com/example/glpi-ai-agent/internal/knowledge", "meta": { "exported": false, - "line": 1645, + "line": 1694, "path": "services/agent/internal/knowledge/store.go" } }, @@ -4516,7 +4528,7 @@ "community": "github.com/example/glpi-ai-agent/internal/knowledge", "meta": { "exported": false, - "line": 1911, + "line": 1960, "path": "services/agent/internal/knowledge/store.go" } }, @@ -4528,7 +4540,19 @@ "community": "github.com/example/glpi-ai-agent/internal/knowledge", "meta": { "exported": false, - "line": 1634, + "line": 1683, + "path": "services/agent/internal/knowledge/store.go" + } + }, + { + "id": "function:github.com/example/glpi-ai-agent/internal/knowledge:listKnowledgeJSONFiles", + "kind": "function", + "label": "listKnowledgeJSONFiles", + "group": "engineering", + "community": "github.com/example/glpi-ai-agent/internal/knowledge", + "meta": { + "exported": false, + "line": 426, "path": "services/agent/internal/knowledge/store.go" } }, @@ -4540,7 +4564,7 @@ "community": "github.com/example/glpi-ai-agent/internal/knowledge", "meta": { "exported": false, - "line": 1512, + "line": 1561, "path": "services/agent/internal/knowledge/store.go" } }, @@ -4552,7 +4576,7 @@ "community": "github.com/example/glpi-ai-agent/internal/knowledge", "meta": { "exported": false, - "line": 589, + "line": 625, "path": "services/agent/internal/knowledge/store.go" } }, @@ -4564,7 +4588,7 @@ "community": "github.com/example/glpi-ai-agent/internal/knowledge", "meta": { "exported": false, - "line": 656, + "line": 692, "path": "services/agent/internal/knowledge/store.go" } }, @@ -4576,7 +4600,7 @@ "community": "github.com/example/glpi-ai-agent/internal/knowledge", "meta": { "exported": false, - "line": 628, + "line": 627, "path": "services/agent/internal/knowledge/persistent_index.go" } }, @@ -4588,7 +4612,7 @@ "community": "github.com/example/glpi-ai-agent/internal/knowledge", "meta": { "exported": false, - "line": 690, + "line": 739, "path": "services/agent/internal/knowledge/store.go" } }, @@ -4600,7 +4624,7 @@ "community": "github.com/example/glpi-ai-agent/internal/knowledge", "meta": { "exported": false, - "line": 1870, + "line": 1919, "path": "services/agent/internal/knowledge/store.go" } }, @@ -4612,7 +4636,7 @@ "community": "github.com/example/glpi-ai-agent/internal/knowledge", "meta": { "exported": false, - "line": 664, + "line": 713, "path": "services/agent/internal/knowledge/store.go" } }, @@ -4636,7 +4660,7 @@ "community": "github.com/example/glpi-ai-agent/internal/knowledge", "meta": { "exported": false, - "line": 1781, + "line": 1830, "path": "services/agent/internal/knowledge/store.go" } }, @@ -4648,7 +4672,7 @@ "community": "github.com/example/glpi-ai-agent/internal/knowledge", "meta": { "exported": false, - "line": 553, + "line": 589, "path": "services/agent/internal/knowledge/store.go" } }, @@ -4660,7 +4684,7 @@ "community": "github.com/example/glpi-ai-agent/internal/knowledge", "meta": { "exported": false, - "line": 522, + "line": 558, "path": "services/agent/internal/knowledge/store.go" } }, @@ -4672,7 +4696,7 @@ "community": "github.com/example/glpi-ai-agent/internal/knowledge", "meta": { "exported": false, - "line": 626, + "line": 662, "path": "services/agent/internal/knowledge/store.go" } }, @@ -4684,7 +4708,7 @@ "community": "github.com/example/glpi-ai-agent/internal/knowledge", "meta": { "exported": false, - "line": 1687, + "line": 1736, "path": "services/agent/internal/knowledge/store.go" } }, @@ -4708,7 +4732,7 @@ "community": "github.com/example/glpi-ai-agent/internal/knowledge", "meta": { "exported": false, - "line": 408, + "line": 453, "path": "services/agent/internal/knowledge/store.go" } }, @@ -4720,7 +4744,7 @@ "community": "github.com/example/glpi-ai-agent/internal/knowledge", "meta": { "exported": false, - "line": 1100, + "line": 1149, "path": "services/agent/internal/knowledge/store.go" } }, @@ -4732,7 +4756,7 @@ "community": "github.com/example/glpi-ai-agent/internal/knowledge", "meta": { "exported": false, - "line": 1555, + "line": 1604, "path": "services/agent/internal/knowledge/store.go" } }, @@ -4744,7 +4768,7 @@ "community": "github.com/example/glpi-ai-agent/internal/knowledge", "meta": { "exported": false, - "line": 1768, + "line": 1817, "path": "services/agent/internal/knowledge/store.go" } }, @@ -4756,7 +4780,7 @@ "community": "github.com/example/glpi-ai-agent/internal/knowledge", "meta": { "exported": false, - "line": 1617, + "line": 1666, "path": "services/agent/internal/knowledge/store.go" } }, @@ -4768,7 +4792,7 @@ "community": "github.com/example/glpi-ai-agent/internal/knowledge", "meta": { "exported": false, - "line": 1699, + "line": 1748, "path": "services/agent/internal/knowledge/store.go" } }, @@ -4780,7 +4804,7 @@ "community": "github.com/example/glpi-ai-agent/internal/knowledge", "meta": { "exported": false, - "line": 1806, + "line": 1855, "path": "services/agent/internal/knowledge/store.go" } }, @@ -4792,7 +4816,7 @@ "community": "github.com/example/glpi-ai-agent/internal/knowledge", "meta": { "exported": false, - "line": 1785, + "line": 1834, "path": "services/agent/internal/knowledge/store.go" } }, @@ -4804,7 +4828,7 @@ "community": "github.com/example/glpi-ai-agent/internal/knowledge", "meta": { "exported": false, - "line": 1716, + "line": 1765, "path": "services/agent/internal/knowledge/store.go" } }, @@ -4816,7 +4840,7 @@ "community": "github.com/example/glpi-ai-agent/internal/knowledge", "meta": { "exported": false, - "line": 1925, + "line": 1974, "path": "services/agent/internal/knowledge/store.go" } }, @@ -4828,7 +4852,7 @@ "community": "github.com/example/glpi-ai-agent/internal/knowledge", "meta": { "exported": false, - "line": 667, + "line": 716, "path": "services/agent/internal/knowledge/store.go" } }, @@ -4852,7 +4876,7 @@ "community": "github.com/example/glpi-ai-agent/internal/knowledge", "meta": { "exported": false, - "line": 1602, + "line": 1651, "path": "services/agent/internal/knowledge/store.go" } }, @@ -5548,7 +5572,7 @@ "community": "github.com/example/glpi-ai-agent/internal/ollama", "meta": { "exported": true, - "line": 280, + "line": 379, "path": "services/agent/internal/ollama/client.go" } }, @@ -5560,7 +5584,7 @@ "community": "github.com/example/glpi-ai-agent/internal/ollama", "meta": { "exported": true, - "line": 83, + "line": 182, "path": "services/agent/internal/ollama/client.go" } }, @@ -5572,7 +5596,7 @@ "community": "github.com/example/glpi-ai-agent/internal/ollama", "meta": { "exported": true, - "line": 420, + "line": 519, "path": "services/agent/internal/ollama/client.go" } }, @@ -5584,7 +5608,7 @@ "community": "github.com/example/glpi-ai-agent/internal/ollama", "meta": { "exported": true, - "line": 376, + "line": 475, "path": "services/agent/internal/ollama/client.go" } }, @@ -5596,7 +5620,7 @@ "community": "github.com/example/glpi-ai-agent/internal/ollama", "meta": { "exported": true, - "line": 192, + "line": 291, "path": "services/agent/internal/ollama/client.go" } }, @@ -5608,7 +5632,7 @@ "community": "github.com/example/glpi-ai-agent/internal/ollama", "meta": { "exported": true, - "line": 125, + "line": 224, "path": "services/agent/internal/ollama/client.go" } }, @@ -5672,6 +5696,30 @@ "path": "services/agent/internal/ollama/client.go" } }, + { + "id": "function:github.com/example/glpi-ai-agent/internal/ollama:Client.embedAdaptive", + "kind": "function", + "label": "Client.embedAdaptive", + "group": "engineering", + "community": "github.com/example/glpi-ai-agent/internal/ollama", + "meta": { + "exported": false, + "line": 79, + "path": "services/agent/internal/ollama/client.go" + } + }, + { + "id": "function:github.com/example/glpi-ai-agent/internal/ollama:Client.embedOnce", + "kind": "function", + "label": "Client.embedOnce", + "group": "engineering", + "community": "github.com/example/glpi-ai-agent/internal/ollama", + "meta": { + "exported": false, + "line": 126, + "path": "services/agent/internal/ollama/client.go" + } + }, { "id": "function:github.com/example/glpi-ai-agent/internal/ollama:Client.executeDecision", "kind": "function", @@ -5680,7 +5728,7 @@ "community": "github.com/example/glpi-ai-agent/internal/ollama", "meta": { "exported": false, - "line": 250, + "line": 349, "path": "services/agent/internal/ollama/client.go" } }, @@ -5692,7 +5740,7 @@ "community": "github.com/example/glpi-ai-agent/internal/ollama", "meta": { "exported": false, - "line": 528, + "line": 627, "path": "services/agent/internal/ollama/client.go" } }, @@ -5704,7 +5752,7 @@ "community": "github.com/example/glpi-ai-agent/internal/ollama", "meta": { "exported": false, - "line": 372, + "line": 471, "path": "services/agent/internal/ollama/client.go" } }, @@ -5908,7 +5956,7 @@ "community": "github.com/example/glpi-ai-agent/internal/ollama", "meta": { "exported": false, - "line": 483, + "line": 582, "path": "services/agent/internal/ollama/client.go" } }, @@ -5924,6 +5972,18 @@ "path": "services/agent/internal/ollama/pool.go" } }, + { + "id": "function:github.com/example/glpi-ai-agent/internal/ollama:isEmbeddingContextLengthError", + "kind": "function", + "label": "isEmbeddingContextLengthError", + "group": "engineering", + "community": "github.com/example/glpi-ai-agent/internal/ollama", + "meta": { + "exported": false, + "line": 140, + "path": "services/agent/internal/ollama/client.go" + } + }, { "id": "function:github.com/example/glpi-ai-agent/internal/ollama:isRetryable", "kind": "function", @@ -5992,7 +6052,7 @@ "community": "github.com/example/glpi-ai-agent/internal/ollama", "meta": { "exported": false, - "line": 493, + "line": 592, "path": "services/agent/internal/ollama/client.go" } }, @@ -6004,7 +6064,7 @@ "community": "github.com/example/glpi-ai-agent/internal/ollama", "meta": { "exported": false, - "line": 517, + "line": 616, "path": "services/agent/internal/ollama/client.go" } }, @@ -6128,6 +6188,18 @@ "path": "services/agent/internal/ollama/pool.go" } }, + { + "id": "function:github.com/example/glpi-ai-agent/internal/ollama:splitEmbeddingText", + "kind": "function", + "label": "splitEmbeddingText", + "group": "engineering", + "community": "github.com/example/glpi-ai-agent/internal/ollama", + "meta": { + "exported": false, + "line": 150, + "path": "services/agent/internal/ollama/client.go" + } + }, { "id": "function:github.com/example/glpi-ai-agent/internal/ollama:uniqueStrings", "kind": "function", @@ -6136,7 +6208,19 @@ "community": "github.com/example/glpi-ai-agent/internal/ollama", "meta": { "exported": false, - "line": 466, + "line": 565, + "path": "services/agent/internal/ollama/client.go" + } + }, + { + "id": "function:github.com/example/glpi-ai-agent/internal/ollama:weightedEmbeddingAverage", + "kind": "function", + "label": "weightedEmbeddingAverage", + "group": "engineering", + "community": "github.com/example/glpi-ai-agent/internal/ollama", + "meta": { + "exported": false, + "line": 168, "path": "services/agent/internal/ollama/client.go" } }, @@ -30903,6 +30987,12 @@ "to": "function:github.com/example/glpi-ai-agent/internal/glpikb:New", "kind": "defines" }, + { + "id": "file:services/agent/internal/glpikb/sync.go-\u003efunction:github.com/example/glpi-ai-agent/internal/glpikb:SyncTimeout:defines", + "from": "file:services/agent/internal/glpikb/sync.go", + "to": "function:github.com/example/glpi-ai-agent/internal/glpikb:SyncTimeout", + "kind": "defines" + }, { "id": "file:services/agent/internal/glpikb/sync.go-\u003efunction:github.com/example/glpi-ai-agent/internal/glpikb:Syncer.LoadCache:defines", "from": "file:services/agent/internal/glpikb/sync.go", @@ -31809,6 +31899,12 @@ "to": "function:github.com/example/glpi-ai-agent/internal/knowledge:lexicalSimilarity", "kind": "defines" }, + { + "id": "file:services/agent/internal/knowledge/store.go-\u003efunction:github.com/example/glpi-ai-agent/internal/knowledge:listKnowledgeJSONFiles:defines", + "from": "file:services/agent/internal/knowledge/store.go", + "to": "function:github.com/example/glpi-ai-agent/internal/knowledge:listKnowledgeJSONFiles", + "kind": "defines" + }, { "id": "file:services/agent/internal/knowledge/store.go-\u003efunction:github.com/example/glpi-ai-agent/internal/knowledge:loadCache:defines", "from": "file:services/agent/internal/knowledge/store.go", @@ -32739,6 +32835,18 @@ "to": "function:github.com/example/glpi-ai-agent/internal/ollama:Client.Start", "kind": "defines" }, + { + "id": "file:services/agent/internal/ollama/client.go-\u003efunction:github.com/example/glpi-ai-agent/internal/ollama:Client.embedAdaptive:defines", + "from": "file:services/agent/internal/ollama/client.go", + "to": "function:github.com/example/glpi-ai-agent/internal/ollama:Client.embedAdaptive", + "kind": "defines" + }, + { + "id": "file:services/agent/internal/ollama/client.go-\u003efunction:github.com/example/glpi-ai-agent/internal/ollama:Client.embedOnce:defines", + "from": "file:services/agent/internal/ollama/client.go", + "to": "function:github.com/example/glpi-ai-agent/internal/ollama:Client.embedOnce", + "kind": "defines" + }, { "id": "file:services/agent/internal/ollama/client.go-\u003efunction:github.com/example/glpi-ai-agent/internal/ollama:Client.executeDecision:defines", "from": "file:services/agent/internal/ollama/client.go", @@ -32775,6 +32883,12 @@ "to": "function:github.com/example/glpi-ai-agent/internal/ollama:containsString", "kind": "defines" }, + { + "id": "file:services/agent/internal/ollama/client.go-\u003efunction:github.com/example/glpi-ai-agent/internal/ollama:isEmbeddingContextLengthError:defines", + "from": "file:services/agent/internal/ollama/client.go", + "to": "function:github.com/example/glpi-ai-agent/internal/ollama:isEmbeddingContextLengthError", + "kind": "defines" + }, { "id": "file:services/agent/internal/ollama/client.go-\u003efunction:github.com/example/glpi-ai-agent/internal/ollama:normalizeAllowedActions:defines", "from": "file:services/agent/internal/ollama/client.go", @@ -32787,12 +32901,24 @@ "to": "function:github.com/example/glpi-ai-agent/internal/ollama:normalizeEscalationModelActions", "kind": "defines" }, + { + "id": "file:services/agent/internal/ollama/client.go-\u003efunction:github.com/example/glpi-ai-agent/internal/ollama:splitEmbeddingText:defines", + "from": "file:services/agent/internal/ollama/client.go", + "to": "function:github.com/example/glpi-ai-agent/internal/ollama:splitEmbeddingText", + "kind": "defines" + }, { "id": "file:services/agent/internal/ollama/client.go-\u003efunction:github.com/example/glpi-ai-agent/internal/ollama:uniqueStrings:defines", "from": "file:services/agent/internal/ollama/client.go", "to": "function:github.com/example/glpi-ai-agent/internal/ollama:uniqueStrings", "kind": "defines" }, + { + "id": "file:services/agent/internal/ollama/client.go-\u003efunction:github.com/example/glpi-ai-agent/internal/ollama:weightedEmbeddingAverage:defines", + "from": "file:services/agent/internal/ollama/client.go", + "to": "function:github.com/example/glpi-ai-agent/internal/ollama:weightedEmbeddingAverage", + "kind": "defines" + }, { "id": "file:services/agent/internal/ollama/client.go-\u003epackage:context:imports", "from": "file:services/agent/internal/ollama/client.go", @@ -36075,12 +36201,6 @@ "to": "package:time", "kind": "imports" }, - { - "id": "function:github.com/example/glpi-ai-agent/cmd/agent:main-\u003efunction:github.com/example/glpi-ai-agent/cmd/agent:maxDuration:calls", - "from": "function:github.com/example/glpi-ai-agent/cmd/agent:main", - "to": "function:github.com/example/glpi-ai-agent/cmd/agent:maxDuration", - "kind": "calls" - }, { "id": "function:github.com/example/glpi-ai-agent/cmd/agent:main-\u003efunction:github.com/example/glpi-ai-agent/cmd/agent:runHealthcheck:calls", "from": "function:github.com/example/glpi-ai-agent/cmd/agent:main", @@ -39990,15 +40110,15 @@ "label": "ReadFile" }, { - "id": "function:github.com/example/glpi-ai-agent/internal/glpikb:Syncer.Start-\u003efunction:github.com/example/glpi-ai-agent/internal/glpikb:Syncer.Sync:calls", + "id": "function:github.com/example/glpi-ai-agent/internal/glpikb:Syncer.Start-\u003efunction:github.com/example/glpi-ai-agent/internal/glpikb:SyncTimeout:calls", "from": "function:github.com/example/glpi-ai-agent/internal/glpikb:Syncer.Start", - "to": "function:github.com/example/glpi-ai-agent/internal/glpikb:Syncer.Sync", + "to": "function:github.com/example/glpi-ai-agent/internal/glpikb:SyncTimeout", "kind": "calls" }, { - "id": "function:github.com/example/glpi-ai-agent/internal/glpikb:Syncer.Start-\u003efunction:github.com/example/glpi-ai-agent/internal/glpikb:maxDuration:calls", + "id": "function:github.com/example/glpi-ai-agent/internal/glpikb:Syncer.Start-\u003efunction:github.com/example/glpi-ai-agent/internal/glpikb:Syncer.Sync:calls", "from": "function:github.com/example/glpi-ai-agent/internal/glpikb:Syncer.Start", - "to": "function:github.com/example/glpi-ai-agent/internal/glpikb:maxDuration", + "to": "function:github.com/example/glpi-ai-agent/internal/glpikb:Syncer.Sync", "kind": "calls" }, { @@ -40706,6 +40826,12 @@ "kind": "calls_package", "label": "ToLower" }, + { + "id": "function:github.com/example/glpi-ai-agent/internal/knowledge:Store.Initialize-\u003efunction:github.com/example/glpi-ai-agent/internal/knowledge:Store.SyncLocal:calls", + "from": "function:github.com/example/glpi-ai-agent/internal/knowledge:Store.Initialize", + "to": "function:github.com/example/glpi-ai-agent/internal/knowledge:Store.SyncLocal", + "kind": "calls" + }, { "id": "function:github.com/example/glpi-ai-agent/internal/knowledge:Store.Initialize-\u003efunction:github.com/example/glpi-ai-agent/internal/knowledge:Store.fullRebuild:calls", "from": "function:github.com/example/glpi-ai-agent/internal/knowledge:Store.Initialize", @@ -41455,14 +41581,14 @@ "from": "function:github.com/example/glpi-ai-agent/internal/knowledge:Store.loadPersistentSnapshot", "to": "package:path/filepath", "kind": "calls_package", - "label": "Base" + "label": "Clean" }, { "id": "function:github.com/example/glpi-ai-agent/internal/knowledge:Store.loadPersistentSnapshot-\u003epackage:strings:calls_package", "from": "function:github.com/example/glpi-ai-agent/internal/knowledge:Store.loadPersistentSnapshot", "to": "package:strings", "kind": "calls_package", - "label": "HasPrefix" + "label": "TrimPrefix" }, { "id": "function:github.com/example/glpi-ai-agent/internal/knowledge:Store.loadPersistentSnapshot-\u003epackage:time:calls_package", @@ -41550,12 +41676,6 @@ "to": "function:github.com/example/glpi-ai-agent/internal/knowledge:Store.persistSnapshot", "kind": "calls" }, - { - "id": "function:github.com/example/glpi-ai-agent/internal/knowledge:Store.scanDeltaDir-\u003efunction:github.com/example/glpi-ai-agent/internal/knowledge:NeuroForgeBackend.Name:calls", - "from": "function:github.com/example/glpi-ai-agent/internal/knowledge:Store.scanDeltaDir", - "to": "function:github.com/example/glpi-ai-agent/internal/knowledge:NeuroForgeBackend.Name", - "kind": "calls" - }, { "id": "function:github.com/example/glpi-ai-agent/internal/knowledge:Store.scanDeltaDir-\u003efunction:github.com/example/glpi-ai-agent/internal/knowledge:appendUniqueString:calls", "from": "function:github.com/example/glpi-ai-agent/internal/knowledge:Store.scanDeltaDir", @@ -41568,6 +41688,12 @@ "to": "function:github.com/example/glpi-ai-agent/internal/knowledge:decodeKnowledgeDoc", "kind": "calls" }, + { + "id": "function:github.com/example/glpi-ai-agent/internal/knowledge:Store.scanDeltaDir-\u003efunction:github.com/example/glpi-ai-agent/internal/knowledge:listKnowledgeJSONFiles:calls", + "from": "function:github.com/example/glpi-ai-agent/internal/knowledge:Store.scanDeltaDir", + "to": "function:github.com/example/glpi-ai-agent/internal/knowledge:listKnowledgeJSONFiles", + "kind": "calls" + }, { "id": "function:github.com/example/glpi-ai-agent/internal/knowledge:Store.scanDeltaDir-\u003efunction:github.com/example/glpi-ai-agent/internal/knowledge:matchesAnyGlob:calls", "from": "function:github.com/example/glpi-ai-agent/internal/knowledge:Store.scanDeltaDir", @@ -41612,21 +41738,14 @@ "from": "function:github.com/example/glpi-ai-agent/internal/knowledge:Store.scanDeltaDir", "to": "package:os", "kind": "calls_package", - "label": "ReadDir" - }, - { - "id": "function:github.com/example/glpi-ai-agent/internal/knowledge:Store.scanDeltaDir-\u003epackage:path/filepath:calls_package", - "from": "function:github.com/example/glpi-ai-agent/internal/knowledge:Store.scanDeltaDir", - "to": "package:path/filepath", - "kind": "calls_package", - "label": "Join" + "label": "ReadFile" }, { "id": "function:github.com/example/glpi-ai-agent/internal/knowledge:Store.scanDeltaDir-\u003epackage:strings:calls_package", "from": "function:github.com/example/glpi-ai-agent/internal/knowledge:Store.scanDeltaDir", "to": "package:strings", "kind": "calls_package", - "label": "HasSuffix" + "label": "ToLower" }, { "id": "function:github.com/example/glpi-ai-agent/internal/knowledge:Store.syncLoadedSemanticBackend-\u003efunction:github.com/example/glpi-ai-agent/internal/knowledge:NeuroForgeBackend.Health:calls", @@ -41749,9 +41868,9 @@ "label": "EqualFold" }, { - "id": "function:github.com/example/glpi-ai-agent/internal/knowledge:augmentManifestAllFiles-\u003efunction:github.com/example/glpi-ai-agent/internal/knowledge:NeuroForgeBackend.Name:calls", + "id": "function:github.com/example/glpi-ai-agent/internal/knowledge:augmentManifestAllFiles-\u003efunction:github.com/example/glpi-ai-agent/internal/knowledge:listKnowledgeJSONFiles:calls", "from": "function:github.com/example/glpi-ai-agent/internal/knowledge:augmentManifestAllFiles", - "to": "function:github.com/example/glpi-ai-agent/internal/knowledge:NeuroForgeBackend.Name", + "to": "function:github.com/example/glpi-ai-agent/internal/knowledge:listKnowledgeJSONFiles", "kind": "calls" }, { @@ -41779,21 +41898,7 @@ "from": "function:github.com/example/glpi-ai-agent/internal/knowledge:augmentManifestAllFiles", "to": "package:os", "kind": "calls_package", - "label": "ReadDir" - }, - { - "id": "function:github.com/example/glpi-ai-agent/internal/knowledge:augmentManifestAllFiles-\u003epackage:path/filepath:calls_package", - "from": "function:github.com/example/glpi-ai-agent/internal/knowledge:augmentManifestAllFiles", - "to": "package:path/filepath", - "kind": "calls_package", - "label": "Join" - }, - { - "id": "function:github.com/example/glpi-ai-agent/internal/knowledge:augmentManifestAllFiles-\u003epackage:strings:calls_package", - "from": "function:github.com/example/glpi-ai-agent/internal/knowledge:augmentManifestAllFiles", - "to": "package:strings", - "kind": "calls_package", - "label": "HasSuffix" + "label": "ReadFile" }, { "id": "function:github.com/example/glpi-ai-agent/internal/knowledge:buildManifestForDocs-\u003epackage:crypto/sha256:calls_package", @@ -41828,7 +41933,7 @@ "from": "function:github.com/example/glpi-ai-agent/internal/knowledge:buildManifestForDocs", "to": "package:path/filepath", "kind": "calls_package", - "label": "Base" + "label": "Rel" }, { "id": "function:github.com/example/glpi-ai-agent/internal/knowledge:categorySimilarity-\u003efunction:github.com/example/glpi-ai-agent/internal/knowledge:clamp01:calls", @@ -42055,6 +42160,33 @@ "kind": "calls_package", "label": "TrimSpace" }, + { + "id": "function:github.com/example/glpi-ai-agent/internal/knowledge:listKnowledgeJSONFiles-\u003efunction:github.com/example/glpi-ai-agent/internal/knowledge:NeuroForgeBackend.Name:calls", + "from": "function:github.com/example/glpi-ai-agent/internal/knowledge:listKnowledgeJSONFiles", + "to": "function:github.com/example/glpi-ai-agent/internal/knowledge:NeuroForgeBackend.Name", + "kind": "calls" + }, + { + "id": "function:github.com/example/glpi-ai-agent/internal/knowledge:listKnowledgeJSONFiles-\u003epackage:path/filepath:calls_package", + "from": "function:github.com/example/glpi-ai-agent/internal/knowledge:listKnowledgeJSONFiles", + "to": "package:path/filepath", + "kind": "calls_package", + "label": "WalkDir" + }, + { + "id": "function:github.com/example/glpi-ai-agent/internal/knowledge:listKnowledgeJSONFiles-\u003epackage:sort:calls_package", + "from": "function:github.com/example/glpi-ai-agent/internal/knowledge:listKnowledgeJSONFiles", + "to": "package:sort", + "kind": "calls_package", + "label": "Slice" + }, + { + "id": "function:github.com/example/glpi-ai-agent/internal/knowledge:listKnowledgeJSONFiles-\u003epackage:strings:calls_package", + "from": "function:github.com/example/glpi-ai-agent/internal/knowledge:listKnowledgeJSONFiles", + "to": "package:strings", + "kind": "calls_package", + "label": "HasSuffix" + }, { "id": "function:github.com/example/glpi-ai-agent/internal/knowledge:loadCache-\u003epackage:encoding/json:calls_package", "from": "function:github.com/example/glpi-ai-agent/internal/knowledge:loadCache", @@ -42120,7 +42252,14 @@ "from": "function:github.com/example/glpi-ai-agent/internal/knowledge:matchesAnyGlob", "to": "package:path/filepath", "kind": "calls_package", - "label": "Match" + "label": "ToSlash" + }, + { + "id": "function:github.com/example/glpi-ai-agent/internal/knowledge:matchesAnyGlob-\u003epackage:strings:calls_package", + "from": "function:github.com/example/glpi-ai-agent/internal/knowledge:matchesAnyGlob", + "to": "package:strings", + "kind": "calls_package", + "label": "TrimSpace" }, { "id": "function:github.com/example/glpi-ai-agent/internal/knowledge:mergeLoadStats-\u003efunction:github.com/example/glpi-ai-agent/internal/knowledge:mergeStrings:calls", @@ -42327,18 +42466,18 @@ "kind": "calls_package", "label": "TrimSpace" }, - { - "id": "function:github.com/example/glpi-ai-agent/internal/knowledge:readDocs-\u003efunction:github.com/example/glpi-ai-agent/internal/knowledge:NeuroForgeBackend.Name:calls", - "from": "function:github.com/example/glpi-ai-agent/internal/knowledge:readDocs", - "to": "function:github.com/example/glpi-ai-agent/internal/knowledge:NeuroForgeBackend.Name", - "kind": "calls" - }, { "id": "function:github.com/example/glpi-ai-agent/internal/knowledge:readDocs-\u003efunction:github.com/example/glpi-ai-agent/internal/knowledge:decodeKnowledgeDoc:calls", "from": "function:github.com/example/glpi-ai-agent/internal/knowledge:readDocs", "to": "function:github.com/example/glpi-ai-agent/internal/knowledge:decodeKnowledgeDoc", "kind": "calls" }, + { + "id": "function:github.com/example/glpi-ai-agent/internal/knowledge:readDocs-\u003efunction:github.com/example/glpi-ai-agent/internal/knowledge:listKnowledgeJSONFiles:calls", + "from": "function:github.com/example/glpi-ai-agent/internal/knowledge:readDocs", + "to": "function:github.com/example/glpi-ai-agent/internal/knowledge:listKnowledgeJSONFiles", + "kind": "calls" + }, { "id": "function:github.com/example/glpi-ai-agent/internal/knowledge:readDocs-\u003efunction:github.com/example/glpi-ai-agent/internal/knowledge:matchesAnyGlob:calls", "from": "function:github.com/example/glpi-ai-agent/internal/knowledge:readDocs", @@ -42369,21 +42508,14 @@ "from": "function:github.com/example/glpi-ai-agent/internal/knowledge:readDocs", "to": "package:os", "kind": "calls_package", - "label": "ReadDir" - }, - { - "id": "function:github.com/example/glpi-ai-agent/internal/knowledge:readDocs-\u003epackage:path/filepath:calls_package", - "from": "function:github.com/example/glpi-ai-agent/internal/knowledge:readDocs", - "to": "package:path/filepath", - "kind": "calls_package", - "label": "Join" + "label": "ReadFile" }, { "id": "function:github.com/example/glpi-ai-agent/internal/knowledge:readDocs-\u003epackage:strings:calls_package", "from": "function:github.com/example/glpi-ai-agent/internal/knowledge:readDocs", "to": "package:strings", "kind": "calls_package", - "label": "HasSuffix" + "label": "ToLower" }, { "id": "function:github.com/example/glpi-ai-agent/internal/knowledge:safeID-\u003epackage:strings:calls_package", @@ -43588,9 +43720,9 @@ "label": "TrimSpace" }, { - "id": "function:github.com/example/glpi-ai-agent/internal/ollama:Client.Embed-\u003efunction:github.com/example/glpi-ai-agent/internal/ollama:Client.post:calls", + "id": "function:github.com/example/glpi-ai-agent/internal/ollama:Client.Embed-\u003efunction:github.com/example/glpi-ai-agent/internal/ollama:Client.embedAdaptive:calls", "from": "function:github.com/example/glpi-ai-agent/internal/ollama:Client.Embed", - "to": "function:github.com/example/glpi-ai-agent/internal/ollama:Client.post", + "to": "function:github.com/example/glpi-ai-agent/internal/ollama:Client.embedAdaptive", "kind": "calls" }, { @@ -43599,13 +43731,6 @@ "to": "function:github.com/example/glpi-ai-agent/internal/ollama:withStage", "kind": "calls" }, - { - "id": "function:github.com/example/glpi-ai-agent/internal/ollama:Client.Embed-\u003epackage:fmt:calls_package", - "from": "function:github.com/example/glpi-ai-agent/internal/ollama:Client.Embed", - "to": "package:fmt", - "kind": "calls_package", - "label": "Errorf" - }, { "id": "function:github.com/example/glpi-ai-agent/internal/ollama:Client.NodeStatuses-\u003efunction:github.com/example/glpi-ai-agent/internal/ollama:poolNode.status:calls", "from": "function:github.com/example/glpi-ai-agent/internal/ollama:Client.NodeStatuses", @@ -43650,6 +43775,50 @@ "kind": "calls_package", "label": "NewTicker" }, + { + "id": "function:github.com/example/glpi-ai-agent/internal/ollama:Client.embedAdaptive-\u003efunction:github.com/example/glpi-ai-agent/internal/ollama:Client.embedOnce:calls", + "from": "function:github.com/example/glpi-ai-agent/internal/ollama:Client.embedAdaptive", + "to": "function:github.com/example/glpi-ai-agent/internal/ollama:Client.embedOnce", + "kind": "calls" + }, + { + "id": "function:github.com/example/glpi-ai-agent/internal/ollama:Client.embedAdaptive-\u003efunction:github.com/example/glpi-ai-agent/internal/ollama:isEmbeddingContextLengthError:calls", + "from": "function:github.com/example/glpi-ai-agent/internal/ollama:Client.embedAdaptive", + "to": "function:github.com/example/glpi-ai-agent/internal/ollama:isEmbeddingContextLengthError", + "kind": "calls" + }, + { + "id": "function:github.com/example/glpi-ai-agent/internal/ollama:Client.embedAdaptive-\u003efunction:github.com/example/glpi-ai-agent/internal/ollama:splitEmbeddingText:calls", + "from": "function:github.com/example/glpi-ai-agent/internal/ollama:Client.embedAdaptive", + "to": "function:github.com/example/glpi-ai-agent/internal/ollama:splitEmbeddingText", + "kind": "calls" + }, + { + "id": "function:github.com/example/glpi-ai-agent/internal/ollama:Client.embedAdaptive-\u003efunction:github.com/example/glpi-ai-agent/internal/ollama:weightedEmbeddingAverage:calls", + "from": "function:github.com/example/glpi-ai-agent/internal/ollama:Client.embedAdaptive", + "to": "function:github.com/example/glpi-ai-agent/internal/ollama:weightedEmbeddingAverage", + "kind": "calls" + }, + { + "id": "function:github.com/example/glpi-ai-agent/internal/ollama:Client.embedAdaptive-\u003epackage:fmt:calls_package", + "from": "function:github.com/example/glpi-ai-agent/internal/ollama:Client.embedAdaptive", + "to": "package:fmt", + "kind": "calls_package", + "label": "Errorf" + }, + { + "id": "function:github.com/example/glpi-ai-agent/internal/ollama:Client.embedOnce-\u003efunction:github.com/example/glpi-ai-agent/internal/ollama:Client.post:calls", + "from": "function:github.com/example/glpi-ai-agent/internal/ollama:Client.embedOnce", + "to": "function:github.com/example/glpi-ai-agent/internal/ollama:Client.post", + "kind": "calls" + }, + { + "id": "function:github.com/example/glpi-ai-agent/internal/ollama:Client.embedOnce-\u003epackage:fmt:calls_package", + "from": "function:github.com/example/glpi-ai-agent/internal/ollama:Client.embedOnce", + "to": "package:fmt", + "kind": "calls_package", + "label": "Errorf" + }, { "id": "function:github.com/example/glpi-ai-agent/internal/ollama:Client.executeDecision-\u003efunction:github.com/example/glpi-ai-agent/internal/ollama:Client.post:calls", "from": "function:github.com/example/glpi-ai-agent/internal/ollama:Client.executeDecision", @@ -44081,6 +44250,13 @@ "kind": "calls_package", "label": "ToLower" }, + { + "id": "function:github.com/example/glpi-ai-agent/internal/ollama:isEmbeddingContextLengthError-\u003epackage:strings:calls_package", + "from": "function:github.com/example/glpi-ai-agent/internal/ollama:isEmbeddingContextLengthError", + "to": "package:strings", + "kind": "calls_package", + "label": "ToLower" + }, { "id": "function:github.com/example/glpi-ai-agent/internal/ollama:modelNameMatches-\u003epackage:strings:calls_package", "from": "function:github.com/example/glpi-ai-agent/internal/ollama:modelNameMatches", @@ -44150,6 +44326,13 @@ "kind": "calls_package", "label": "TrimSpace" }, + { + "id": "function:github.com/example/glpi-ai-agent/internal/ollama:splitEmbeddingText-\u003epackage:strings:calls_package", + "from": "function:github.com/example/glpi-ai-agent/internal/ollama:splitEmbeddingText", + "to": "package:strings", + "kind": "calls_package", + "label": "TrimSpace" + }, { "id": "function:github.com/example/glpi-ai-agent/internal/ollama:uniqueStrings-\u003epackage:strings:calls_package", "from": "function:github.com/example/glpi-ai-agent/internal/ollama:uniqueStrings", @@ -44157,6 +44340,13 @@ "kind": "calls_package", "label": "ToLower" }, + { + "id": "function:github.com/example/glpi-ai-agent/internal/ollama:weightedEmbeddingAverage-\u003epackage:fmt:calls_package", + "from": "function:github.com/example/glpi-ai-agent/internal/ollama:weightedEmbeddingAverage", + "to": "package:fmt", + "kind": "calls_package", + "label": "Errorf" + }, { "id": "function:github.com/example/glpi-ai-agent/internal/ollama:withStage-\u003epackage:context:calls_package", "from": "function:github.com/example/glpi-ai-agent/internal/ollama:withStage", @@ -67002,10 +67192,10 @@ } ], "meta": { - "edges": 7289, + "edges": 7306, "format_version": 1, "generator": "go-ast+compose", "modules": 4, - "nodes": 1848 + "nodes": 1855 } }