{"title":"O1 Insight Agent — rApp integration guide","role":"This service is a read-only observability agent, not an rApp. It polls the O1 simulator, runs deterministic anomaly rules, and asks an LLM for root-cause reasoning. It NEVER writes to the O-RAN network, calls no mutating O1-sim endpoint, and has no action/command endpoint of its own — every route under /api/agent/* is a GET. All decision-making, policy, and any SMO/O1 write operation belongs entirely to the downstream rApp consuming this feed.","standards_alignment":"This agent is R1-service-shaped, not R1-compliant — no O-RAN spec currently defines a canonical rApp analytics output format the way O1 defines YANG models or A1 defines policy types, so there is no fixed schema to claim conformance to. What we do claim: this service occupies the position an rApp's analytics/data-consumption layer would occupy relative to the non-RT RIC/SMO, the same position R1 exists to serve — polling read-only fault/PM data, applying analytics, and publishing structured insights for a consumer to poll, with zero write path back into the network (no O1, A1, or O2 write call anywhere in this service). It is deliberately scoped as the analysis layer only; decision-making, policy, and any actual O1/A1/O2 write belongs entirely to the downstream rApp or non-RT RIC function consuming this feed, matching the standard's separation between analytics and control. Treat 'R1-service-shaped' as 'modeled on the pattern', not as an interoperability guarantee against a real R1 implementation.","rate_limits":"Every /api/agent/* route (including the SSE stream's connection attempts) is capped at 120 requests/minute per client IP — a sliding window, not a fixed per-clock-minute bucket. Exceeding it returns HTTP 429 with a Retry-After: 60 header and a plain-JSON {'detail': ...} body; back off and retry rather than hammering it, the count doesn't reset early. This is comfortably above the recommended integration pattern below (poll roughly as often as this agent's own 15s cycle), so a well-behaved rApp should never see one. The live SSE stream additionally caps concurrent connections at 3 per client IP (50 globally) — HTTP 503 if you exceed it — since each open connection holds real server-side state for as long as it stays open, unlike a one-shot GET.","feed_endpoints":{"list":{"method":"GET","path":"/api/agent/insights","query_params":{"site_id":"optional, exact match","severity":"optional, one of WARNING|MINOR|MAJOR|CRITICAL","limit":"optional, default 50, max 200"},"ordering":"most-recent-first"},"detail":{"method":"GET","path":"/api/agent/insights/{insight_id}","notes":"404 once the record has been evicted from the in-memory ring buffer."},"live_activity":{"method":"GET","path":"/api/agent/events/stream","notes":"SSE stream of pipeline stage events (poll/evaluate/reason/synthesize/publish). Optional — useful for visibility/debugging, not required to consume insights."}},"persistence":"In-memory ring buffer only, capped, resets on service restart. Poll on an interval rather than relying on this service to retain long-term history — an rApp that needs durable history should persist insights on its own side.","record_schema":{"insight_id":"unique id for this insight","correlation_id":"ties this insight back to the poll cycle that produced it (see /api/agent/events)","created_at":"ISO 8601 UTC timestamp","site_id":"O-DU site, format '<conn_id>::<gnbdu-suffix>', e.g. 'local::3'; null if not resolvable","cell_local_id":"NRCellDU local id, when the triggering evidence was cell-scoped; null for site-wide signals","triggering_rule_ids":"deterministic rule id(s) that flagged this — always present, independent of LLM availability","severity":"WARNING|MINOR|MAJOR|CRITICAL — derived from the underlying rule(s), not from the LLM","summary":"short human-readable description of the triggering rule(s)","root_cause_narrative":"LLM hypothesis for root cause; null if llm_status != 'ok'","confidence":"LLM-reported confidence 0-1; null if llm_status != 'ok'","contributing_factors":"LLM-listed supporting evidence points; empty if llm_status != 'ok'","suggested_focus_areas":"things worth reviewing or investigating further, phrased as review suggestions — never as actions, commands, or config changes. This is intentional: interpreting these into an actual remediation is the rApp's job, not this agent's.","llm_status":"ok|unavailable|timeout|error|omitted — see below","evidence":"raw evidence dict keyed by rule_id (alarm fields and/or PM trend data) that the rule and LLM reasoned over","provenance":"optional YANG source-leaf citation from /api/pm/trace, when available","related_to_insight_id":"set when this insight concerns alarm(s) already covered by an earlier insight — each of this sim's alarm IDs is permanently pinned to one fixed site/cell for its lifetime, so a re-raise or severity change of the same alarm_id is genuinely the same underlying issue, not a new one. Either a fresh escalation-driven re-reasoning (llm_status='ok', summary prefixed '[continues insight <id>]') or a same/lower-severity re-raise that skipped a fresh LLM call entirely (llm_status='omitted') — check llm_status to tell which. null when this is a genuinely new signal."},"dedup_ttl":"A same/lower-severity re-raise of an already-explained signal (alarm_id, or site+PM-rule) is skipped rather than re-billed against the LLM budget — but only for a bounded window (see gemini_dedup_ttl_seconds, default 1h server-side). Once that window elapses the signal is treated as stale and reasoned about again from scratch on its next occurrence, even with no severity change, so root_cause_narrative reflects reasoning no older than that window rather than a one-time explanation frozen from hours or days earlier. Check root_cause_narrative alongside the insight's created_at / the prior insight's created_at (via related_to_insight_id) if your rApp cares about how fresh a given narrative is.","llm_status_handling":"llm_status is not a health flag to ignore — it tells you whether root_cause_narrative and suggested_focus_areas are populated. 'ok' means Gemini reasoning succeeded. 'timeout'/'error'/'unavailable' (e.g. free-tier quota exhausted) means the anomaly is still real and rule-flagged — severity, triggering_rule_ids and evidence are always present regardless — but no LLM narrative was produced this cycle. A downstream rApp should be able to act on the deterministic fields alone and treat the LLM fields as an optional enrichment, not a dependency.","grouping":"One insight can cover more than one rule firing: within a single poll cycle, all anomalies flagged for the same site are merged into one insight and one Gemini call, rather than reasoning about each rule separately. len(triggering_rule_ids) > 1 is how you tell this happened — evidence is keyed by rule_id, so each contributing rule's raw evidence is still visible individually even though the narrative reasons about them together. severity is the max across the merged rules; cell_local_id is only set when every merged anomaly shares the same cell, null otherwise. Anomalies at different sites in the same poll cycle are never merged, and always get separate insights (see /api/agent/events for the correlation_id chain showing how a poll cycle fanned out into however many site-scoped reason/synthesize/publish sequences it needed).","recommended_integration_pattern":["Poll GET /api/agent/insights on an interval at least as frequent as this agent's own poll cycle (default 15s).","Track insight_id to avoid reprocessing records already handled.","Correlate by site_id / cell_local_id / triggering_rule_ids against your own topology and policy model.","Treat root_cause_narrative, contributing_factors and suggested_focus_areas as hypotheses to weigh, not instructions to execute.","Decide remediation policy, thresholds for action, and any actual O1/NETCONF write entirely within the rApp — this agent will never do that on your behalf."]}