agentops-accelerator 0.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. agentops/__init__.py +10 -0
  2. agentops/__main__.py +6 -0
  3. agentops/agent/__init__.py +12 -0
  4. agentops/agent/_legacy_ids.py +92 -0
  5. agentops/agent/analyzer.py +207 -0
  6. agentops/agent/checks/__init__.py +1 -0
  7. agentops/agent/checks/catalog.py +880 -0
  8. agentops/agent/checks/errors.py +279 -0
  9. agentops/agent/checks/foundry_config.py +75 -0
  10. agentops/agent/checks/latency.py +84 -0
  11. agentops/agent/checks/opex.py +157 -0
  12. agentops/agent/checks/opex_workspace.py +874 -0
  13. agentops/agent/checks/posture.py +36 -0
  14. agentops/agent/checks/posture_rules/__init__.py +53 -0
  15. agentops/agent/checks/posture_rules/content_filter.py +59 -0
  16. agentops/agent/checks/posture_rules/diagnostics.py +74 -0
  17. agentops/agent/checks/posture_rules/local_auth.py +55 -0
  18. agentops/agent/checks/posture_rules/managed_identity.py +59 -0
  19. agentops/agent/checks/posture_rules/network.py +68 -0
  20. agentops/agent/checks/regression.py +78 -0
  21. agentops/agent/checks/release_readiness.py +182 -0
  22. agentops/agent/checks/safety.py +247 -0
  23. agentops/agent/checks/spec_conformance.py +375 -0
  24. agentops/agent/cockpit.py +5159 -0
  25. agentops/agent/config.py +240 -0
  26. agentops/agent/findings.py +113 -0
  27. agentops/agent/history.py +142 -0
  28. agentops/agent/knowledge/__init__.py +182 -0
  29. agentops/agent/knowledge/waf-checklist.csv +39 -0
  30. agentops/agent/llm_assist/__init__.py +16 -0
  31. agentops/agent/llm_assist/_base.py +124 -0
  32. agentops/agent/llm_assist/_bundle_rule.py +154 -0
  33. agentops/agent/llm_assist/_client.py +347 -0
  34. agentops/agent/llm_assist/_dataset_rules.py +191 -0
  35. agentops/agent/llm_assist/_engine.py +106 -0
  36. agentops/agent/llm_assist/_prompt_rules.py +291 -0
  37. agentops/agent/llm_assist/_spec_rules.py +235 -0
  38. agentops/agent/production_telemetry.py +430 -0
  39. agentops/agent/report.py +207 -0
  40. agentops/agent/server/__init__.py +1 -0
  41. agentops/agent/server/app.py +84 -0
  42. agentops/agent/server/auth.py +94 -0
  43. agentops/agent/server/chat.py +44 -0
  44. agentops/agent/server/protocol.py +72 -0
  45. agentops/agent/sources/__init__.py +1 -0
  46. agentops/agent/sources/azure_monitor.py +523 -0
  47. agentops/agent/sources/azure_resources.py +602 -0
  48. agentops/agent/sources/foundry_control.py +174 -0
  49. agentops/agent/sources/results_history.py +494 -0
  50. agentops/agent/sources/spec_detectors/__init__.py +42 -0
  51. agentops/agent/sources/spec_detectors/_base.py +58 -0
  52. agentops/agent/sources/spec_detectors/agents_md.py +75 -0
  53. agentops/agent/sources/spec_detectors/spec_kit.py +172 -0
  54. agentops/agent/time_range.py +117 -0
  55. agentops/cli/__init__.py +1 -0
  56. agentops/cli/app.py +4823 -0
  57. agentops/core/__init__.py +1 -0
  58. agentops/core/agentops_config.py +592 -0
  59. agentops/core/config_loader.py +22 -0
  60. agentops/core/evaluators.py +480 -0
  61. agentops/core/release_evidence.py +56 -0
  62. agentops/core/results.py +117 -0
  63. agentops/mcp/__init__.py +10 -0
  64. agentops/mcp/server.py +232 -0
  65. agentops/pipeline/__init__.py +8 -0
  66. agentops/pipeline/cloud_results.py +189 -0
  67. agentops/pipeline/cloud_runner.py +901 -0
  68. agentops/pipeline/comparison.py +108 -0
  69. agentops/pipeline/diagnostics.py +51 -0
  70. agentops/pipeline/invocations.py +535 -0
  71. agentops/pipeline/official_eval.py +414 -0
  72. agentops/pipeline/orchestrator.py +775 -0
  73. agentops/pipeline/prompt_deploy.py +377 -0
  74. agentops/pipeline/publisher.py +121 -0
  75. agentops/pipeline/reporter.py +202 -0
  76. agentops/pipeline/runtime.py +409 -0
  77. agentops/pipeline/thresholds.py +84 -0
  78. agentops/services/__init__.py +1 -0
  79. agentops/services/cicd.py +720 -0
  80. agentops/services/eval_analysis.py +848 -0
  81. agentops/services/evidence_pack.py +757 -0
  82. agentops/services/initializer.py +86 -0
  83. agentops/services/preflight.py +470 -0
  84. agentops/services/setup_wizard.py +709 -0
  85. agentops/services/skills.py +643 -0
  86. agentops/services/trace_promotion.py +300 -0
  87. agentops/services/workflow_analysis.py +1129 -0
  88. agentops/templates/.gitignore +15 -0
  89. agentops/templates/__init__.py +1 -0
  90. agentops/templates/agent-server/Dockerfile +23 -0
  91. agentops/templates/agent-server/README.md +61 -0
  92. agentops/templates/agent-server/main.bicep +94 -0
  93. agentops/templates/agent.yaml +87 -0
  94. agentops/templates/agentops.yaml +58 -0
  95. agentops/templates/foundry.svg +71 -0
  96. agentops/templates/icon.png +0 -0
  97. agentops/templates/pipelines/azuredevops/agentops-deploy-dev-azd.yml +118 -0
  98. agentops/templates/pipelines/azuredevops/agentops-deploy-dev.yml +73 -0
  99. agentops/templates/pipelines/azuredevops/agentops-deploy-prod-azd.yml +141 -0
  100. agentops/templates/pipelines/azuredevops/agentops-deploy-prod.yml +94 -0
  101. agentops/templates/pipelines/azuredevops/agentops-deploy-prompt-agent.yml +167 -0
  102. agentops/templates/pipelines/azuredevops/agentops-deploy-qa-azd.yml +118 -0
  103. agentops/templates/pipelines/azuredevops/agentops-deploy-qa.yml +68 -0
  104. agentops/templates/pipelines/azuredevops/agentops-pr-prompt-agent.yml +210 -0
  105. agentops/templates/pipelines/azuredevops/agentops-pr.yml +155 -0
  106. agentops/templates/pipelines/azuredevops/agentops-watchdog.yml +106 -0
  107. agentops/templates/project.gitignore +36 -0
  108. agentops/templates/sample-traces.jsonl +3 -0
  109. agentops/templates/skills/agentops-agent/SKILL.md +137 -0
  110. agentops/templates/skills/agentops-config/SKILL.md +113 -0
  111. agentops/templates/skills/agentops-dataset/SKILL.md +84 -0
  112. agentops/templates/skills/agentops-eval/SKILL.md +189 -0
  113. agentops/templates/skills/agentops-report/SKILL.md +71 -0
  114. agentops/templates/skills/agentops-workflow/SKILL.md +471 -0
  115. agentops/templates/smoke.jsonl +3 -0
  116. agentops/templates/waf-checklist.README.md +84 -0
  117. agentops/templates/waf-checklist.csv +22 -0
  118. agentops/templates/workflows/agentops-deploy-dev-azd.yml +166 -0
  119. agentops/templates/workflows/agentops-deploy-dev.yml +187 -0
  120. agentops/templates/workflows/agentops-deploy-prod-azd.yml +183 -0
  121. agentops/templates/workflows/agentops-deploy-prod.yml +171 -0
  122. agentops/templates/workflows/agentops-deploy-prompt-agent.yml +197 -0
  123. agentops/templates/workflows/agentops-deploy-qa-azd.yml +156 -0
  124. agentops/templates/workflows/agentops-deploy-qa.yml +145 -0
  125. agentops/templates/workflows/agentops-pr-prompt-agent.yml +210 -0
  126. agentops/templates/workflows/agentops-pr.yml +148 -0
  127. agentops/templates/workflows/agentops-watchdog.yml +122 -0
  128. agentops/utils/__init__.py +1 -0
  129. agentops/utils/azd_env.py +435 -0
  130. agentops/utils/azure_endpoints.py +62 -0
  131. agentops/utils/colors.py +47 -0
  132. agentops/utils/dotenv_loader.py +105 -0
  133. agentops/utils/foundry_discovery.py +229 -0
  134. agentops/utils/logging.py +59 -0
  135. agentops/utils/telemetry.py +554 -0
  136. agentops/utils/yaml.py +36 -0
  137. agentops_accelerator-0.3.0.dist-info/METADATA +278 -0
  138. agentops_accelerator-0.3.0.dist-info/RECORD +142 -0
  139. agentops_accelerator-0.3.0.dist-info/WHEEL +5 -0
  140. agentops_accelerator-0.3.0.dist-info/entry_points.txt +2 -0
  141. agentops_accelerator-0.3.0.dist-info/licenses/LICENSE +21 -0
  142. agentops_accelerator-0.3.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,58 @@
