update-1.6.2c
ci / test (services/control) (push) Successful in 49s
ci / test (services/knowledge) (push) Successful in 50s
ci / test (platform/neuroforge) (push) Successful in 54s
ci / test (services/agent) (push) Successful in 1m0s
ci / docker-build (push) Failing after 1m10s
release-tag / release-image (push) Successful in 6m33s

This commit is contained in:
2026-09-09 15:27:42 +02:00
parent f776b2322a
commit faa416ac89
20 changed files with 352 additions and 125 deletions
+4
View File
@@ -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.
@@ -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. |
@@ -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. |
+3 -1
View File
@@ -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
```
+1
View File
@@ -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
+2 -1
View File
@@ -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)
@@ -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}
+1
View File
@@ -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}
+5
View File
@@ -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")
}
@@ -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"
+1
View File
@@ -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.
+68 -14
View File
@@ -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
@@ -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: `<p>RICH_SECRET_MARKUP</p>`}}}
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 {
+8 -7
View File
@@ -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)
+1 -1
View File
@@ -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,
+166 -82
View File
@@ -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
}
}