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.
- agentops/__init__.py +10 -0
- agentops/__main__.py +6 -0
- agentops/agent/__init__.py +12 -0
- agentops/agent/_legacy_ids.py +92 -0
- agentops/agent/analyzer.py +207 -0
- agentops/agent/checks/__init__.py +1 -0
- agentops/agent/checks/catalog.py +880 -0
- agentops/agent/checks/errors.py +279 -0
- agentops/agent/checks/foundry_config.py +75 -0
- agentops/agent/checks/latency.py +84 -0
- agentops/agent/checks/opex.py +157 -0
- agentops/agent/checks/opex_workspace.py +874 -0
- agentops/agent/checks/posture.py +36 -0
- agentops/agent/checks/posture_rules/__init__.py +53 -0
- agentops/agent/checks/posture_rules/content_filter.py +59 -0
- agentops/agent/checks/posture_rules/diagnostics.py +74 -0
- agentops/agent/checks/posture_rules/local_auth.py +55 -0
- agentops/agent/checks/posture_rules/managed_identity.py +59 -0
- agentops/agent/checks/posture_rules/network.py +68 -0
- agentops/agent/checks/regression.py +78 -0
- agentops/agent/checks/release_readiness.py +182 -0
- agentops/agent/checks/safety.py +247 -0
- agentops/agent/checks/spec_conformance.py +375 -0
- agentops/agent/cockpit.py +5159 -0
- agentops/agent/config.py +240 -0
- agentops/agent/findings.py +113 -0
- agentops/agent/history.py +142 -0
- agentops/agent/knowledge/__init__.py +182 -0
- agentops/agent/knowledge/waf-checklist.csv +39 -0
- agentops/agent/llm_assist/__init__.py +16 -0
- agentops/agent/llm_assist/_base.py +124 -0
- agentops/agent/llm_assist/_bundle_rule.py +154 -0
- agentops/agent/llm_assist/_client.py +347 -0
- agentops/agent/llm_assist/_dataset_rules.py +191 -0
- agentops/agent/llm_assist/_engine.py +106 -0
- agentops/agent/llm_assist/_prompt_rules.py +291 -0
- agentops/agent/llm_assist/_spec_rules.py +235 -0
- agentops/agent/production_telemetry.py +430 -0
- agentops/agent/report.py +207 -0
- agentops/agent/server/__init__.py +1 -0
- agentops/agent/server/app.py +84 -0
- agentops/agent/server/auth.py +94 -0
- agentops/agent/server/chat.py +44 -0
- agentops/agent/server/protocol.py +72 -0
- agentops/agent/sources/__init__.py +1 -0
- agentops/agent/sources/azure_monitor.py +523 -0
- agentops/agent/sources/azure_resources.py +602 -0
- agentops/agent/sources/foundry_control.py +174 -0
- agentops/agent/sources/results_history.py +494 -0
- agentops/agent/sources/spec_detectors/__init__.py +42 -0
- agentops/agent/sources/spec_detectors/_base.py +58 -0
- agentops/agent/sources/spec_detectors/agents_md.py +75 -0
- agentops/agent/sources/spec_detectors/spec_kit.py +172 -0
- agentops/agent/time_range.py +117 -0
- agentops/cli/__init__.py +1 -0
- agentops/cli/app.py +4823 -0
- agentops/core/__init__.py +1 -0
- agentops/core/agentops_config.py +592 -0
- agentops/core/config_loader.py +22 -0
- agentops/core/evaluators.py +480 -0
- agentops/core/release_evidence.py +56 -0
- agentops/core/results.py +117 -0
- agentops/mcp/__init__.py +10 -0
- agentops/mcp/server.py +232 -0
- agentops/pipeline/__init__.py +8 -0
- agentops/pipeline/cloud_results.py +189 -0
- agentops/pipeline/cloud_runner.py +901 -0
- agentops/pipeline/comparison.py +108 -0
- agentops/pipeline/diagnostics.py +51 -0
- agentops/pipeline/invocations.py +535 -0
- agentops/pipeline/official_eval.py +414 -0
- agentops/pipeline/orchestrator.py +775 -0
- agentops/pipeline/prompt_deploy.py +377 -0
- agentops/pipeline/publisher.py +121 -0
- agentops/pipeline/reporter.py +202 -0
- agentops/pipeline/runtime.py +409 -0
- agentops/pipeline/thresholds.py +84 -0
- agentops/services/__init__.py +1 -0
- agentops/services/cicd.py +720 -0
- agentops/services/eval_analysis.py +848 -0
- agentops/services/evidence_pack.py +757 -0
- agentops/services/initializer.py +86 -0
- agentops/services/preflight.py +470 -0
- agentops/services/setup_wizard.py +709 -0
- agentops/services/skills.py +643 -0
- agentops/services/trace_promotion.py +300 -0
- agentops/services/workflow_analysis.py +1129 -0
- agentops/templates/.gitignore +15 -0
- agentops/templates/__init__.py +1 -0
- agentops/templates/agent-server/Dockerfile +23 -0
- agentops/templates/agent-server/README.md +61 -0
- agentops/templates/agent-server/main.bicep +94 -0
- agentops/templates/agent.yaml +87 -0
- agentops/templates/agentops.yaml +58 -0
- agentops/templates/foundry.svg +71 -0
- agentops/templates/icon.png +0 -0
- agentops/templates/pipelines/azuredevops/agentops-deploy-dev-azd.yml +118 -0
- agentops/templates/pipelines/azuredevops/agentops-deploy-dev.yml +73 -0
- agentops/templates/pipelines/azuredevops/agentops-deploy-prod-azd.yml +141 -0
- agentops/templates/pipelines/azuredevops/agentops-deploy-prod.yml +94 -0
- agentops/templates/pipelines/azuredevops/agentops-deploy-prompt-agent.yml +167 -0
- agentops/templates/pipelines/azuredevops/agentops-deploy-qa-azd.yml +118 -0
- agentops/templates/pipelines/azuredevops/agentops-deploy-qa.yml +68 -0
- agentops/templates/pipelines/azuredevops/agentops-pr-prompt-agent.yml +210 -0
- agentops/templates/pipelines/azuredevops/agentops-pr.yml +155 -0
- agentops/templates/pipelines/azuredevops/agentops-watchdog.yml +106 -0
- agentops/templates/project.gitignore +36 -0
- agentops/templates/sample-traces.jsonl +3 -0
- agentops/templates/skills/agentops-agent/SKILL.md +137 -0
- agentops/templates/skills/agentops-config/SKILL.md +113 -0
- agentops/templates/skills/agentops-dataset/SKILL.md +84 -0
- agentops/templates/skills/agentops-eval/SKILL.md +189 -0
- agentops/templates/skills/agentops-report/SKILL.md +71 -0
- agentops/templates/skills/agentops-workflow/SKILL.md +471 -0
- agentops/templates/smoke.jsonl +3 -0
- agentops/templates/waf-checklist.README.md +84 -0
- agentops/templates/waf-checklist.csv +22 -0
- agentops/templates/workflows/agentops-deploy-dev-azd.yml +166 -0
- agentops/templates/workflows/agentops-deploy-dev.yml +187 -0
- agentops/templates/workflows/agentops-deploy-prod-azd.yml +183 -0
- agentops/templates/workflows/agentops-deploy-prod.yml +171 -0
- agentops/templates/workflows/agentops-deploy-prompt-agent.yml +197 -0
- agentops/templates/workflows/agentops-deploy-qa-azd.yml +156 -0
- agentops/templates/workflows/agentops-deploy-qa.yml +145 -0
- agentops/templates/workflows/agentops-pr-prompt-agent.yml +210 -0
- agentops/templates/workflows/agentops-pr.yml +148 -0
- agentops/templates/workflows/agentops-watchdog.yml +122 -0
- agentops/utils/__init__.py +1 -0
- agentops/utils/azd_env.py +435 -0
- agentops/utils/azure_endpoints.py +62 -0
- agentops/utils/colors.py +47 -0
- agentops/utils/dotenv_loader.py +105 -0
- agentops/utils/foundry_discovery.py +229 -0
- agentops/utils/logging.py +59 -0
- agentops/utils/telemetry.py +554 -0
- agentops/utils/yaml.py +36 -0
- agentops_accelerator-0.3.0.dist-info/METADATA +278 -0
- agentops_accelerator-0.3.0.dist-info/RECORD +142 -0
- agentops_accelerator-0.3.0.dist-info/WHEEL +5 -0
- agentops_accelerator-0.3.0.dist-info/entry_points.txt +2 -0
- agentops_accelerator-0.3.0.dist-info/licenses/LICENSE +21 -0
- 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
|
agentops/cli/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""CLI package for AgentOps."""
|