1
+ """Detector protocol and shared data classes for spec-conformance."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from datetime import datetime
7
+ from pathlib import Path
8
+ from typing import Dict, List, Optional, Protocol
9
+
10
+
11
+ @dataclass
12
+ class SpecTask:
13
+ """A single checklist item extracted from a spec document."""
14
+
15
+ text: str
16
+ checked: bool
17
+ line: int
18
+ mentioned_paths: List[str] = field(default_factory=list)
19
+
20
+
21
+ @dataclass
22
+ class SpecDocument:
23
+ """Unified view of a project's spec, populated by a :class:`Detector`.
24
+
25
+ The structure is intentionally narrow: every field is either a list
26
+ of strings (extracted via simple regex heuristics) or a primitive,
27
+ so detectors stay deterministic and easy to test.
28
+ """
29
+
30
+ format: str
31
+ root: Path
32
+ files: List[Path] = field(default_factory=list)
33
+ capabilities: List[str] = field(default_factory=list)
34
+ tasks: List[SpecTask] = field(default_factory=list)
35
+ references: Dict[str, List[str]] = field(default_factory=dict)
36
+ text: str = ""
37
+ last_modified: Optional[datetime] = None
38
+
39
+ @property
40
+ def is_empty(self) -> bool:
41
+ return not (self.capabilities or self.tasks or self.text.strip())
42
+
43
+
44
+ class Detector(Protocol):
45
+ """Protocol every spec-format detector must satisfy."""
46
+
47
+ name: str
48
+
49
+ def hint_paths(self, workspace: Path) -> List[Path]:
50
+ """Paths that, if present, suggest the user *intends* to use
51
+ this spec format even when the actual content is missing or
52
+ empty (e.g. ``.specify/`` directory with no spec.md). Used by
53
+ the doctor to fire a ``spec_missing`` finding instead of
54
+ silently skipping the check."""
55
+
56
+ def detect(self, workspace: Path) -> Optional[SpecDocument]:
57
+ """Inspect ``workspace`` and return a :class:`SpecDocument`
58
+ when this format is detected. Return ``None`` to opt out."""
@@ -0,0 +1,75 @@
1
+ """Detector for the AGENTS.md / copilot-instructions.md convention."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from datetime import datetime, timezone
7
+ from pathlib import Path
8
+ from typing import List, Optional
9
+
10
+ from agentops.agent.sources.spec_detectors._base import SpecDocument
11
+ from agentops.agent.sources.spec_detectors.spec_kit import (
12
+ _extract_capabilities,
13
+ _extract_references,
14
+ _extract_tasks,
15
+ )
16
+
17
+ _CANDIDATE_PATHS = (
18
+ "AGENTS.md",
19
+ ".github/copilot-instructions.md",
20
+ ".github/instructions.md",
21
+ "CLAUDE.md",
22
+ )
23
+
24
+
25
+ @dataclass
26
+ class AgentsMdDetector:
27
+ """Detector for AGENTS.md-style single-file spec conventions."""
28
+
29
+ name: str = "agents-md"
30
+
31
+ def hint_paths(self, workspace: Path) -> List[Path]:
32
+ return [workspace / p for p in _CANDIDATE_PATHS if (workspace / p).exists()]
33
+
34
+ def detect(self, workspace: Path) -> Optional[SpecDocument]:
35
+ files: List[Path] = []
36
+ chunks: List[str] = []
37
+ latest_mtime: Optional[float] = None
38
+
39
+ for relative in _CANDIDATE_PATHS:
40
+ p = workspace / relative
41
+ if not p.is_file():
42
+ continue
43
+ files.append(p)
44
+ try:
45
+ text = p.read_text(encoding="utf-8", errors="replace")
46
+ except OSError:
47
+ continue
48
+ chunks.append(f"# === {relative} ===\n{text}")
49
+ try:
50
+ mtime = p.stat().st_mtime
51
+ if latest_mtime is None or mtime > latest_mtime:
52
+ latest_mtime = mtime
53
+ except OSError:
54
+ pass
55
+
56
+ if not files:
57
+ return None
58
+
59
+ merged = "\n\n".join(chunks)
60
+ last_modified = (
61
+ datetime.fromtimestamp(latest_mtime, tz=timezone.utc)
62
+ if latest_mtime is not None
63
+ else None
64
+ )
65
+
66
+ return SpecDocument(
67
+ format=self.name,
68
+ root=workspace,
69
+ files=files,
70
+ capabilities=_extract_capabilities(merged),
71
+ tasks=_extract_tasks(merged),
72
+ references=_extract_references(merged),
73
+ text=merged,
74
+ last_modified=last_modified,
75
+ )
@@ -0,0 +1,172 @@
1
+ """Detector for the GitHub spec-kit layout (``.specify/``).
2
+
3
+ Reads ``spec.md``, ``plan.md``, and ``tasks.md`` from the ``.specify/``
4
+ directory at the workspace root. Capabilities are extracted from
5
+ markdown headings + leading bullet points; tasks come from
6
+ ``- [ ]`` / ``- [x]`` checklist items in ``tasks.md``. References
7
+ to evaluators, datasets, and agent ids are detected via backticked
8
+ identifiers and common AgentOps keywords.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import re
14
+ from dataclasses import dataclass
15
+ from datetime import datetime, timezone
16
+ from pathlib import Path
17
+ from typing import Dict, List, Optional
18
+
19
+ from agentops.agent.sources.spec_detectors._base import SpecDocument, SpecTask
20
+
21
+ _SPECIFY_DIR = ".specify"
22
+ _DOC_NAMES = ("spec.md", "plan.md", "tasks.md")
23
+
24
+ _HEADING_RE = re.compile(r"^(#{1,3})\s+(.+?)\s*$", re.MULTILINE)
25
+ _BULLET_RE = re.compile(r"^\s*[-*]\s+(.+?)\s*$", re.MULTILINE)
26
+ _TASK_RE = re.compile(r"^\s*[-*]\s+\[([ xX])\]\s+(.+?)\s*$")
27
+ _BACKTICK_RE = re.compile(r"`([^`\n]+)`")
28
+ _EVALUATOR_RE = re.compile(r"\b([A-Z][A-Za-z0-9]*Evaluator)\b")
29
+ _AGENT_ID_RE = re.compile(r"\b([a-z][a-z0-9_\-]*:[a-zA-Z0-9._\-]+)\b")
30
+ _PATH_RE = re.compile(r"`?(\.?/?[\w\-./]+\.(?:py|md|yaml|yml|jsonl|json|ts|tsx|js))`?")
31
+
32
+
33
+ @dataclass
34
+ class SpecKitDetector:
35
+ """Detector for GitHub spec-kit (``.specify/``) projects."""
36
+
37
+ name: str = "spec-kit"
38
+
39
+ def hint_paths(self, workspace: Path) -> List[Path]:
40
+ base = workspace / _SPECIFY_DIR
41
+ return [base] if base.exists() else []
42
+
43
+ def detect(self, workspace: Path) -> Optional[SpecDocument]:
44
+ base = workspace / _SPECIFY_DIR
45
+ if not base.is_dir():
46
+ return None
47
+
48
+ files: List[Path] = []
49
+ chunks: List[str] = []
50
+ latest_mtime: Optional[float] = None
51
+ tasks: List[SpecTask] = []
52
+
53
+ for name in _DOC_NAMES:
54
+ p = base / name
55
+ if not p.is_file():
56
+ continue
57
+ files.append(p)
58
+ try:
59
+ text = p.read_text(encoding="utf-8", errors="replace")
60
+ except OSError:
61
+ continue
62
+ chunks.append(f"# === {name} ===\n{text}")
63
+ try:
64
+ mtime = p.stat().st_mtime
65
+ if latest_mtime is None or mtime > latest_mtime:
66
+ latest_mtime = mtime
67
+ except OSError:
68
+ pass
69
+ if name == "tasks.md":
70
+ tasks.extend(_extract_tasks(text))
71
+
72
+ if not files:
73
+ return None
74
+
75
+ merged = "\n\n".join(chunks)
76
+ capabilities = _extract_capabilities(merged)
77
+ references = _extract_references(merged)
78
+ last_modified = (
79
+ datetime.fromtimestamp(latest_mtime, tz=timezone.utc)
80
+ if latest_mtime is not None
81
+ else None
82
+ )
83
+
84
+ return SpecDocument(
85
+ format=self.name,
86
+ root=base,
87
+ files=files,
88
+ capabilities=capabilities,
89
+ tasks=tasks,
90
+ references=references,
91
+ text=merged,
92
+ last_modified=last_modified,
93
+ )
94
+
95
+
96
+ def _extract_capabilities(text: str) -> List[str]:
97
+ """Heuristic capability extraction: top-2 heading levels + first-bullet items."""
98
+ caps: List[str] = []
99
+ seen: set[str] = set()
100
+ for match in _HEADING_RE.finditer(text):
101
+ level = len(match.group(1))
102
+ title = match.group(2).strip().rstrip("#").strip()
103
+ if level <= 2 and title and title.lower() not in seen:
104
+ caps.append(title)
105
+ seen.add(title.lower())
106
+ for match in _BULLET_RE.finditer(text):
107
+ item = match.group(1).strip()
108
+ if item.startswith("[") and "]" in item:
109
+ continue # task checklist item handled separately
110
+ key = item.lower()
111
+ if key not in seen and len(item) > 4:
112
+ caps.append(item)
113
+ seen.add(key)
114
+ return caps[:60]
115
+
116
+
117
+ def _extract_tasks(text: str) -> List[SpecTask]:
118
+ """Parse ``- [ ]`` / ``- [x]`` checklist items from ``tasks.md``."""
119
+ tasks: List[SpecTask] = []
120
+ for idx, raw_line in enumerate(text.splitlines(), start=1):
121
+ m = _TASK_RE.match(raw_line)
122
+ if not m:
123
+ continue
124
+ mark = m.group(1).lower()
125
+ item = m.group(2).strip()
126
+ paths = list(_PATH_RE.findall(item))
127
+ tasks.append(
128
+ SpecTask(
129
+ text=item,
130
+ checked=(mark == "x"),
131
+ line=idx,
132
+ mentioned_paths=[p for p in paths if p],
133
+ )
134
+ )
135
+ return tasks
136
+
137
+
138
+ def _extract_references(text: str) -> Dict[str, List[str]]:
139
+ """Collect evaluator class names, agent-id-like tokens, and backticked
140
+ identifiers that look like file / module / dataset references."""
141
+ refs: Dict[str, List[str]] = {
142
+ "evaluators": [],
143
+ "agent_ids": [],
144
+ "datasets": [],
145
+ "files": [],
146
+ }
147
+ seen_per_bucket: Dict[str, set[str]] = {k: set() for k in refs}
148
+
149
+ def _push(bucket: str, value: str) -> None:
150
+ v = value.strip()
151
+ if not v or v in seen_per_bucket[bucket]:
152
+ return
153
+ seen_per_bucket[bucket].add(v)
154
+ refs[bucket].append(v)
155
+
156
+ for m in _EVALUATOR_RE.finditer(text):
157
+ _push("evaluators", m.group(1))
158
+
159
+ for m in _AGENT_ID_RE.finditer(text):
160
+ token = m.group(1)
161
+ if token.startswith(("http", "https")) or token in {"int:0", "str:0"}:
162
+ continue
163
+ _push("agent_ids", token)
164
+
165
+ for m in _BACKTICK_RE.finditer(text):
166
+ token = m.group(1).strip()
167
+ if "/" in token and "." in token:
168
+ _push("files", token)
169
+ elif token.endswith(".jsonl") or token.endswith(".yaml") or token.endswith(".yml"):
170
+ _push("datasets", token)
171
+
172
+ return refs
@@ -0,0 +1,117 @@
1
+ """Time range parsing for the AgentOps cockpit.
2
+
3
+ The cockpit supports three preset windows (``1d``, ``7d``, ``30d``)
4
+ plus a ``custom`` mode driven by ``from`` / ``to`` ISO date strings.
5
+ The parsing is intentionally tolerant: any unknown value falls back to
6
+ the 7-day default so the cockpit never breaks on bad query strings.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from dataclasses import dataclass
12
+ from datetime import datetime, timedelta, timezone
13
+ from typing import Iterable, Optional
14
+
15
+
16
+ @dataclass(frozen=True)
17
+ class TimeRange:
18
+ """A resolved time window for filtering cockpit data."""
19
+
20
+ key: str # "1d" | "7d" | "30d" | "custom"
21
+ label: str # display label, e.g. "Last 24h"
22
+ start: datetime # UTC, inclusive
23
+ end: datetime # UTC, exclusive
24
+ hours: int # convenience: rounded duration in hours, used by KQL
25
+
26
+ def contains(self, ts: Optional[datetime]) -> bool:
27
+ """Return True if ``ts`` (UTC) falls inside this window."""
28
+ if ts is None:
29
+ return False
30
+ if ts.tzinfo is None:
31
+ ts = ts.replace(tzinfo=timezone.utc)
32
+ return self.start <= ts < self.end
33
+
34
+ def to_query(self) -> str:
35
+ """Return the URL query string that reproduces this range."""
36
+ if self.key == "custom":
37
+ return (
38
+ f"range=custom"
39
+ f"&from={self.start.strftime('%Y-%m-%d')}"
40
+ f"&to={self.end.strftime('%Y-%m-%d')}"
41
+ )
42
+ return f"range={self.key}"
43
+
44
+
45
+ _PRESET_HOURS = {
46
+ "1d": 24,
47
+ "7d": 24 * 7,
48
+ "30d": 24 * 30,
49
+ }
50
+
51
+ _PRESET_LABELS = {
52
+ "1d": "Last 24h",
53
+ "7d": "Last 7 days",
54
+ "30d": "Last 30 days",
55
+ }
56
+
57
+
58
+ def parse_time_range(
59
+ range_param: Optional[str] = None,
60
+ from_param: Optional[str] = None,
61
+ to_param: Optional[str] = None,
62
+ *,
63
+ now: Optional[datetime] = None,
64
+ ) -> TimeRange:
65
+ """Parse the URL ``range``, ``from``, ``to`` params into a TimeRange.
66
+
67
+ Falls back to the ``7d`` preset on any malformed input.
68
+ """
69
+ current = now or datetime.now(timezone.utc)
70
+ key = (range_param or "7d").lower().strip()
71
+
72
+ if key == "custom":
73
+ start = _parse_iso_date(from_param)
74
+ end = _parse_iso_date(to_param)
75
+ if start and end and end > start:
76
+ # Inclusive end-of-day for the "to" date.
77
+ end = end + timedelta(days=1)
78
+ hours = max(int((end - start).total_seconds() // 3600), 1)
79
+ return TimeRange(
80
+ key="custom",
81
+ label=f"{start.strftime('%Y-%m-%d')} → {end.strftime('%Y-%m-%d')}",
82
+ start=start,
83
+ end=end,
84
+ hours=hours,
85
+ )
86
+ # Invalid custom params → fall through to default.
87
+ key = "7d"
88
+
89
+ if key not in _PRESET_HOURS:
90
+ key = "7d"
91
+
92
+ hours = _PRESET_HOURS[key]
93
+ return TimeRange(
94
+ key=key,
95
+ label=_PRESET_LABELS[key],
96
+ start=current - timedelta(hours=hours),
97
+ end=current,
98
+ hours=hours,
99
+ )
100
+
101
+
102
+ def preset_keys() -> Iterable[str]:
103
+ """Iterate the supported preset keys, in display order."""
104
+ return ("1d", "7d", "30d")
105
+
106
+
107
+ def _parse_iso_date(text: Optional[str]) -> Optional[datetime]:
108
+ if not text:
109
+ return None
110
+ text = text.strip()
111
+ # Accept either "2026-05-12" or full ISO-8601.
112
+ try:
113
+ if len(text) == 10:
114
+ return datetime.strptime(text, "%Y-%m-%d").replace(tzinfo=timezone.utc)
115
+ return datetime.fromisoformat(text.replace("Z", "+00:00")).astimezone(timezone.utc)
116
+ except ValueError:
117
+ return None
@@ -0,0 +1 @@
1
+ """CLI package for AgentOps."""