Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2026-08-12 09:36:15

0001 """
0002 Snapper temporal-query MCP tools — thin wrappers over snapper_ai.queries.
0003 
0004 Snapper records immutable, coherent snapshots ("snaps") of system-wide
0005 state at aligned opportunities, turning "what did the system look like
0006 at time T" from an inference problem into a retrieval problem. These
0007 tools return the typed evidence envelope unchanged; they never infer
0008 continuity that was not observed.
0009 """
0010 
0011 from django.utils.dateparse import parse_datetime
0012 
0013 from asgiref.sync import sync_to_async
0014 
0015 from monitor_app.mcp import mcp
0016 from snapper_ai import queries
0017 
0018 
0019 def _time(raw, label):
0020     value = parse_datetime(str(raw or '').strip())
0021     if value is None or value.tzinfo is None:
0022         raise queries.InvalidQuery(
0023             f"{label} must be ISO 8601 with timezone, "
0024             f"e.g. '2026-07-23T04:00:00Z'")
0025     return value
0026 
0027 
0028 def _call(fn):
0029     try:
0030         return fn().as_dict()
0031     except queries.SnapperError as e:
0032         return {'error': str(e)}
0033 
0034 
0035 @mcp.tool()
0036 async def snapper_latest(scope: str) -> dict:
0037     """
0038     Latest recorded system state for one Snapper scope.
0039 
0040     Use this to know what the whole system looked like at the most recent
0041     coherent capture — component states with their assessment times, not
0042     live probes.
0043 
0044     Args:
0045         scope: 'epicprod' (PanDA/production) or 'testbed'.
0046 
0047     Reading the result (shared by all snapper_* tools):
0048         state: the recorded component state documents, each with its own
0049             assessment time and, when applicable, source time.
0050         snap_time: when the returned state was ACTUALLY captured — always
0051             report this actual time.
0052         coverage: 'covered' means the observer was recording; 'gap' means
0053             a known observation gap (state across it is unknown);
0054             'unknown' means the evidence cannot say. Never treat gap or
0055             unknown intervals as if the last state persisted through them.
0056         Schema/policy versions, hashes, and provenance identify how to
0057             interpret each snap; old snaps keep the shape that was true
0058             when captured.
0059     """
0060     return await sync_to_async(_call)(lambda: queries.latest(scope))
0061 
0062 
0063 @mcp.tool()
0064 async def snapper_state_at(scope: str, time: str) -> dict:
0065     """
0066     Recorded system state at (or last before) a past instant.
0067 
0068     Use this for questions like "what was running when the incident
0069     began?". The answer is the latest snap at or before the requested
0070     time, returned with its ACTUAL snap time and honest coverage — it
0071     does not pretend the state was observed at the requested instant.
0072 
0073     Args:
0074         scope: 'epicprod' (PanDA/production) or 'testbed'.
0075         time: the past instant, ISO 8601 with timezone, e.g.
0076             '2026-07-22T14:30:00Z'.
0077 
0078     Reading the result: state documents plus snap_time (the actual
0079     capture time, possibly earlier than requested) and coverage —
0080     'covered', 'gap' (known observation gap; state across it is
0081     unknown), or 'unknown'. Never present state across a gap or unknown
0082     interval as observed fact.
0083     """
0084     return await sync_to_async(_call)(
0085         lambda: queries.state_at(scope, _time(time, 'time')))
0086 
0087 
0088 @mcp.tool()
0089 async def snapper_component_history(
0090     scope: str,
0091     component: str,
0092     start: str,
0093     end: str,
0094     include_unchanged: bool = False,
0095 ) -> dict:
0096     """
0097     One component's evolution over an interval.
0098 
0099     The first entry is the component's state at the interval start (with
0100     its actual snap time); subsequent entries are recorded changes.
0101     Absence and appearance are explicit; recovery evidence is never
0102     suppressed.
0103 
0104     Args:
0105         scope: 'epicprod' (PanDA/production) or 'testbed'.
0106         component: registered component name — 'health' (either scope),
0107             'datataking' (testbed), or 'panda' (epicprod).
0108         start: interval start, ISO 8601 with timezone.
0109         end: interval end, ISO 8601 with timezone.
0110         include_unchanged: also return semantically unchanged baseline
0111             entries (default False).
0112 
0113     Reading the result: entries carry actual snap times, content hashes,
0114     revisions, and schema versions; coverage is reported at both
0115     requested endpoints ('covered', 'gap', or 'unknown'). Never treat a
0116     gap or unknown interval as continuity of the last recorded value.
0117     """
0118     return await sync_to_async(_call)(
0119         lambda: queries.component_history(
0120             scope, component, _time(start, 'start'), _time(end, 'end'),
0121             suppress_unchanged_baselines=not include_unchanged))
0122 
0123 
0124 @mcp.tool()
0125 async def snapper_context_around(scope: str, time: str,
0126                                  window_seconds: float = 3600) -> dict:
0127     """
0128     Full temporal context at an instant: coherent state, nearby changes,
0129     and resolvable references to the exact event streams.
0130 
0131     Use this first when investigating an incident time: it returns the
0132     recorded system state at the instant, every component change in the
0133     window around it, and for each component a reference naming the
0134     authoritative service (REST URL and MCP tools in the reference's
0135     transport field) that holds the exact transitions — drill down
0136     there for event-level truth.
0137 
0138     Args:
0139         scope: 'epicprod' (PanDA/production) or 'testbed'.
0140         time: the instant, ISO 8601 with timezone.
0141         window_seconds: window centered on the instant (default 3600).
0142 
0143     Reading the result: state carries its ACTUAL snap time and coverage
0144     ('covered', 'gap', 'unknown' — never infer continuity across gap or
0145     unknown intervals); references carry availability and a transport
0146     with rest_url, rest_params, and mcp_tools naming exactly how to
0147     fetch the underlying events.
0148     """
0149     from monitor_app.snapper_resolvers import annotate_references
0150 
0151     def call():
0152         result = queries.context_around(
0153             scope, _time(time, 'time'), window_seconds).as_dict()
0154         result['references'] = annotate_references(result['references'])
0155         return result
0156 
0157     def guarded():
0158         try:
0159             return call()
0160         except queries.SnapperError as e:
0161             return {'error': str(e)}
0162 
0163     return await sync_to_async(guarded)()
0164 
0165 
0166 @mcp.tool()
0167 async def snapper_changes_between(scope: str, start: str, end: str) -> dict:
0168     """
0169     What changed across the whole system between two moments.
0170 
0171     Every component difference is classified added, changed, or removed,
0172     with previous and current documents, hashes, and versions.
0173     Value-identical baselines are omitted; recovery and capture-policy
0174     transitions remain as evidence.
0175 
0176     Args:
0177         scope: 'epicprod' (PanDA/production) or 'testbed'.
0178         start: comparison boundary, ISO 8601 with timezone.
0179         end: interval end, ISO 8601 with timezone.
0180 
0181     Reading the result: the comparison boundary snap and its actual time
0182     are returned with the changes; coverage is reported at both
0183     requested endpoints ('covered', 'gap', or 'unknown'). Never treat a
0184     gap or unknown interval as if nothing changed within it.
0185 
0186     Counting job outcomes over an interval: the epicprod panda
0187     component carries monotonic cumulative terminal-job counters —
0188     jobs.cum (finished, failed, cancelled, closed) and, per site,
0189     jobs.sites.<site>.cum plus jobs.sites.<site>.cum_failed_by_class
0190     (error component classes). Subtract the counter at start from the
0191     counter at end to count that interval's outcomes, e.g. how many
0192     jobs finished and failed at one site during a production test and
0193     which failure classes dominated.
0194     """
0195     return await sync_to_async(_call)(
0196         lambda: queries.changes_between(
0197             scope, _time(start, 'start'), _time(end, 'end')))