diff --git a/.env.example b/.env.example index 8ad45c1..a657992 100644 --- a/.env.example +++ b/.env.example @@ -82,6 +82,10 @@ NEUROFORGE_WORKER_OLLAMA_API_KEY= NEUROFORGE_OLLAMA_API_KEY= OLLAMA_TIMEOUT=10m OLLAMA_MAX_CONCURRENT=1 +# Explicit chat context for Agent requests. Prevents a gateway/server default +# (often 4k on smaller VRAM profiles) from truncating structured JSON. +# gemma4 can use larger values such as 32768/65536/131072 when the runtime supports it. +OLLAMA_NUM_CTX=8192 OLLAMA_NUM_PREDICT=768 OLLAMA_JSON_RETRIES=1 OLLAMA_KEEP_ALIVE=10m @@ -438,6 +442,8 @@ KNOWLEDGE_EMBEDDING_PROFILE=auto # OLLAMA_MAX_CONCURRENT bleibt als Legacy-Alias für # OLLAMA_NODE_MAX_INFLIGHT erhalten, falls der neue Wert nicht gesetzt ist. +# Explizite Kontextgröße für Agent-Chatrequests. +# OLLAMA_NUM_CTX=8192 # already configured in SHARED OLLAMA RUNTIME above # Maximale Anzahl generierter Tokens für strukturierte Antworten. # OLLAMA_NUM_PREDICT=768 # already configured in SHARED OLLAMA RUNTIME above # Wiederholungen bei semantisch/strukturell fehlerhaftem Modell-JSON. diff --git a/MANIFEST.sha256 b/MANIFEST.sha256 index b514761..9c7f0c0 100644 --- a/MANIFEST.sha256 +++ b/MANIFEST.sha256 @@ -1,5 +1,5 @@ 27dc46be5cbb1b171deff7fbd2f28bff1be802dff403797535fd8968bb98c8eb ./.cbmignore -6b603e52548748b2e9717e53543905bc323d25ce3e541643e6bb0d4f4bc8bbce ./.env.example +c2bd51b30547a9b7f947932335db762a581f9120f7d5076c0685c10a88bf1631 ./.env.example 813c6a53b389380997fc71835ef72d3b593e1808c4fcc16dc0c309608a389f40 ./.env.subagent.example 2d49d2ee955bf352cc7e76068ec842808c0bf7bffaacb6a129b641a78bbde0e5 ./.gitea/workflows/ci.yml ed22fda7661db8203563611dc144998161cd024e161b0471d615eaf0defeb7db ./.gitea/workflows/release-tag-gitea.yml @@ -44,7 +44,7 @@ d38f8433fb34c0d9b88b788cc95219c8b8ca96e6de93243c9a9eb8fc64946e40 ./deployments/ 16f8e8224c0c46e5c79f35b322695046edb13e608d176cb0b9f25590f11b6b01 ./deployments/master/README.md e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 ./deployments/master/backups/.gitkeep 8127e9db5e5e0af1d88770dc8fa60b381de45dbcc843262698cf9501409b4d58 ./deployments/master/deploy/searxng/settings.yml -a7d49be8630c475230585db69ad977339d6b3e95c36379e75682f6ff124ab588 ./deployments/master/docker-compose.yml +21ad6610fa50cfb81ce0367ce79d5cdd69da88ac75f2c821f651e9c23a310a6b ./deployments/master/docker-compose.yml e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 ./deployments/master/knowledge/.gitkeep 6cd2e5bdf4c2a213f548e4f7a139faeeb56ff74f54e6da72e430fbd71342d64d ./deployments/master/monitoring/README.md 0c2fc23f8f6deced618e025cc66fa5711349439814d8a2c6d0f347990b4632e1 ./deployments/master/monitoring/grafana/dashboards/neuroforge-master-subagents.json @@ -61,7 +61,7 @@ e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 ./deployments/ 5d2ca8625ccb84cb868c3f7450a0c63fc128967aab5677363dfeab8f214f92c3 ./docker-bake.hcl 58db2a87b703a5bbddc20b140907d187f8388e7db87b079ae9f6ffaac9c121fc ./docker-compose.dev.yml 1360e44c867fdb9b754fe4585d57a02daf0f4820a7602a4e588c94c45eeb28e8 ./docker-compose.subagent.yml -7883c941cbe0be323601661567168aebfd7ec2f441063c453cfce56fb7af1083 ./docker-compose.yml +9319836cf9d143d62ec320d440cb76561449c28d3397a68f3e25388a29d4fd82 ./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 -4db4de9cac0f05961cfdc20b8c7e741cdb64d71432f4b5162fa005cf2c19a4d3 ./services/agent/.env.example +b21b8b6f8d48923c3b4a4e43a9431c1cf33f0e20b24d7d1fb81508f2741dc956 ./services/agent/.env.example 236713daf159ff0a8067e80a442ae3404fa28a5251ae6f24782f263bcfc17005 ./services/agent/.gitea/workflows/registry.yml 9ee46f2156ee805c4ff40e418b004ead5cfbc3a9b6df3968a1b772fe1b313cdc ./services/agent/.gitignore -e947cd34f7188f09581e5d04dd45ceed6fcef530d88ca4138cc4299cec08b43e ./services/agent/BETRIEBSANLEITUNG_GLPI_AI_AGENT.md -5343fe8bde3261ac5cb8e60759a43e7ea44ca647287d1fad7dfc350672f799ac ./services/agent/BETRIEBSANLEITUNG_GLPI_AI_AGENT_OLLAMA_POOL.md +7527d6ab43e02aae9779c92cc75e846ab908f0b93befda51e867cccee7fc9874 ./services/agent/BETRIEBSANLEITUNG_GLPI_AI_AGENT.md +5fca0fc1defb410011eb183875ed3a8aa9b1505fa17e9e3bf1e1bde387e3f7a8 ./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,15 +432,15 @@ c0e13c6703cf3feda57a9eaaa718e501280fbb526f466040eb685a27700b5797 ./services/age 0ed7ee0e4846be09994dd5f3b49ffdca6b59177b9018b1947388949f0dd49a14 ./services/agent/Makefile 80b20d2c2c78badc0dcc47e485e46bfd647877445c35faf261fb677ad58e8359 ./services/agent/OLLAMA-POOL.md 80b20d2c2c78badc0dcc47e485e46bfd647877445c35faf261fb677ad58e8359 ./services/agent/OLLAMA_POOL_BETRIEB.md -b0e1f907b26b31f3df4f9fae9ceaa59e468f3aa30487695fec89213912b685c8 ./services/agent/README.md +4cae0a453b46af7ab4afbc820a68be66a665e5d5806c10288d25519ece41d5ea ./services/agent/README.md aeac85d1adc92a8b34ffa5a95f7f19c2c6fb94742a7359a7fd452529d59ad87b ./services/agent/SECURITY.md -ee0b752092cd437e215c05064560b455e844c4a190780afe70900b9a275a2c2b ./services/agent/UPGRADE.md -5f12f3841c224f7a1658ec34fcb93d7ae8d91030f80469ecba25bfeae0c91c61 ./services/agent/cmd/agent/main.go +ed0ad852e9024fdbf1a0a4c4efe7aa88546fd440b4e3edb6ff78df65e31c8d7a ./services/agent/UPGRADE.md +9dbbaeab222a8d7fb4db07ebc2026bd377d1299704fd00d8b2b195ff50a81e29 ./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 -1457cb04cfd6bd05d45f7820f778c681c1a63cd2f8e7e500954c1b74c94a1b20 ./services/agent/docker-compose.registry.yml -cfc570de023b80f9a775d82a8ad5398521a4f377a85bb86642e60d0f22333d94 ./services/agent/docker-compose.yml +9eacaa9a7790d967064b424ad5135b809d461e6f882a34e4b8181dbbee195f68 ./services/agent/docker-compose.registry.yml +1f7494f1d244416a561d32a080c62bb98e032ccc0ca4734b5cc5a6213b7c798d ./services/agent/docker-compose.yml a0105475dc054977223fac36618b8cd8137c55be1d11fddcd24e9a4d3074c170 ./services/agent/glpi-ai-agent-neural-brain.patch 8a7805be0da45875d45a5e5b26780fe659f671edd2d6b72c0d7da9606bf15755 ./services/agent/go.mod beab3aa1f73f3b3f78f76ff43b60d2ea3d9c85353288b53e83b01bf3fd59db22 ./services/agent/internal/agent/agent.go @@ -458,8 +458,8 @@ 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 -3ce010749e2d378249e2da9ebd0471673a6c558db9273940788ddcc096d7103d ./services/agent/internal/config/config.go -fa1717032ad536bff867a07d3fdf0ae2e0a9d5d7637a27b43274832d51322eb6 ./services/agent/internal/config/config_test.go +0e44d44bf34e52e195804e994cf0d841490a97120c1860418a9d28d116f1c4dd ./services/agent/internal/config/config.go +ccd0f7c51c9cafec3756252f24de1991bfbab7df36afd63e7efcd3ce1ed6c4eb ./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 @@ -476,14 +476,14 @@ d3a9909672ea420db87be29ce669f3cd973125ff15bb46ca90a54c3aa98d9e73 ./services/age 482ca79552b02a8a53bbd547318a04e01a0b41e64d4a347b00f850c0218febda ./services/agent/internal/learning/store.go 07505924d3a15046836e7e6d278d1583c6591268f2216c3be78c58e35eafc71a ./services/agent/internal/learning/store_test.go 5030ec3f51830f7decfb24c27f9b243cb64f97302a6a51df096bc957f24b3fae ./services/agent/internal/metrics/metrics.go -809c3f2833ed2093bf1f9e4466eb3d855f2a84d827cb21deb698dd0e7b63b403 ./services/agent/internal/model/model.go +0e3943a88b9ad154e498eab821be4751341892f89ef8ecce9be1d1948050c235 ./services/agent/internal/model/model.go 975d5846fa38bf2478fa3ab9696660740c8d9a0549ce8d4f3443d4ca917c4aba ./services/agent/internal/model/reason_codes.go 821850d649c7661f1665dc6be030ebeeacb60e4e007e1e84891d4b779b9a5d0d ./services/agent/internal/model/reason_codes_test.go c89248224adf72720289215684b916eb209387c1d38b757d64804a8d3148011c ./services/agent/internal/obsidian/export.go b75ffec53e9a94505ee57ecb9361bffbb6805cfd06ea05a7bf68b6b28b268c09 ./services/agent/internal/obsidian/export_test.go -73ab85693941c2af0ab6999a366e4d5baab629405233869699001123110bed32 ./services/agent/internal/ollama/client.go -32949d24dbfef098426ed1891d27860b295a8d7f312972020beb6976360705e3 ./services/agent/internal/ollama/client_test.go -0e4e98a3db5ad781a4f7022bd74bbc4b434e64187afee077daf9174165509ea6 ./services/agent/internal/ollama/pool.go +69f1c79dd4940685036a053080486eb89a1ba3256be41eac333d07a513897b19 ./services/agent/internal/ollama/client.go +69ede3809a6c26163340e786c121b443026f7649a0f911e314b68bde2bdaeb3b ./services/agent/internal/ollama/client_test.go +9e3e2b74c9462f7ea0016c8a8e3fc7d974181b653e702bd8d1bb8cb1facd2f84 ./services/agent/internal/ollama/pool.go 4da6a7f953bfce654dbd8ab2267d1d519057fb230773e37538ce81aa049f9751 ./services/agent/internal/ollama/pool_test.go ab29c5808a06f6b21a0776355eeaa030f69be5eee251e7b1dbd7e00c71045c61 ./services/agent/internal/prioritysignals/signals.go 149bfefd67291ee288beed89a27235f58d3c33d87a1eff98cb2b7cc312fbd6b2 ./services/agent/internal/prioritysignals/signals_test.go @@ -495,7 +495,7 @@ ab29c5808a06f6b21a0776355eeaa030f69be5eee251e7b1dbd7e00c71045c61 ./services/age c849df7e346e1bd1e122037a4eafc30fe461add9b1876d7b9d63a02e33d831ad ./services/agent/internal/uptimekuma/client_test.go 5f0375f33c4c3f0804e58bb7b3a8699a450a05100274fbb19762d59f4d5d0034 ./services/agent/internal/web/control_graph.go cb68a23bef549186d251766a3d3820b611defd55e8f7386b8b87490a78b9e11d ./services/agent/internal/web/control_graph_test.go -6b457a720d8c8c6b07b590ce827d5a51de611e50b6905ff9668ec8f3f4046fdc ./services/agent/internal/web/server.go +f2eeda7068f88fbe9b188a5fa5062f95f36c631a111fec8ece9a737bbe353665 ./services/agent/internal/web/server.go 1edf74892fa1da9f614fee8ec47e6b900132677167f2642a625effc028f2dfa0 ./services/agent/internal/web/server_test.go 21b32b0b863ed62ae352011d7dd85f0134bf9909c23ee80b1c7e3c3605ef8cc4 ./services/agent/internal/web/templates/category-mappings.html 158f91963bbcd39e19fedd6d8a3ced2b26ffc92a51f00a84d883f24557e03cb0 ./services/agent/internal/web/templates/dashboard.html @@ -607,7 +607,7 @@ f1eab883370e0a40ef52a6b6d785a510d8ed48a19d25e0a2bc95f4f2cc8e329e ./services/age 895f8400aaad550ff2b96262eaa54e954d603129682e97aa4964539eb19bd7c2 ./services/agent/run.ps1 d1d4ab8145a7131c36df51d3a9fbdcdeb0cda3de134fe24c0e8d52370862f3b2 ./services/control/Dockerfile f30ddf9251860d92717276483f7a2c2d7405f0516940ca62abc21d97ff3f3ad2 ./services/control/cmd/engineering-graph/main.go -5b2b6ec4790c89d07834c4e52bbde9a75d67f72dd0ec1b5b4ad033958043fac3 ./services/control/engineering-graph.json +71fbe3877b30bc8a1453318026d587c7edaf3b7d6d8bdf2210ce62a9fca9ef81 ./services/control/engineering-graph.json ba44c599b9faf861eca647614c4abd18548e2708233cf462b11eb277e96449b8 ./services/control/go.mod da37393e58ff53847f26b1051f6d3a5270571d290b33d79360bae0dcb3513829 ./services/control/graph.go e6a8021a44219219fe37ab5edc45d1908fc6579f653aa80c9966810d891ad27d ./services/control/graph_test.go diff --git a/deployments/master/docker-compose.yml b/deployments/master/docker-compose.yml index 9bd0c2b..54fb920 100644 --- a/deployments/master/docker-compose.yml +++ b/deployments/master/docker-compose.yml @@ -312,6 +312,7 @@ services: OLLAMA_NODE_NAMES: ${OLLAMA_NODE_NAMES:-} OLLAMA_NODE_REQUEST_TIMEOUT: ${OLLAMA_NODE_REQUEST_TIMEOUT:-} OLLAMA_NODE_WEIGHTS: ${OLLAMA_NODE_WEIGHTS:-} + OLLAMA_NUM_CTX: ${OLLAMA_NUM_CTX:-} OLLAMA_NUM_PREDICT: ${OLLAMA_NUM_PREDICT:-} OLLAMA_REQUIRE_EMBEDDING_MODEL: ${OLLAMA_REQUIRE_EMBEDDING_MODEL:-} OLLAMA_REQUIRE_SAME_MODEL_DIGEST: ${OLLAMA_REQUIRE_SAME_MODEL_DIGEST:-} diff --git a/docker-compose.yml b/docker-compose.yml index 4ffe63b..4fb36ab 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -374,6 +374,7 @@ services: OLLAMA_NODE_NAMES: ${OLLAMA_NODE_NAMES:-} OLLAMA_NODE_REQUEST_TIMEOUT: ${OLLAMA_NODE_REQUEST_TIMEOUT:-} OLLAMA_NODE_WEIGHTS: ${OLLAMA_NODE_WEIGHTS:-} + OLLAMA_NUM_CTX: ${OLLAMA_NUM_CTX:-} OLLAMA_NUM_PREDICT: ${OLLAMA_NUM_PREDICT:-} OLLAMA_REQUIRE_EMBEDDING_MODEL: ${OLLAMA_REQUIRE_EMBEDDING_MODEL:-} OLLAMA_REQUIRE_SAME_MODEL_DIGEST: ${OLLAMA_REQUIRE_SAME_MODEL_DIGEST:-} diff --git a/services/agent/.env.example b/services/agent/.env.example index e6cb35a..376b10e 100644 --- a/services/agent/.env.example +++ b/services/agent/.env.example @@ -303,6 +303,10 @@ KNOWLEDGE_EMBEDDING_PROFILE=auto # OLLAMA_MAX_CONCURRENT bleibt als Legacy-Alias für # OLLAMA_NODE_MAX_INFLIGHT erhalten, falls der neue Wert nicht gesetzt ist. +# Explizite Kontextgröße für strukturierte Agent-Chatrequests. +# Höhere Werte benötigen mehr Runtime-Speicher; 8192 verhindert insbesondere +# die 4k-Trunkierung bei umfangreichen Kategorieprompts. +OLLAMA_NUM_CTX=8192 # Maximale Anzahl generierter Tokens für strukturierte Antworten. OLLAMA_NUM_PREDICT=768 # Wiederholungen bei semantisch/strukturell fehlerhaftem Modell-JSON. diff --git a/services/agent/BETRIEBSANLEITUNG_GLPI_AI_AGENT.md b/services/agent/BETRIEBSANLEITUNG_GLPI_AI_AGENT.md index 2cce8c0..7c8072b 100644 --- a/services/agent/BETRIEBSANLEITUNG_GLPI_AI_AGENT.md +++ b/services/agent/BETRIEBSANLEITUNG_GLPI_AI_AGENT.md @@ -737,6 +737,7 @@ Für gemeinsam genutzte Knowledge-Verzeichnisse ist `unscoped` der kompatibelste | `OLLAMA_REQUIRE_EMBEDDING_MODEL` | Agent | Verlangt das konfigurierte Embedding-Modell auf jedem Node. Bei false dürfen Chat-only-Nodes teilnehmen; Embedding-Requests werden weiterhin nur an Nodes mit Embeddingmodell gesendet. | true \| false | true | true | Bei `RAG_ENABLED=true` empfohlen. | | `OLLAMA_EMBEDDING_MODEL` | Agent | OLLAMA_MODEL ist bereits oben im gemeinsamen Compose-/Ollama-Bereich gesetzt: OLLAMA_MODEL=qwen3:8b Embedding-Modell für RAG. | Freier Text beziehungsweise installationsspezifischer Wert. | embeddinggemma | embeddinggemma | Optional; Wirkung abhängig von aktivierten Funktionen. | | `KNOWLEDGE_EMBEDDING_PROFILE` | Agent | Modellspezifisches Retrieval-Prompting. auto Modell automatisch erkennen und passende Retrieval-Prompts verwenden. Für embeddinggemma empfohlen. plain keine modellspezifischen Retrieval-Prompts. | auto \| plain \| embeddinggemma | auto | auto | Optional; Wirkung abhängig von aktivierten Funktionen. | +| `OLLAMA_NUM_CTX` | Agent | Explizite Kontextgröße für jeden `/api/chat`-Request; verhindert kleinere Ollama-/Gateway-Defaults, die strukturiertes JSON am Kontextende abschneiden können. | Ganzzahl 2048–1048576; für `gemma4` je nach Runtime auch 131072 möglich. | 8192 | 8192 | Höhere Werte erhöhen den Speicherbedarf der Modell-Runtime. | | `OLLAMA_NUM_PREDICT` | Agent | OLLAMA_TIMEOUT und OLLAMA_MAX_CONCURRENT sind bereits oben gesetzt. Maximale Anzahl generierter Tokens für strukturierte Antworten. | Ganzzahl 1–4096. | 768 | 768 | Optional; Wirkung abhängig von aktivierten Funktionen. | | `OLLAMA_JSON_RETRIES` | Agent | Wiederholungen bei fehlerhaftem / abgeschnittenem JSON. | Ganzzahl 0–3. | 1 | 1 | Optional; Wirkung abhängig von aktivierten Funktionen. | | `OLLAMA_KEEP_ALIVE` | Agent | Ollama-Modell nach Benutzung im Speicher halten. 5m 10m 30m | Dauer >= 0; 0 ist zulässig. | 10m | 10m | Optional; Wirkung abhängig von aktivierten Funktionen. | diff --git a/services/agent/BETRIEBSANLEITUNG_GLPI_AI_AGENT_OLLAMA_POOL.md b/services/agent/BETRIEBSANLEITUNG_GLPI_AI_AGENT_OLLAMA_POOL.md index babbde0..748417e 100644 --- a/services/agent/BETRIEBSANLEITUNG_GLPI_AI_AGENT_OLLAMA_POOL.md +++ b/services/agent/BETRIEBSANLEITUNG_GLPI_AI_AGENT_OLLAMA_POOL.md @@ -737,6 +737,7 @@ Für gemeinsam genutzte Knowledge-Verzeichnisse ist `unscoped` der kompatibelste | `OLLAMA_REQUIRE_EMBEDDING_MODEL` | Agent | Verlangt das konfigurierte Embedding-Modell auf jedem Node. Bei false dürfen Chat-only-Nodes teilnehmen; Embedding-Requests werden weiterhin nur an Nodes mit Embeddingmodell gesendet. | true \| false | true | true | Bei `RAG_ENABLED=true` empfohlen. | | `OLLAMA_EMBEDDING_MODEL` | Agent | OLLAMA_MODEL ist bereits oben im gemeinsamen Compose-/Ollama-Bereich gesetzt: OLLAMA_MODEL=qwen3:8b Embedding-Modell für RAG. | Freier Text beziehungsweise installationsspezifischer Wert. | embeddinggemma | embeddinggemma | Optional; Wirkung abhängig von aktivierten Funktionen. | | `KNOWLEDGE_EMBEDDING_PROFILE` | Agent | Modellspezifisches Retrieval-Prompting. auto Modell automatisch erkennen und passende Retrieval-Prompts verwenden. Für embeddinggemma empfohlen. plain keine modellspezifischen Retrieval-Prompts. | auto \| plain \| embeddinggemma | auto | auto | Optional; Wirkung abhängig von aktivierten Funktionen. | +| `OLLAMA_NUM_CTX` | Agent | Explizite Kontextgröße für jeden `/api/chat`-Request; verhindert kleinere Ollama-/Gateway-Defaults, die strukturiertes JSON am Kontextende abschneiden können. | Ganzzahl 2048–1048576; für `gemma4` je nach Runtime auch 131072 möglich. | 8192 | 8192 | Höhere Werte erhöhen den Speicherbedarf der Modell-Runtime. | | `OLLAMA_NUM_PREDICT` | Agent | OLLAMA_TIMEOUT und OLLAMA_MAX_CONCURRENT sind bereits oben gesetzt. Maximale Anzahl generierter Tokens für strukturierte Antworten. | Ganzzahl 1–4096. | 768 | 768 | Optional; Wirkung abhängig von aktivierten Funktionen. | | `OLLAMA_JSON_RETRIES` | Agent | Wiederholungen bei fehlerhaftem / abgeschnittenem JSON. | Ganzzahl 0–3. | 1 | 1 | Optional; Wirkung abhängig von aktivierten Funktionen. | | `OLLAMA_KEEP_ALIVE` | Agent | Ollama-Modell nach Benutzung im Speicher halten. 5m 10m 30m | Dauer >= 0; 0 ist zulässig. | 10m | 10m | Optional; Wirkung abhängig von aktivierten Funktionen. | diff --git a/services/agent/README.md b/services/agent/README.md index 5a4ca49..cd8606c 100644 --- a/services/agent/README.md +++ b/services/agent/README.md @@ -547,13 +547,14 @@ For local LLMs, the default request budget is intentionally longer than a typica ```env OLLAMA_TIMEOUT=10m +OLLAMA_NUM_CTX=8192 OLLAMA_NUM_PREDICT=256 OLLAMA_KEEP_ALIVE=10m OLLAMA_THINK=false OLLAMA_MAX_CONCURRENT=1 ``` -`OLLAMA_NUM_PREDICT` limits the maximum generated tokens for the small structured decision. `OLLAMA_KEEP_ALIVE` asks Ollama to keep the analysis model loaded between tickets. `OLLAMA_THINK=false` disables optional model thinking for this deterministic classification task. `OLLAMA_NODE_MAX_INFLIGHT=1` serializes inference on each individual pool node. `OLLAMA_MAX_CONCURRENT` remains a backwards-compatible alias when the new per-node value is not set. On very slow CPU-only hosts, use a smaller local model and/or increase `OLLAMA_TIMEOUT`. +`OLLAMA_NUM_CTX` is sent explicitly with every `/api/chat` request so an Ollama server or compatible gateway cannot silently fall back to a smaller per-request context (for example 4k) and truncate structured JSON. Increase it only when the model/runtime has sufficient memory. `OLLAMA_NUM_PREDICT` limits the maximum generated tokens for the small structured decision. `OLLAMA_KEEP_ALIVE` asks Ollama to keep the analysis model loaded between tickets. `OLLAMA_THINK=false` disables optional model thinking for this deterministic classification task. `OLLAMA_NODE_MAX_INFLIGHT=1` serializes inference on each individual pool node. `OLLAMA_MAX_CONCURRENT` remains a backwards-compatible alias when the new per-node value is not set. On very slow CPU-only hosts, use a smaller local model and/or increase `OLLAMA_TIMEOUT`. After upgrading an existing Compose deployment, recreate the stack so the init service runs: @@ -591,6 +592,7 @@ Web-verwaltete Artikel landen **nicht** im statischen `KNOWLEDGE_DIR`, sondern u Für stabilere Structured Outputs sind die empfohlenen Startwerte: ```env +OLLAMA_NUM_CTX=8192 OLLAMA_NUM_PREDICT=768 OLLAMA_JSON_RETRIES=1 ``` diff --git a/services/agent/UPGRADE.md b/services/agent/UPGRADE.md index b6b160b..e57334c 100644 --- a/services/agent/UPGRADE.md +++ b/services/agent/UPGRADE.md @@ -148,6 +148,7 @@ Variable ausdrücklich auf `internal-category` gesetzt werden. ## Neue/empfohlene Variablen ```env +OLLAMA_NUM_CTX=8192 OLLAMA_NUM_PREDICT=768 OLLAMA_JSON_RETRIES=1 diff --git a/services/agent/cmd/agent/main.go b/services/agent/cmd/agent/main.go index 88339c1..cfe6a7b 100644 --- a/services/agent/cmd/agent/main.go +++ b/services/agent/cmd/agent/main.go @@ -72,8 +72,9 @@ func main() { slog.Error("Ollama pool configuration failed", "error", err) os.Exit(1) } + o.SetNumCtx(cfg.OllamaNumCtx) o.Start(ctx) - slog.Info("Ollama pool configured", "nodes", len(nodes), "routing", cfg.OllamaRoutingMode, "max_inflight_per_node", cfg.OllamaNodeMaxInflight, "failover", cfg.OllamaFailoverEnabled, "failover_attempts", cfg.OllamaFailoverAttempts, "require_same_digest", cfg.OllamaRequireSameDigest) + slog.Info("Ollama pool configured", "nodes", len(nodes), "routing", cfg.OllamaRoutingMode, "max_inflight_per_node", cfg.OllamaNodeMaxInflight, "failover", cfg.OllamaFailoverEnabled, "failover_attempts", cfg.OllamaFailoverAttempts, "require_same_digest", cfg.OllamaRequireSameDigest, "num_ctx", cfg.OllamaNumCtx) if err := g.ValidateContract(ctx); err != nil { slog.Error("GLPI API contract validation failed", "error", err) os.Exit(1) diff --git a/services/agent/docker-compose.registry.yml b/services/agent/docker-compose.registry.yml index b075921..09aa8d7 100644 --- a/services/agent/docker-compose.registry.yml +++ b/services/agent/docker-compose.registry.yml @@ -10,6 +10,7 @@ services: OLLAMA_URL: ${OLLAMA_URL:-http://ollama:11434} OLLAMA_API_KEY: ${OLLAMA_API_KEY:-} OLLAMA_TIMEOUT: ${OLLAMA_TIMEOUT:-10m} + OLLAMA_NUM_CTX: ${OLLAMA_NUM_CTX:-8192} OLLAMA_NUM_PREDICT: ${OLLAMA_NUM_PREDICT:-768} OLLAMA_JSON_RETRIES: ${OLLAMA_JSON_RETRIES:-1} OLLAMA_KEEP_ALIVE: ${OLLAMA_KEEP_ALIVE:-10m} diff --git a/services/agent/docker-compose.yml b/services/agent/docker-compose.yml index 92e9667..825db6d 100644 --- a/services/agent/docker-compose.yml +++ b/services/agent/docker-compose.yml @@ -27,6 +27,7 @@ services: OLLAMA_API_KEY: ${OLLAMA_API_KEY:-} # Local CPU inference can take several minutes on the first request. OLLAMA_TIMEOUT: ${OLLAMA_TIMEOUT:-10m} + OLLAMA_NUM_CTX: ${OLLAMA_NUM_CTX:-8192} OLLAMA_NUM_PREDICT: ${OLLAMA_NUM_PREDICT:-768} OLLAMA_JSON_RETRIES: ${OLLAMA_JSON_RETRIES:-1} OLLAMA_KEEP_ALIVE: ${OLLAMA_KEEP_ALIVE:-10m} diff --git a/services/agent/internal/config/config.go b/services/agent/internal/config/config.go index 12f9463..8be6ba6 100644 --- a/services/agent/internal/config/config.go +++ b/services/agent/internal/config/config.go @@ -47,6 +47,7 @@ type Config struct { OllamaEmbeddingModel string OllamaTimeout time.Duration OllamaNumPredict int + OllamaNumCtx int OllamaKeepAlive time.Duration OllamaThink bool OllamaMaxConcurrent int // legacy alias for per-node concurrency @@ -256,6 +257,7 @@ func Load() (Config, error) { OllamaEmbeddingModel: env("OLLAMA_EMBEDDING_MODEL", "embeddinggemma"), OllamaTimeout: envDuration("OLLAMA_TIMEOUT", 10*time.Minute), OllamaNumPredict: envInt("OLLAMA_NUM_PREDICT", 768), + OllamaNumCtx: envInt("OLLAMA_NUM_CTX", 8192), OllamaKeepAlive: envDuration("OLLAMA_KEEP_ALIVE", 10*time.Minute), OllamaThink: envBool("OLLAMA_THINK", false), OllamaMaxConcurrent: envInt("OLLAMA_MAX_CONCURRENT", 1), @@ -499,6 +501,9 @@ func (c Config) Validate() error { if c.OllamaNumPredict <= 0 || c.OllamaNumPredict > 4096 { return errors.New("OLLAMA_NUM_PREDICT must be between 1 and 4096") } + if c.OllamaNumCtx != 0 && (c.OllamaNumCtx < 2048 || c.OllamaNumCtx > 1048576) { + return errors.New("OLLAMA_NUM_CTX must be 0 or between 2048 and 1048576") + } if c.OllamaKeepAlive < 0 { return errors.New("OLLAMA_KEEP_ALIVE must be >= 0") } diff --git a/services/agent/internal/config/config_test.go b/services/agent/internal/config/config_test.go index 2c5c63e..776b933 100644 --- a/services/agent/internal/config/config_test.go +++ b/services/agent/internal/config/config_test.go @@ -27,12 +27,25 @@ func validConfig() Config { CommunicationStyle: "formal", OllamaTimeout: time.Minute, OllamaNumPredict: 256, + OllamaNumCtx: 8192, OllamaKeepAlive: 10 * time.Minute, OllamaThink: false, OllamaMaxConcurrent: 1, } } +func TestValidateOllamaNumCtx(t *testing.T) { + c := validConfig() + c.OllamaNumCtx = 1024 + if err := c.Validate(); err == nil { + t.Fatal("expected too-small OLLAMA_NUM_CTX to be rejected") + } + c.OllamaNumCtx = 131072 + if err := c.Validate(); err != nil { + t.Fatalf("131072 context should validate: %v", err) + } +} + func TestValidateRejectsPlainHTTPByDefault(t *testing.T) { c := validConfig() c.GLPIURL = "http://glpi.internal.example" diff --git a/services/agent/internal/model/model.go b/services/agent/internal/model/model.go index a551609..d3c27c1 100644 --- a/services/agent/internal/model/model.go +++ b/services/agent/internal/model/model.go @@ -380,6 +380,7 @@ type OllamaRequestAttempt struct { PromptEvalDuration int64 `json:"prompt_eval_duration_ns,omitempty"` EvalCount int64 `json:"eval_count,omitempty"` EvalDuration int64 `json:"eval_duration_ns,omitempty"` + DoneReason string `json:"done_reason,omitempty"` } // OllamaProviderTrace summarizes the routing of one logical analysis. diff --git a/services/agent/internal/ollama/client.go b/services/agent/internal/ollama/client.go index c3f37a3..66a6e9f 100644 --- a/services/agent/internal/ollama/client.go +++ b/services/agent/internal/ollama/client.go @@ -16,6 +16,7 @@ type Client struct { model, embeddingModel string language, communicationStyle string numPredict int + numCtx int jsonRetries int keepAlive time.Duration think bool @@ -63,6 +64,20 @@ func (c *Client) Start(ctx context.Context) { c.pool.Start(ctx) } func (c *Client) Ping(ctx context.Context) error { return c.pool.Ping(ctx) } func (c *Client) NodeStatuses() []model.OllamaNodeStatus { return c.pool.NodeStatuses() } func (c *Client) RoutingMode() string { return c.routingMode } + +// SetNumCtx configures the context window sent explicitly with every chat +// request. Ollama/gateways may otherwise apply a smaller per-request default +// than the model itself supports, which can truncate structured JSON output. +// Call this during startup before the client begins serving requests. +func (c *Client) SetNumCtx(numCtx int) { c.numCtx = numCtx } + +func (c *Client) chatOptions() map[string]any { + options := map[string]any{"temperature": 0, "num_predict": c.numPredict} + if c.numCtx > 0 { + options["num_ctx"] = c.numCtx + } + return options +} func (c *Client) Embed(ctx context.Context, texts []string) ([][]float64, error) { ctx = withStage(ctx, "embedding") return c.embedAdaptive(ctx, texts, 0) @@ -192,7 +207,7 @@ func (c *Client) AnalyseCategory(ctx context.Context, t model.Ticket, categories "id": map[string]any{"type": "integer", "enum": categoryIDs}, "confidence": map[string]any{"type": "number", "minimum": 0, "maximum": 1}, }, "required": []string{"id", "confidence"}}, - "reason": map[string]any{"type": "string"}, + "reason": map[string]any{"type": "string", "maxLength": 320}, }, "required": []string{"category", "reason"}} promptHits := append([]model.KnowledgeHit(nil), categoryHits...) @@ -208,7 +223,7 @@ func (c *Client) AnalyseCategory(ctx context.Context, t model.Ticket, categories user := fmt.Sprintf("Ticket ID: %d\nAktuelle Kategorie: %d\nBetreff: %s\nInhalt:\n%s\n\nErlaubte Kategorien:\n%s\n\nKategorisierungs-Wissenseintraege:\n%s\n\nRead-only Betriebs- und Asset-Kontext:\n%s", t.ID, t.CategoryID, t.Name, t.Content, string(categoryJSON), string(hitJSON), string(contextJSON)) payload := map[string]any{ "model": c.model, "stream": false, "format": schema, "keep_alive": c.keepAlive.String(), "think": c.think, - "options": map[string]any{"temperature": 0, "num_predict": c.numPredict}, + "options": c.chatOptions(), "messages": []map[string]string{{"role": "system", "content": system}, {"role": "user", "content": user}}, } return c.executeDecision(ctx, payload, func(d model.Decision) error { @@ -249,13 +264,20 @@ func (c *Client) AnalyseStatus(ctx context.Context, t model.Ticket, category mod user := fmt.Sprintf("Ticket ID: %d\nBetreff: %s\nInhalt:\n%s\n\nEffektive Kategorie:\n%s\n\nAktive Uptime-Kuma-Kandidaten:\n%s", t.ID, t.Name, t.Content, string(categoryJSON), string(candidateJSON)) payload := map[string]any{ "model": c.model, "stream": false, "format": schema, "keep_alive": c.keepAlive.String(), "think": c.think, - "options": map[string]any{"temperature": 0, "num_predict": c.numPredict}, + "options": c.chatOptions(), "messages": []map[string]string{{"role": "system", "content": system}, {"role": "user", "content": user}}, } var lastErr error + lastWasDecode := false for attempt := 0; attempt <= c.jsonRetries; attempt++ { if attempt > 0 { - payload["messages"] = append(payload["messages"].([]map[string]string), map[string]string{"role": "user", "content": "Die vorherige Ausgabe war ungültig. Wiederhole nur die strukturierte Zuordnung als gültiges JSON."}) + if lastWasDecode { + options := c.chatOptions() + options["temperature"] = 0.05 + payload["options"] = options + } else { + payload["messages"] = append(payload["messages"].([]map[string]string), map[string]string{"role": "user", "content": "Die vorherige strukturierte Zuordnung war semantisch ungültig. Korrigiere sie gemäß Schema."}) + } } var resp struct { Message struct { @@ -268,8 +290,10 @@ func (c *Client) AnalyseStatus(ctx context.Context, t model.Ticket, category mod var d model.StatusDecision if err := json.Unmarshal([]byte(resp.Message.Content), &d); err != nil { lastErr = fmt.Errorf("invalid Ollama status response: %w", err) + lastWasDecode = true continue } + lastWasDecode = false if !d.Matched { d.CandidateID = "" return d, nil @@ -324,7 +348,7 @@ func (c *Client) AnalyseReply(ctx context.Context, t model.Ticket, category mode user := fmt.Sprintf("Ticket ID: %d\nBetreff: %s\nInhalt:\n%s\n\nEffektive Kategorie fuer die Antwortauswahl:\n%s\n\nErlaubte Antwort-Wissenseintraege:\n%s\n\nRead-only Betriebs- und Asset-Kontext:\n%s", t.ID, t.Name, t.Content, string(categoryJSON), string(hitJSON), string(contextJSON)) payload := map[string]any{ "model": c.model, "stream": false, "format": schema, "keep_alive": c.keepAlive.String(), "think": c.think, - "options": map[string]any{"temperature": 0, "num_predict": c.numPredict}, + "options": c.chatOptions(), "messages": []map[string]string{{"role": "system", "content": system}, {"role": "user", "content": user}}, } d, err := c.executeDecision(ctx, payload, func(d model.Decision) error { @@ -348,9 +372,19 @@ func (c *Client) AnalyseReply(ctx context.Context, t model.Ticket, category mode func (c *Client) executeDecision(ctx context.Context, payload map[string]any, validate func(model.Decision) error) (model.Decision, error) { var lastErr error + lastWasDecode := false for attempt := 0; attempt <= c.jsonRetries; attempt++ { if attempt > 0 { - payload["messages"] = append(payload["messages"].([]map[string]string), map[string]string{"role": "user", "content": "Die vorherige Ausgabe war unvollstaendig oder ungueltig. Wiederhole die Entscheidung vollstaendig und gib ausschliesslich ein gueltiges JSON-Objekt gemaess Schema zurueck."}) + if lastWasDecode { + // Keep parse-error retries at the same prompt size. Appending an + // instruction can consume the remaining output budget when an upstream + // Ollama gateway is running with a small context window. + options := c.chatOptions() + options["temperature"] = 0.05 + payload["options"] = options + } else { + payload["messages"] = append(payload["messages"].([]map[string]string), map[string]string{"role": "user", "content": "Die vorherige JSON-Entscheidung war semantisch ungültig. Korrigiere sie vollständig gemäß Schema."}) + } } var resp struct { Message struct { @@ -363,8 +397,10 @@ func (c *Client) executeDecision(ctx context.Context, payload map[string]any, va var d model.Decision if err := json.Unmarshal([]byte(resp.Message.Content), &d); err != nil { lastErr = fmt.Errorf("invalid Ollama structured response: %w", err) + lastWasDecode = true continue } + lastWasDecode = false if validate != nil { if err := validate(d); err != nil { lastErr = err @@ -423,13 +459,20 @@ func (c *Client) Analyse(ctx context.Context, t model.Ticket, categories []model "format": schema, "keep_alive": c.keepAlive.String(), "think": c.think, - "options": map[string]any{"temperature": 0, "num_predict": c.numPredict}, + "options": c.chatOptions(), "messages": []map[string]string{{"role": "system", "content": system}, {"role": "user", "content": user}}, } var lastErr error + lastWasDecode := false for attempt := 0; attempt <= c.jsonRetries; attempt++ { if attempt > 0 { - payload["messages"] = append(payload["messages"].([]map[string]string), map[string]string{"role": "user", "content": "Die vorherige Ausgabe war unvollstaendig oder kein gueltiges JSON. Wiederhole die Entscheidung jetzt vollstaendig und gib ausschliesslich ein gueltiges JSON-Objekt gemaess Schema zurueck."}) + if lastWasDecode { + options := c.chatOptions() + options["temperature"] = 0.05 + payload["options"] = options + } else { + payload["messages"] = append(payload["messages"].([]map[string]string), map[string]string{"role": "user", "content": "Die vorherige JSON-Entscheidung war semantisch ungültig. Korrigiere sie vollständig gemäß Schema."}) + } } var resp struct { Message struct { @@ -442,8 +485,10 @@ func (c *Client) Analyse(ctx context.Context, t model.Ticket, categories []model var d model.Decision if err := json.Unmarshal([]byte(resp.Message.Content), &d); err != nil { lastErr = fmt.Errorf("invalid Ollama structured response: %w", err) + lastWasDecode = true continue } + lastWasDecode = false if len(knownKnowledge) == 0 { // A reply is structurally impossible without an explicitly supplied // Knowledge candidate. Ignore any contradictory model output instead @@ -500,7 +545,7 @@ Die deterministisch extrahierten Belege sind keine fertige Prioritätsentscheidu Verwende insufficient_information nur, wenn weder Auswirkung noch Dringlichkeit aus Ticket, Kategorie, Kontext oder den deterministischen Belegen belastbar eingeordnet werden können. Wenn ein expliziter Mehrbenutzer-, Standort-, Organisations-, Workaround- oder Kein-Workaround-Beleg vorhanden ist, darf insufficient_information nicht verwendet werden. Bei unzureichenden Angaben verwende genau einmal insufficient_information, empfehle die aktuelle Priorität unverändert und nenne keine weiteren reason_codes. Verwende höchstens drei unterschiedliche reason_codes. Das Feld reason muss eine kurze, verständliche Begründung in ganzen Sätzen sein und darf nicht nur aus einem reason_code bestehen. Die interne Begründung ist in %s und im Stil %s. Gib ausschließlich das geforderte JSON zurück.`, c.language, c.communicationStyle) user := fmt.Sprintf("Ticket ID: %d\nBetreff: %s\nInhalt:\n%s\n\nAktuelle Werte: priority=%d impact=%d urgency=%d status=%d\nEffektive Kategorie:\n%s\n\nDeterministisch extrahierte Belege:\n%s\n\nRead-only Kontext:\n%s", t.ID, t.Name, t.Content, t.Priority, t.Impact, t.Urgency, t.StatusID, string(categoryJSON), string(evidenceJSON), string(contextJSON)) - payload := map[string]any{"model": c.model, "stream": false, "format": schema, "keep_alive": c.keepAlive.String(), "think": c.think, "options": map[string]any{"temperature": 0, "num_predict": c.numPredict}, "messages": []map[string]string{{"role": "system", "content": system}, {"role": "user", "content": user}}} + payload := map[string]any{"model": c.model, "stream": false, "format": schema, "keep_alive": c.keepAlive.String(), "think": c.think, "options": c.chatOptions(), "messages": []map[string]string{{"role": "system", "content": system}, {"role": "user", "content": user}}} var out model.PriorityDecision err := c.executeStructured(ctx, payload, &out, func() error { out.ReasonCodes = model.NormalizeReasonCodes(out.ReasonCodes) @@ -541,7 +586,7 @@ Verwende die deterministischen Belege als Tatsachen: no_human_response, unassign Verwende ausschließlich die bereitgestellten reason_codes und Aktionen. Erfinde keine SLA, Frist, Zuständigkeit, Ziel-ID oder Sicherheitslage. Die interne Begründung ist in %s und im Stil %s. Gib ausschließlich das geforderte JSON zurück.`, c.language, c.communicationStyle) user := fmt.Sprintf("Ticket ID: %d\nErstellt: %s\nGeändert: %s\nSLA-Ziel: %s\nStatus: %d\nPriorität: %d\nZugewiesene Gruppen: %v\nZugewiesene Benutzer: %v\nBetreff: %s\nInhalt:\n%s\n\nDeterministische Belege:\n%s\n\nEskalations-Constraints:\n%s\n\nFollowups:\n%s\n\nRead-only Kontext:\n%s", t.ID, t.DateCreation, t.DateMod, t.TimeToResolve, t.StatusID, t.Priority, t.AssignedGroups, t.AssignedUsers, t.Name, t.Content, string(evidenceJSON), string(constraintsJSON), string(followupJSON), string(contextJSON)) - payload := map[string]any{"model": c.model, "stream": false, "format": schema, "keep_alive": c.keepAlive.String(), "think": c.think, "options": map[string]any{"temperature": 0, "num_predict": c.numPredict}, "messages": []map[string]string{{"role": "system", "content": system}, {"role": "user", "content": user}}} + payload := map[string]any{"model": c.model, "stream": false, "format": schema, "keep_alive": c.keepAlive.String(), "think": c.think, "options": c.chatOptions(), "messages": []map[string]string{{"role": "system", "content": system}, {"role": "user", "content": user}}} var out model.EscalationDecision err := c.executeStructured(ctx, payload, &out, func() error { out.ReasonCodes = model.NormalizeReasonCodes(out.ReasonCodes) @@ -626,13 +671,20 @@ func normalizeEscalationModelActions(values []string) []string { func (c *Client) executeStructured(ctx context.Context, payload map[string]any, out any, validate func() error) error { var lastErr error + lastWasDecode := false for attempt := 0; attempt <= c.jsonRetries; attempt++ { if attempt > 0 { - msg := "Die vorherige Ausgabe war ungültig. Wiederhole ausschließlich das vollständige JSON gemäß Schema." - if lastErr != nil { - msg += " Validierungsfehler: " + lastErr.Error() + if lastWasDecode { + options := c.chatOptions() + options["temperature"] = 0.05 + payload["options"] = options + } else { + msg := "Die vorherige JSON-Ausgabe war semantisch ungültig. Korrigiere sie vollständig gemäß Schema." + if lastErr != nil { + msg += " Validierungsfehler: " + lastErr.Error() + } + payload["messages"] = append(payload["messages"].([]map[string]string), map[string]string{"role": "user", "content": msg}) } - payload["messages"] = append(payload["messages"].([]map[string]string), map[string]string{"role": "user", "content": msg}) } var resp struct { Message struct { @@ -644,8 +696,10 @@ func (c *Client) executeStructured(ctx context.Context, payload map[string]any, } if err := json.Unmarshal([]byte(resp.Message.Content), out); err != nil { lastErr = fmt.Errorf("invalid Ollama structured response: %w", err) + lastWasDecode = true continue } + lastWasDecode = false if validate != nil { if err := validate(); err != nil { lastErr = err diff --git a/services/agent/internal/ollama/client_test.go b/services/agent/internal/ollama/client_test.go index 450b54b..1931b2f 100644 --- a/services/agent/internal/ollama/client_test.go +++ b/services/agent/internal/ollama/client_test.go @@ -31,6 +31,9 @@ func TestAnalyseStructured(t *testing.T) { if options["num_predict"] != float64(256) { t.Errorf("unexpected num_predict: %v", options["num_predict"]) } + if options["num_ctx"] != float64(131072) { + t.Errorf("unexpected num_ctx: %v", options["num_ctx"]) + } if body["keep_alive"] != "10m0s" { t.Errorf("unexpected keep_alive: %v", body["keep_alive"]) } @@ -41,6 +44,7 @@ func TestAnalyseStructured(t *testing.T) { })) defer srv.Close() c := New(srv.URL, "m", "e", "de-DE", "formal", time.Second, 256, 10*time.Minute, false, 1, 1) + c.SetNumCtx(131072) d, err := c.Analyse(context.Background(), model.Ticket{ID: 1}, []model.Category{{ID: 1}}, nil, nil, model.ContextSnapshot{}) if err != nil { t.Fatal(err) @@ -71,6 +75,42 @@ func TestAnalyseRetriesInvalidJSON(t *testing.T) { } } +func TestStructuredRetryDoesNotGrowPrompt(t *testing.T) { + calls := 0 + var firstMessages string + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + calls++ + var body map[string]any + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + t.Fatal(err) + } + messages, _ := json.Marshal(body["messages"]) + if calls == 1 { + firstMessages = string(messages) + } else if string(messages) != firstMessages { + t.Fatalf("retry grew or changed messages:\nfirst=%s\nretry=%s", firstMessages, messages) + } + options, _ := body["options"].(map[string]any) + if options["num_ctx"] != float64(8192) { + t.Fatalf("retry num_ctx=%v, want 8192", options["num_ctx"]) + } + content := `{"category":` + if calls > 1 { + content = `{"category":{"id":2,"confidence":0.95},"reason":"ok"}` + } + _ = json.NewEncoder(w).Encode(map[string]any{"message": map[string]any{"content": content}}) + })) + defer srv.Close() + c := New(srv.URL, "m", "e", "de-DE", "formal", time.Second, 768, time.Minute, false, 1, 1) + c.SetNumCtx(8192) + if _, err := c.AnalyseCategory(context.Background(), model.Ticket{ID: 1}, []model.Category{{ID: 2, Name: "VPN"}}, nil, model.ContextSnapshot{}); err != nil { + t.Fatal(err) + } + if calls != 2 { + t.Fatalf("calls=%d, want 2", calls) + } +} + func TestEmbedDisablesSilentTruncation(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var body map[string]any @@ -160,6 +200,7 @@ func TestAnalyseDoesNotExposeRichAnswerHTMLToModel(t *testing.T) { })) defer srv.Close() c := New(srv.URL, "m", "e", "de-DE", "formal", time.Second, 768, time.Minute, false, 1, 0) + c.SetNumCtx(8192) hits := []model.KnowledgeHit{{Doc: model.KnowledgeDoc{ID: "GLPI-KB-1", Title: "Login", Text: "plain", Answer: "plain", AnswerHTML: `
RICH_SECRET_MARKUP
`}}} if _, err := c.Analyse(context.Background(), model.Ticket{ID: 1}, []model.Category{{ID: 2}}, nil, hits, model.ContextSnapshot{}); err != nil { t.Fatal(err) @@ -208,6 +249,13 @@ func TestAnalyseCategoryUsesDedicatedSchemaAndSanitizedKnowledge(t *testing.T) { if strings.Contains(string(formatJSON), `"reply"`) { t.Fatalf("reply schema leaked into category stage: %s", formatJSON) } + if !strings.Contains(string(formatJSON), `"maxLength":320`) { + t.Fatalf("category reason length guard missing: %s", formatJSON) + } + options, _ := body["options"].(map[string]any) + if options["num_ctx"] != float64(8192) { + t.Fatalf("category num_ctx=%v, want 8192", options["num_ctx"]) + } messagesJSON, _ := json.Marshal(body["messages"]) if strings.Contains(string(messagesJSON), "SECRET_ANSWER") { t.Fatalf("category answer leaked into prompt: %s", messagesJSON) @@ -216,6 +264,7 @@ func TestAnalyseCategoryUsesDedicatedSchemaAndSanitizedKnowledge(t *testing.T) { })) defer srv.Close() c := New(srv.URL, "m", "e", "de-DE", "formal", time.Second, 768, time.Minute, false, 1, 0) + c.SetNumCtx(8192) hits := []model.KnowledgeHit{{Doc: model.KnowledgeDoc{ID: "CAT", Text: "vpn evidence", Answer: "SECRET_ANSWER"}}} d, err := c.AnalyseCategory(context.Background(), model.Ticket{ID: 1}, []model.Category{{ID: 2, Name: "VPN"}}, hits, model.ContextSnapshot{}) if err != nil { diff --git a/services/agent/internal/ollama/pool.go b/services/agent/internal/ollama/pool.go index bd0579f..4a9ec61 100644 --- a/services/agent/internal/ollama/pool.go +++ b/services/agent/internal/ollama/pool.go @@ -45,12 +45,13 @@ type PoolConfig struct { } type requestMeta struct { - TotalDuration int64 `json:"total_duration"` - LoadDuration int64 `json:"load_duration"` - PromptEvalCount int64 `json:"prompt_eval_count"` - PromptEvalDuration int64 `json:"prompt_eval_duration"` - EvalCount int64 `json:"eval_count"` - EvalDuration int64 `json:"eval_duration"` + TotalDuration int64 `json:"total_duration"` + LoadDuration int64 `json:"load_duration"` + PromptEvalCount int64 `json:"prompt_eval_count"` + PromptEvalDuration int64 `json:"prompt_eval_duration"` + EvalCount int64 `json:"eval_count"` + EvalDuration int64 `json:"eval_duration"` + DoneReason string `json:"done_reason"` } type poolNode struct { @@ -519,7 +520,7 @@ func (p *Pool) post(ctx context.Context, path string, payload any, out any) erro InflightAtStart: inflight, HTTPStatus: status, Outcome: outcomeText(reqErr), Retryable: retryable, Error: errorText(reqErr), TotalDurationNS: meta.TotalDuration, LoadDurationNS: meta.LoadDuration, PromptEvalCount: meta.PromptEvalCount, PromptEvalDuration: meta.PromptEvalDuration, - EvalCount: meta.EvalCount, EvalDuration: meta.EvalDuration, + EvalCount: meta.EvalCount, EvalDuration: meta.EvalDuration, DoneReason: meta.DoneReason, }) if reqErr == nil { markTraceSuccess(ctx, n.name, n.baseURL) diff --git a/services/agent/internal/web/server.go b/services/agent/internal/web/server.go index 1dd2215..0ced7af 100644 --- a/services/agent/internal/web/server.go +++ b/services/agent/internal/web/server.go @@ -452,7 +452,7 @@ func (s *Server) status(w http.ResponseWriter, r *http.Request) { "uptime_kuma_enabled": s.cfg.UptimeKumaEnabled, "uptime_kuma_mode": s.cfg.UptimeKumaMode, "uptime_kuma_status_pages": s.cfg.UptimeKumaStatusPages, "context_fail_closed": s.cfg.ContextBlockReplyOnError, "context_incident_block": s.cfg.ContextBlockReplyOnIncident, "context_status_reply_enabled": s.cfg.ContextStatusReplyEnabled, "context_status_reply_min_relevance": s.cfg.ContextStatusReplyMinRelevance, "context_status_reply_min_ai_confidence": s.cfg.ContextStatusReplyMinAIConfidence, "context_status_reply_min_final_score": s.cfg.ContextStatusReplyMinFinalScore, "context_incident_reply_text_configured": strings.TrimSpace(s.cfg.ContextIncidentReplyText) != "", "context_maintenance_reply_text_configured": strings.TrimSpace(s.cfg.ContextMaintenanceReplyText) != "", "workers": s.cfg.Workers, "queue_size": s.cfg.QueueSize, "glpi_api_version": s.cfg.GLPIAPIVersion, "glpi_poll_interval": s.cfg.GLPIPollInterval.String(), "glpi_poll_limit": s.cfg.GLPIPollLimit, "glpi_allowed_status_ids": s.cfg.GLPIAllowedStatusIDs, "glpi_ticket_filter_configured": strings.TrimSpace(s.cfg.GLPITicketFilter) != "", "glpi_timeout": s.cfg.GLPITimeout.String(), - "ollama_model": s.cfg.OllamaModel, "ollama_embedding_model": s.cfg.OllamaEmbeddingModel, "ollama_timeout": s.cfg.OllamaTimeout.String(), "ollama_num_predict": s.cfg.OllamaNumPredict, "ollama_keep_alive": s.cfg.OllamaKeepAlive.String(), "ollama_think": s.cfg.OllamaThink, "ollama_max_concurrent": s.cfg.OllamaMaxConcurrent, "ollama_json_retries": s.cfg.OllamaJSONRetries, + "ollama_model": s.cfg.OllamaModel, "ollama_embedding_model": s.cfg.OllamaEmbeddingModel, "ollama_timeout": s.cfg.OllamaTimeout.String(), "ollama_num_predict": s.cfg.OllamaNumPredict, "ollama_num_ctx": s.cfg.OllamaNumCtx, "ollama_keep_alive": s.cfg.OllamaKeepAlive.String(), "ollama_think": s.cfg.OllamaThink, "ollama_max_concurrent": s.cfg.OllamaMaxConcurrent, "ollama_json_retries": s.cfg.OllamaJSONRetries, "ollama_nodes": ollamaNodes, "ollama_node_count": len(ollamaNodes), "ollama_healthy_nodes": ollamaHealthyNodes, "ollama_available_nodes": ollamaAvailableNodes, "ollama_routing_mode": ollamaRoutingMode, "ollama_node_max_inflight": s.cfg.OllamaNodeMaxInflight, "ollama_node_health_interval": s.cfg.OllamaNodeHealthInterval.String(), "ollama_node_failure_cooldown": s.cfg.OllamaNodeFailureCooldown.String(), "ollama_node_request_timeout": s.cfg.OllamaNodeRequestTimeout.String(), "ollama_failover_enabled": s.cfg.OllamaFailoverEnabled, "ollama_failover_attempts": s.cfg.OllamaFailoverAttempts, "ollama_require_same_model_digest": s.cfg.OllamaRequireSameDigest, "ollama_require_embedding_model": s.cfg.OllamaRequireEmbeddingModel, "rag_enabled": s.cfg.RAGEnabled, "knowledge_top_k": s.cfg.KnowledgeTopK, "knowledge_audit_top_k": s.cfg.KnowledgeAuditTopK, "knowledge_candidate_max_gap": s.cfg.KnowledgeCandidateMaxGap, "category_prompt_limit": s.cfg.CategoryPromptLimit, "knowledge_max_query_chunks": s.cfg.KnowledgeMaxQueryChunks, "glpi_kb_path": s.cfg.GLPIKBPath, "glpi_kb_filter_configured": strings.TrimSpace(s.cfg.GLPIKBFilter) != "", "glpi_kb_limit": s.cfg.GLPIKBLimit, "glpi_kb_auto_reply": s.cfg.GLPIKBAutoReply, "glpi_kb_auto_reply_category_ids": s.cfg.GLPIKBAutoReplyCategoryIDs, "glpi_kb_auto_reply_allow_uncategorized": s.cfg.GLPIKBAutoReplyAllowUncategorized, "glpi_kb_auto_reply_uncategorized_article_ids": s.cfg.GLPIKBAutoReplyUncategorizedArticleIDs, diff --git a/services/control/engineering-graph.json b/services/control/engineering-graph.json index 953bc96..9d5b4af 100644 --- a/services/control/engineering-graph.json +++ b/services/control/engineering-graph.json @@ -1072,7 +1072,7 @@ "community": "github.com/example/glpi-ai-agent/cmd/agent", "meta": { "exported": false, - "line": 260, + "line": 261, "path": "services/agent/cmd/agent/main.go" } }, @@ -1084,7 +1084,7 @@ "community": "github.com/example/glpi-ai-agent/cmd/agent", "meta": { "exported": false, - "line": 211, + "line": 212, "path": "services/agent/cmd/agent/main.go" } }, @@ -1096,7 +1096,7 @@ "community": "github.com/example/glpi-ai-agent/cmd/agent", "meta": { "exported": false, - "line": 227, + "line": 228, "path": "services/agent/cmd/agent/main.go" } }, @@ -2464,7 +2464,7 @@ "community": "github.com/example/glpi-ai-agent/internal/config", "meta": { "exported": true, - "line": 1070, + "line": 1075, "path": "services/agent/internal/config/config.go" } }, @@ -2476,7 +2476,7 @@ "community": "github.com/example/glpi-ai-agent/internal/config", "meta": { "exported": true, - "line": 444, + "line": 446, "path": "services/agent/internal/config/config.go" } }, @@ -2488,7 +2488,7 @@ "community": "github.com/example/glpi-ai-agent/internal/config", "meta": { "exported": true, - "line": 226, + "line": 227, "path": "services/agent/internal/config/config.go" } }, @@ -2500,7 +2500,7 @@ "community": "github.com/example/glpi-ai-agent/internal/config", "meta": { "exported": false, - "line": 1044, + "line": 1049, "path": "services/agent/internal/config/config.go" } }, @@ -2512,7 +2512,7 @@ "community": "github.com/example/glpi-ai-agent/internal/config", "meta": { "exported": false, - "line": 1215, + "line": 1220, "path": "services/agent/internal/config/config.go" } }, @@ -2524,7 +2524,7 @@ "community": "github.com/example/glpi-ai-agent/internal/config", "meta": { "exported": false, - "line": 1259, + "line": 1264, "path": "services/agent/internal/config/config.go" } }, @@ -2536,7 +2536,7 @@ "community": "github.com/example/glpi-ai-agent/internal/config", "meta": { "exported": false, - "line": 1248, + "line": 1253, "path": "services/agent/internal/config/config.go" } }, @@ -2548,7 +2548,7 @@ "community": "github.com/example/glpi-ai-agent/internal/config", "meta": { "exported": false, - "line": 1226, + "line": 1231, "path": "services/agent/internal/config/config.go" } }, @@ -2560,7 +2560,7 @@ "community": "github.com/example/glpi-ai-agent/internal/config", "meta": { "exported": false, - "line": 1237, + "line": 1242, "path": "services/agent/internal/config/config.go" } }, @@ -2572,7 +2572,7 @@ "community": "github.com/example/glpi-ai-agent/internal/config", "meta": { "exported": false, - "line": 1093, + "line": 1098, "path": "services/agent/internal/config/config.go" } }, @@ -2584,7 +2584,7 @@ "community": "github.com/example/glpi-ai-agent/internal/config", "meta": { "exported": false, - "line": 1115, + "line": 1120, "path": "services/agent/internal/config/config.go" } }, @@ -2596,7 +2596,7 @@ "community": "github.com/example/glpi-ai-agent/internal/config", "meta": { "exported": false, - "line": 1198, + "line": 1203, "path": "services/agent/internal/config/config.go" } }, @@ -2608,7 +2608,7 @@ "community": "github.com/example/glpi-ai-agent/internal/config", "meta": { "exported": false, - "line": 1059, + "line": 1064, "path": "services/agent/internal/config/config.go" } }, @@ -2620,7 +2620,7 @@ "community": "github.com/example/glpi-ai-agent/internal/config", "meta": { "exported": false, - "line": 1190, + "line": 1195, "path": "services/agent/internal/config/config.go" } }, @@ -2632,7 +2632,7 @@ "community": "github.com/example/glpi-ai-agent/internal/config", "meta": { "exported": false, - "line": 1137, + "line": 1142, "path": "services/agent/internal/config/config.go" } }, @@ -2644,7 +2644,7 @@ "community": "github.com/example/glpi-ai-agent/internal/config", "meta": { "exported": false, - "line": 1163, + "line": 1168, "path": "services/agent/internal/config/config.go" } }, @@ -2656,7 +2656,7 @@ "community": "github.com/example/glpi-ai-agent/internal/config", "meta": { "exported": false, - "line": 1053, + "line": 1058, "path": "services/agent/internal/config/config.go" } }, @@ -2668,7 +2668,7 @@ "community": "github.com/example/glpi-ai-agent/internal/config", "meta": { "exported": false, - "line": 1089, + "line": 1094, "path": "services/agent/internal/config/config.go" } }, @@ -2680,7 +2680,7 @@ "community": "github.com/example/glpi-ai-agent/internal/config", "meta": { "exported": false, - "line": 1030, + "line": 1035, "path": "services/agent/internal/config/config.go" } }, @@ -2692,7 +2692,7 @@ "community": "github.com/example/glpi-ai-agent/internal/config", "meta": { "exported": false, - "line": 1025, + "line": 1030, "path": "services/agent/internal/config/config.go" } }, @@ -2704,7 +2704,7 @@ "community": "github.com/example/glpi-ai-agent/internal/config", "meta": { "exported": false, - "line": 1000, + "line": 1005, "path": "services/agent/internal/config/config.go" } }, @@ -5572,7 +5572,7 @@ "community": "github.com/example/glpi-ai-agent/internal/ollama", "meta": { "exported": true, - "line": 379, + "line": 415, "path": "services/agent/internal/ollama/client.go" } }, @@ -5584,7 +5584,7 @@ "community": "github.com/example/glpi-ai-agent/internal/ollama", "meta": { "exported": true, - "line": 182, + "line": 197, "path": "services/agent/internal/ollama/client.go" } }, @@ -5596,7 +5596,7 @@ "community": "github.com/example/glpi-ai-agent/internal/ollama", "meta": { "exported": true, - "line": 519, + "line": 564, "path": "services/agent/internal/ollama/client.go" } }, @@ -5608,7 +5608,7 @@ "community": "github.com/example/glpi-ai-agent/internal/ollama", "meta": { "exported": true, - "line": 475, + "line": 520, "path": "services/agent/internal/ollama/client.go" } }, @@ -5620,7 +5620,7 @@ "community": "github.com/example/glpi-ai-agent/internal/ollama", "meta": { "exported": true, - "line": 291, + "line": 315, "path": "services/agent/internal/ollama/client.go" } }, @@ -5632,7 +5632,7 @@ "community": "github.com/example/glpi-ai-agent/internal/ollama", "meta": { "exported": true, - "line": 224, + "line": 239, "path": "services/agent/internal/ollama/client.go" } }, @@ -5644,7 +5644,7 @@ "community": "github.com/example/glpi-ai-agent/internal/ollama", "meta": { "exported": true, - "line": 66, + "line": 81, "path": "services/agent/internal/ollama/client.go" } }, @@ -5656,7 +5656,7 @@ "community": "github.com/example/glpi-ai-agent/internal/ollama", "meta": { "exported": true, - "line": 64, + "line": 65, "path": "services/agent/internal/ollama/client.go" } }, @@ -5668,7 +5668,7 @@ "community": "github.com/example/glpi-ai-agent/internal/ollama", "meta": { "exported": true, - "line": 63, + "line": 64, "path": "services/agent/internal/ollama/client.go" } }, @@ -5680,7 +5680,19 @@ "community": "github.com/example/glpi-ai-agent/internal/ollama", "meta": { "exported": true, - "line": 65, + "line": 66, + "path": "services/agent/internal/ollama/client.go" + } + }, + { + "id": "function:github.com/example/glpi-ai-agent/internal/ollama:Client.SetNumCtx", + "kind": "function", + "label": "Client.SetNumCtx", + "group": "engineering", + "community": "github.com/example/glpi-ai-agent/internal/ollama", + "meta": { + "exported": true, + "line": 72, "path": "services/agent/internal/ollama/client.go" } }, @@ -5692,7 +5704,19 @@ "community": "github.com/example/glpi-ai-agent/internal/ollama", "meta": { "exported": true, - "line": 62, + "line": 63, + "path": "services/agent/internal/ollama/client.go" + } + }, + { + "id": "function:github.com/example/glpi-ai-agent/internal/ollama:Client.chatOptions", + "kind": "function", + "label": "Client.chatOptions", + "group": "engineering", + "community": "github.com/example/glpi-ai-agent/internal/ollama", + "meta": { + "exported": false, + "line": 74, "path": "services/agent/internal/ollama/client.go" } }, @@ -5704,7 +5728,7 @@ "community": "github.com/example/glpi-ai-agent/internal/ollama", "meta": { "exported": false, - "line": 79, + "line": 94, "path": "services/agent/internal/ollama/client.go" } }, @@ -5716,7 +5740,7 @@ "community": "github.com/example/glpi-ai-agent/internal/ollama", "meta": { "exported": false, - "line": 126, + "line": 141, "path": "services/agent/internal/ollama/client.go" } }, @@ -5728,7 +5752,7 @@ "community": "github.com/example/glpi-ai-agent/internal/ollama", "meta": { "exported": false, - "line": 349, + "line": 373, "path": "services/agent/internal/ollama/client.go" } }, @@ -5740,7 +5764,7 @@ "community": "github.com/example/glpi-ai-agent/internal/ollama", "meta": { "exported": false, - "line": 627, + "line": 672, "path": "services/agent/internal/ollama/client.go" } }, @@ -5752,7 +5776,7 @@ "community": "github.com/example/glpi-ai-agent/internal/ollama", "meta": { "exported": false, - "line": 471, + "line": 516, "path": "services/agent/internal/ollama/client.go" } }, @@ -5764,7 +5788,7 @@ "community": "github.com/example/glpi-ai-agent/internal/ollama", "meta": { "exported": true, - "line": 27, + "line": 28, "path": "services/agent/internal/ollama/client.go" } }, @@ -5776,7 +5800,7 @@ "community": "github.com/example/glpi-ai-agent/internal/ollama", "meta": { "exported": true, - "line": 54, + "line": 55, "path": "services/agent/internal/ollama/client.go" } }, @@ -5788,7 +5812,7 @@ "community": "github.com/example/glpi-ai-agent/internal/ollama", "meta": { "exported": true, - "line": 273, + "line": 274, "path": "services/agent/internal/ollama/pool.go" } }, @@ -5800,7 +5824,7 @@ "community": "github.com/example/glpi-ai-agent/internal/ollama", "meta": { "exported": true, - "line": 263, + "line": 264, "path": "services/agent/internal/ollama/pool.go" } }, @@ -5812,7 +5836,7 @@ "community": "github.com/example/glpi-ai-agent/internal/ollama", "meta": { "exported": true, - "line": 244, + "line": 245, "path": "services/agent/internal/ollama/pool.go" } }, @@ -5824,7 +5848,7 @@ "community": "github.com/example/glpi-ai-agent/internal/ollama", "meta": { "exported": false, - "line": 677, + "line": 678, "path": "services/agent/internal/ollama/pool.go" } }, @@ -5836,7 +5860,7 @@ "community": "github.com/example/glpi-ai-agent/internal/ollama", "meta": { "exported": false, - "line": 389, + "line": 390, "path": "services/agent/internal/ollama/pool.go" } }, @@ -5848,7 +5872,7 @@ "community": "github.com/example/glpi-ai-agent/internal/ollama", "meta": { "exported": false, - "line": 551, + "line": 552, "path": "services/agent/internal/ollama/pool.go" } }, @@ -5860,7 +5884,7 @@ "community": "github.com/example/glpi-ai-agent/internal/ollama", "meta": { "exported": false, - "line": 606, + "line": 607, "path": "services/agent/internal/ollama/pool.go" } }, @@ -5872,7 +5896,7 @@ "community": "github.com/example/glpi-ai-agent/internal/ollama", "meta": { "exported": false, - "line": 455, + "line": 456, "path": "services/agent/internal/ollama/pool.go" } }, @@ -5884,7 +5908,7 @@ "community": "github.com/example/glpi-ai-agent/internal/ollama", "meta": { "exported": false, - "line": 281, + "line": 282, "path": "services/agent/internal/ollama/pool.go" } }, @@ -5896,7 +5920,7 @@ "community": "github.com/example/glpi-ai-agent/internal/ollama", "meta": { "exported": false, - "line": 587, + "line": 588, "path": "services/agent/internal/ollama/pool.go" } }, @@ -5908,7 +5932,7 @@ "community": "github.com/example/glpi-ai-agent/internal/ollama", "meta": { "exported": false, - "line": 690, + "line": 691, "path": "services/agent/internal/ollama/pool.go" } }, @@ -5920,7 +5944,7 @@ "community": "github.com/example/glpi-ai-agent/internal/ollama", "meta": { "exported": true, - "line": 784, + "line": 785, "path": "services/agent/internal/ollama/pool.go" } }, @@ -5932,7 +5956,7 @@ "community": "github.com/example/glpi-ai-agent/internal/ollama", "meta": { "exported": true, - "line": 741, + "line": 742, "path": "services/agent/internal/ollama/pool.go" } }, @@ -5944,7 +5968,7 @@ "community": "github.com/example/glpi-ai-agent/internal/ollama", "meta": { "exported": false, - "line": 370, + "line": 371, "path": "services/agent/internal/ollama/pool.go" } }, @@ -5956,7 +5980,7 @@ "community": "github.com/example/glpi-ai-agent/internal/ollama", "meta": { "exported": false, - "line": 582, + "line": 627, "path": "services/agent/internal/ollama/client.go" } }, @@ -5968,7 +5992,7 @@ "community": "github.com/example/glpi-ai-agent/internal/ollama", "meta": { "exported": false, - "line": 719, + "line": 720, "path": "services/agent/internal/ollama/pool.go" } }, @@ -5980,7 +6004,7 @@ "community": "github.com/example/glpi-ai-agent/internal/ollama", "meta": { "exported": false, - "line": 140, + "line": 155, "path": "services/agent/internal/ollama/client.go" } }, @@ -5992,7 +6016,7 @@ "community": "github.com/example/glpi-ai-agent/internal/ollama", "meta": { "exported": false, - "line": 703, + "line": 704, "path": "services/agent/internal/ollama/pool.go" } }, @@ -6004,7 +6028,7 @@ "community": "github.com/example/glpi-ai-agent/internal/ollama", "meta": { "exported": false, - "line": 773, + "line": 774, "path": "services/agent/internal/ollama/pool.go" } }, @@ -6016,7 +6040,7 @@ "community": "github.com/example/glpi-ai-agent/internal/ollama", "meta": { "exported": false, - "line": 725, + "line": 726, "path": "services/agent/internal/ollama/pool.go" } }, @@ -6028,7 +6052,7 @@ "community": "github.com/example/glpi-ai-agent/internal/ollama", "meta": { "exported": false, - "line": 440, + "line": 441, "path": "services/agent/internal/ollama/pool.go" } }, @@ -6040,7 +6064,7 @@ "community": "github.com/example/glpi-ai-agent/internal/ollama", "meta": { "exported": false, - "line": 162, + "line": 163, "path": "services/agent/internal/ollama/pool.go" } }, @@ -6052,7 +6076,7 @@ "community": "github.com/example/glpi-ai-agent/internal/ollama", "meta": { "exported": false, - "line": 592, + "line": 637, "path": "services/agent/internal/ollama/client.go" } }, @@ -6064,7 +6088,7 @@ "community": "github.com/example/glpi-ai-agent/internal/ollama", "meta": { "exported": false, - "line": 616, + "line": 661, "path": "services/agent/internal/ollama/client.go" } }, @@ -6076,7 +6100,7 @@ "community": "github.com/example/glpi-ai-agent/internal/ollama", "meta": { "exported": false, - "line": 713, + "line": 714, "path": "services/agent/internal/ollama/pool.go" } }, @@ -6088,7 +6112,7 @@ "community": "github.com/example/glpi-ai-agent/internal/ollama", "meta": { "exported": false, - "line": 79, + "line": 80, "path": "services/agent/internal/ollama/pool.go" } }, @@ -6100,7 +6124,7 @@ "community": "github.com/example/glpi-ai-agent/internal/ollama", "meta": { "exported": false, - "line": 542, + "line": 543, "path": "services/agent/internal/ollama/pool.go" } }, @@ -6112,7 +6136,7 @@ "community": "github.com/example/glpi-ai-agent/internal/ollama", "meta": { "exported": false, - "line": 93, + "line": 94, "path": "services/agent/internal/ollama/pool.go" } }, @@ -6124,7 +6148,7 @@ "community": "github.com/example/glpi-ai-agent/internal/ollama", "meta": { "exported": false, - "line": 105, + "line": 106, "path": "services/agent/internal/ollama/pool.go" } }, @@ -6136,7 +6160,7 @@ "community": "github.com/example/glpi-ai-agent/internal/ollama", "meta": { "exported": false, - "line": 88, + "line": 89, "path": "services/agent/internal/ollama/pool.go" } }, @@ -6148,7 +6172,7 @@ "community": "github.com/example/glpi-ai-agent/internal/ollama", "meta": { "exported": false, - "line": 136, + "line": 137, "path": "services/agent/internal/ollama/pool.go" } }, @@ -6160,7 +6184,7 @@ "community": "github.com/example/glpi-ai-agent/internal/ollama", "meta": { "exported": false, - "line": 755, + "line": 756, "path": "services/agent/internal/ollama/pool.go" } }, @@ -6172,7 +6196,7 @@ "community": "github.com/example/glpi-ai-agent/internal/ollama", "meta": { "exported": false, - "line": 750, + "line": 751, "path": "services/agent/internal/ollama/pool.go" } }, @@ -6184,7 +6208,7 @@ "community": "github.com/example/glpi-ai-agent/internal/ollama", "meta": { "exported": false, - "line": 580, + "line": 581, "path": "services/agent/internal/ollama/pool.go" } }, @@ -6196,7 +6220,7 @@ "community": "github.com/example/glpi-ai-agent/internal/ollama", "meta": { "exported": false, - "line": 150, + "line": 165, "path": "services/agent/internal/ollama/client.go" } }, @@ -6208,7 +6232,7 @@ "community": "github.com/example/glpi-ai-agent/internal/ollama", "meta": { "exported": false, - "line": 565, + "line": 610, "path": "services/agent/internal/ollama/client.go" } }, @@ -6220,7 +6244,7 @@ "community": "github.com/example/glpi-ai-agent/internal/ollama", "meta": { "exported": false, - "line": 168, + "line": 183, "path": "services/agent/internal/ollama/client.go" } }, @@ -6232,7 +6256,7 @@ "community": "github.com/example/glpi-ai-agent/internal/ollama", "meta": { "exported": false, - "line": 746, + "line": 747, "path": "services/agent/internal/ollama/pool.go" } }, @@ -32829,12 +32853,24 @@ "to": "function:github.com/example/glpi-ai-agent/internal/ollama:Client.RoutingMode", "kind": "defines" }, + { + "id": "file:services/agent/internal/ollama/client.go-\u003efunction:github.com/example/glpi-ai-agent/internal/ollama:Client.SetNumCtx:defines", + "from": "file:services/agent/internal/ollama/client.go", + "to": "function:github.com/example/glpi-ai-agent/internal/ollama:Client.SetNumCtx", + "kind": "defines" + }, { "id": "file:services/agent/internal/ollama/client.go-\u003efunction:github.com/example/glpi-ai-agent/internal/ollama:Client.Start:defines", "from": "file:services/agent/internal/ollama/client.go", "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.chatOptions:defines", + "from": "file:services/agent/internal/ollama/client.go", + "to": "function:github.com/example/glpi-ai-agent/internal/ollama:Client.chatOptions", + "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", @@ -43467,6 +43503,12 @@ "kind": "calls_package", "label": "Marshal" }, + { + "id": "function:github.com/example/glpi-ai-agent/internal/ollama:Client.Analyse-\u003efunction:github.com/example/glpi-ai-agent/internal/ollama:Client.chatOptions:calls", + "from": "function:github.com/example/glpi-ai-agent/internal/ollama:Client.Analyse", + "to": "function:github.com/example/glpi-ai-agent/internal/ollama:Client.chatOptions", + "kind": "calls" + }, { "id": "function:github.com/example/glpi-ai-agent/internal/ollama:Client.Analyse-\u003efunction:github.com/example/glpi-ai-agent/internal/ollama:Client.post:calls", "from": "function:github.com/example/glpi-ai-agent/internal/ollama:Client.Analyse", @@ -43506,6 +43548,12 @@ "kind": "calls_package", "label": "TrimSpace" }, + { + "id": "function:github.com/example/glpi-ai-agent/internal/ollama:Client.AnalyseCategory-\u003efunction:github.com/example/glpi-ai-agent/internal/ollama:Client.chatOptions:calls", + "from": "function:github.com/example/glpi-ai-agent/internal/ollama:Client.AnalyseCategory", + "to": "function:github.com/example/glpi-ai-agent/internal/ollama:Client.chatOptions", + "kind": "calls" + }, { "id": "function:github.com/example/glpi-ai-agent/internal/ollama:Client.AnalyseCategory-\u003efunction:github.com/example/glpi-ai-agent/internal/ollama:Client.executeDecision:calls", "from": "function:github.com/example/glpi-ai-agent/internal/ollama:Client.AnalyseCategory", @@ -43532,6 +43580,12 @@ "kind": "calls_package", "label": "Sprintf" }, + { + "id": "function:github.com/example/glpi-ai-agent/internal/ollama:Client.AnalyseEscalation-\u003efunction:github.com/example/glpi-ai-agent/internal/ollama:Client.chatOptions:calls", + "from": "function:github.com/example/glpi-ai-agent/internal/ollama:Client.AnalyseEscalation", + "to": "function:github.com/example/glpi-ai-agent/internal/ollama:Client.chatOptions", + "kind": "calls" + }, { "id": "function:github.com/example/glpi-ai-agent/internal/ollama:Client.AnalyseEscalation-\u003efunction:github.com/example/glpi-ai-agent/internal/ollama:Client.executeStructured:calls", "from": "function:github.com/example/glpi-ai-agent/internal/ollama:Client.AnalyseEscalation", @@ -43595,6 +43649,12 @@ "kind": "calls_package", "label": "NormalizeReasonCodes" }, + { + "id": "function:github.com/example/glpi-ai-agent/internal/ollama:Client.AnalysePriority-\u003efunction:github.com/example/glpi-ai-agent/internal/ollama:Client.chatOptions:calls", + "from": "function:github.com/example/glpi-ai-agent/internal/ollama:Client.AnalysePriority", + "to": "function:github.com/example/glpi-ai-agent/internal/ollama:Client.chatOptions", + "kind": "calls" + }, { "id": "function:github.com/example/glpi-ai-agent/internal/ollama:Client.AnalysePriority-\u003efunction:github.com/example/glpi-ai-agent/internal/ollama:Client.executeStructured:calls", "from": "function:github.com/example/glpi-ai-agent/internal/ollama:Client.AnalysePriority", @@ -43641,6 +43701,12 @@ "kind": "calls_package", "label": "Extract" }, + { + "id": "function:github.com/example/glpi-ai-agent/internal/ollama:Client.AnalyseReply-\u003efunction:github.com/example/glpi-ai-agent/internal/ollama:Client.chatOptions:calls", + "from": "function:github.com/example/glpi-ai-agent/internal/ollama:Client.AnalyseReply", + "to": "function:github.com/example/glpi-ai-agent/internal/ollama:Client.chatOptions", + "kind": "calls" + }, { "id": "function:github.com/example/glpi-ai-agent/internal/ollama:Client.AnalyseReply-\u003efunction:github.com/example/glpi-ai-agent/internal/ollama:Client.executeDecision:calls", "from": "function:github.com/example/glpi-ai-agent/internal/ollama:Client.AnalyseReply", @@ -43680,6 +43746,12 @@ "kind": "calls_package", "label": "TrimSpace" }, + { + "id": "function:github.com/example/glpi-ai-agent/internal/ollama:Client.AnalyseStatus-\u003efunction:github.com/example/glpi-ai-agent/internal/ollama:Client.chatOptions:calls", + "from": "function:github.com/example/glpi-ai-agent/internal/ollama:Client.AnalyseStatus", + "to": "function:github.com/example/glpi-ai-agent/internal/ollama:Client.chatOptions", + "kind": "calls" + }, { "id": "function:github.com/example/glpi-ai-agent/internal/ollama:Client.AnalyseStatus-\u003efunction:github.com/example/glpi-ai-agent/internal/ollama:Client.post:calls", "from": "function:github.com/example/glpi-ai-agent/internal/ollama:Client.AnalyseStatus", @@ -43819,6 +43891,12 @@ "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.chatOptions:calls", + "from": "function:github.com/example/glpi-ai-agent/internal/ollama:Client.executeDecision", + "to": "function:github.com/example/glpi-ai-agent/internal/ollama:Client.chatOptions", + "kind": "calls" + }, { "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", @@ -43839,6 +43917,12 @@ "kind": "calls_package", "label": "Errorf" }, + { + "id": "function:github.com/example/glpi-ai-agent/internal/ollama:Client.executeStructured-\u003efunction:github.com/example/glpi-ai-agent/internal/ollama:Client.chatOptions:calls", + "from": "function:github.com/example/glpi-ai-agent/internal/ollama:Client.executeStructured", + "to": "function:github.com/example/glpi-ai-agent/internal/ollama:Client.chatOptions", + "kind": "calls" + }, { "id": "function:github.com/example/glpi-ai-agent/internal/ollama:Client.executeStructured-\u003efunction:github.com/example/glpi-ai-agent/internal/ollama:Client.post:calls", "from": "function:github.com/example/glpi-ai-agent/internal/ollama:Client.executeStructured", @@ -67192,10 +67276,10 @@ } ], "meta": { - "edges": 7306, + "edges": 7316, "format_version": 1, "generator": "go-ast+compose", "modules": 4, - "nodes": 1855 + "nodes": 1857 } }