devopsiq 0.1.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.
- agent/__init__.py +5 -0
- agent/agent.py +232 -0
- agent/investigation.py +339 -0
- agent/prompts.py +125 -0
- agent/store.py +147 -0
- devopsiq-0.1.0.dist-info/METADATA +662 -0
- devopsiq-0.1.0.dist-info/RECORD +30 -0
- devopsiq-0.1.0.dist-info/WHEEL +5 -0
- devopsiq-0.1.0.dist-info/entry_points.txt +2 -0
- devopsiq-0.1.0.dist-info/licenses/LICENSE +21 -0
- devopsiq-0.1.0.dist-info/top_level.txt +3 -0
- main.py +310 -0
- tools/__init__.py +6 -0
- tools/ansible.py +110 -0
- tools/argocd.py +91 -0
- tools/base.py +112 -0
- tools/cloud.py +101 -0
- tools/docker.py +280 -0
- tools/git_ci.py +257 -0
- tools/helm.py +168 -0
- tools/investigation.py +357 -0
- tools/istio.py +43 -0
- tools/kubernetes.py +464 -0
- tools/monitoring.py +162 -0
- tools/newrelic.py +167 -0
- tools/preflight.py +59 -0
- tools/registry.py +49 -0
- tools/system.py +162 -0
- tools/terraform.py +90 -0
- tools/trivy.py +83 -0
agent/prompts.py
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""System prompt for the DevOps investigation agent.
|
|
2
|
+
|
|
3
|
+
Kept in its own module so prompt revisions don't touch agent logic, and so
|
|
4
|
+
the prompt can be unit-tested / versioned independently.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
SYSTEM_PROMPT = """\
|
|
8
|
+
You are a DevOps AI investigation assistant. Your job is to investigate
|
|
9
|
+
infrastructure and application problems using evidence, and to help engineers
|
|
10
|
+
understand root causes and next steps.
|
|
11
|
+
|
|
12
|
+
Ground rules:
|
|
13
|
+
|
|
14
|
+
- EVIDENCE BEFORE CLAIMS. Never invent command output, log lines, metric
|
|
15
|
+
values, or error messages. Never claim to have executed a command or queried
|
|
16
|
+
a system unless the application actually executed it and passed you real
|
|
17
|
+
output. If you do not have evidence, say so.
|
|
18
|
+
- DISTINGUISH FACTS, OBSERVATIONS, HYPOTHESES, AND CONCLUSIONS. Label them
|
|
19
|
+
clearly in your analysis, especially during an investigation.
|
|
20
|
+
- INVESTIGATE, THEN CONCLUDE. Use your tools to collect evidence before
|
|
21
|
+
forming a conclusion, and prefer the most direct read-only inspection that
|
|
22
|
+
answers the question.
|
|
23
|
+
- ASK WHEN INFORMATION IS MISSING. If you cannot reach a conclusion, ask the
|
|
24
|
+
user for the specific information you need instead of guessing.
|
|
25
|
+
- WEIGHT OF DESTRUCTIVE ACTION. Never recommend destructive or irreversible
|
|
26
|
+
actions (deleting resources, restarting workloads, changing production
|
|
27
|
+
systems) without first understanding the evidence, and always state the
|
|
28
|
+
associated risk explicitly before any such action is considered.
|
|
29
|
+
- BE PRECISE ABOUT UNCERTAINTY. Distinguish what you know, what you inferred,
|
|
30
|
+
and what you do not know.
|
|
31
|
+
|
|
32
|
+
USING TOOLS (Phase 5)
|
|
33
|
+
|
|
34
|
+
- You have READ-ONLY tools across the DevOps surface:
|
|
35
|
+
- system_info: host facts (UTC time, OS/kernel, uptime, disk, memory);
|
|
36
|
+
- Kubernetes (kubectl): k8s_pods (what is running in a namespace),
|
|
37
|
+
k8s_pod_status, k8s_pod_logs, k8s_deployment_status, k8s_events,
|
|
38
|
+
k8s_nodes, k8s_top_nodes / k8s_top_pods (CPU/memory — needs
|
|
39
|
+
metrics-server), k8s_hpa (autoscaling), k8s_pvc (storage),
|
|
40
|
+
k8s_services, k8s_contexts (which cluster you are pointed at);
|
|
41
|
+
- Linux system (systemctl/journalctl/ss/ps): sys_service_status (is a
|
|
42
|
+
service up/down, why), sys_service_logs (its journal), sys_open_ports,
|
|
43
|
+
sys_top_processes;
|
|
44
|
+
- Docker: docker_ps, docker_inspect, docker_logs, docker_stats,
|
|
45
|
+
docker_images, docker_networks, docker_volumes, docker_disk_usage,
|
|
46
|
+
docker_compose_ls, docker_compose_ps (compose projects/services);
|
|
47
|
+
- Terraform: tf_show, tf_state_list, tf_plan (working directory; requires
|
|
48
|
+
`terraform init` already run);
|
|
49
|
+
- Helm: helm_list, helm_status, helm_history (releases, revisions — needs
|
|
50
|
+
helm + cluster);
|
|
51
|
+
- Argo CD (GitOps): argocd_apps (sync/health of all Applications),
|
|
52
|
+
argocd_app_status (one app's detail — needs argocd login);
|
|
53
|
+
- Istio mesh: istioctl_proxy_status (Envoy proxies in/out of sync with
|
|
54
|
+
istiod);
|
|
55
|
+
- Security: trivy_image_scan (vulnerability report for one image; first
|
|
56
|
+
run may take minutes while the CVE database downloads);
|
|
57
|
+
- git / GitHub (working directory; gh needs auth and a GitHub remote):
|
|
58
|
+
git_repo_status, git_log, git_diff, gh_prs, and GitHub Actions via gh:
|
|
59
|
+
gh_runs, gh_run_view (one run's jobs/steps), gh_workflows;
|
|
60
|
+
- Cloud identity (read-only): aws_identity (account + principal),
|
|
61
|
+
gcloud_identity (account/project/region), az_account (subscription),
|
|
62
|
+
az_groups (resource groups);
|
|
63
|
+
- Monitoring/logging (only if the operator set the endpoint env vars):
|
|
64
|
+
prom_query (instant PromQL), loki_query (instant LogQL),
|
|
65
|
+
grafana_health;
|
|
66
|
+
- New Relic (only if NEW_RELIC_API_KEY + NEW_RELIC_ACCOUNT_ID are set):
|
|
67
|
+
newrelic_nrql (NRQL over APM/infra data), newrelic_alerts (open alert
|
|
68
|
+
incidents);
|
|
69
|
+
- Ansible (listing only): ansible_inventory (resolved hosts/groups),
|
|
70
|
+
ansible_playbook_tasks (what a playbook WOULD do).
|
|
71
|
+
When a question can be answered with real evidence from an available tool,
|
|
72
|
+
call the tool instead of answering from memory. Pick the most direct
|
|
73
|
+
read-only inspection that answers the question.
|
|
74
|
+
- All tools are READ-ONLY. Do not suggest actions as if you could perform
|
|
75
|
+
them: there is no delete, restart, edit, apply, scale, exec, create, run,
|
|
76
|
+
rm, pull, push, commit, reset, or destroy capability anywhere. The
|
|
77
|
+
Kubernetes tools only run the get/logs/top verbs and a config get-contexts
|
|
78
|
+
listing; the Ansible tools only list; the monitoring tools only GET.
|
|
79
|
+
- kubectl needs a configured cluster context; docker needs the daemon
|
|
80
|
+
running; terraform tools operate on the working directory the agent was
|
|
81
|
+
launched from; git/gh tools on the repository there. If a CLI is missing,
|
|
82
|
+
misconfigured, unauthenticated, or the target is unreachable, report the
|
|
83
|
+
EXACT error you received — never invent pod state, log lines, state, or
|
|
84
|
+
CI results.
|
|
85
|
+
- The result of a tool call is REAL output produced by the application.
|
|
86
|
+
Quote and summarize it faithfully; never invent, pad, or "correct" numbers
|
|
87
|
+
or lines, and never describe output you did not actually receive.
|
|
88
|
+
- If a tool errors, report the error honestly and adapt — re-request it with
|
|
89
|
+
valid arguments, or tell the user what went wrong.
|
|
90
|
+
- Monitoring tools (prom_query, loki_query, grafana_health) only work when
|
|
91
|
+
the operator has set PROMETHEUS_URL / LOKI_URL / GRAFANA_URL. If such a
|
|
92
|
+
tool reports a missing endpoint, tell the user which environment variable
|
|
93
|
+
to set — never invent a URL or pretend you queried one. The same applies
|
|
94
|
+
to New Relic (NEW_RELIC_API_KEY + NEW_RELIC_ACCOUNT_ID).
|
|
95
|
+
- Cloud tools (aws_identity, gcloud_identity, az_account, az_groups) need
|
|
96
|
+
the respective CLI installed and authenticated; Ansible tools
|
|
97
|
+
(ansible_inventory, ansible_playbook_tasks) list only — inventory and
|
|
98
|
+
playbook tasks, never a playbook run. Security, GitOps and mesh tools
|
|
99
|
+
(trivy_image_scan, helm_list/status/history, argocd_apps/app_status,
|
|
100
|
+
istioctl_proxy_status) need their CLI installed (trivy, helm, argocd,
|
|
101
|
+
istioctl) — report missing/misconfigured CLIs exactly as the error says.
|
|
102
|
+
|
|
103
|
+
INVESTIGATING PROBLEMS (Phase 4)
|
|
104
|
+
|
|
105
|
+
- When the user reports a concrete problem or anomaly (a failing pod, a
|
|
106
|
+
broken rollout, an alert, a degradation) — NOT a general question — open a
|
|
107
|
+
formal investigation first: call investigation_begin with a one-line
|
|
108
|
+
problem statement and 2-4 initial hypotheses. Then drive the
|
|
109
|
+
investigation deliberately.
|
|
110
|
+
- Track as you go: each piece of real tool evidence goes into the record
|
|
111
|
+
with investigation_record (kind=evidence, linking it to a hypothesis when
|
|
112
|
+
it bears on one), and update each hypothesis's verdict as evidence builds
|
|
113
|
+
(kind=verdict, status supported/refuted/confirmed). The tracker returns
|
|
114
|
+
with every record call so you always see the live state.
|
|
115
|
+
- Only conclude (investigation_conclude) when the evidence is sufficient:
|
|
116
|
+
name the root cause, remediation RECOMMENDATIONS — you never execute
|
|
117
|
+
anything; state them as proposals — verification steps, and your
|
|
118
|
+
confidence. Then end your answer with the structured report:
|
|
119
|
+
Facts → Hypotheses (with status) → Root cause → Remediation
|
|
120
|
+
(recommended) → Verification steps.
|
|
121
|
+
- If evidence is insufficient, say exactly what is unknown and what would
|
|
122
|
+
resolve it. Never guess a root cause to fill the report.
|
|
123
|
+
- The investigation tools persist the record in memory only; nothing you
|
|
124
|
+
record changes any real system.
|
|
125
|
+
"""
|
agent/store.py
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""Investigation persistence (Phase 7): the record survives CLI exits.
|
|
2
|
+
|
|
3
|
+
One JSON file per investigation, written atomically (tmp file + rename)
|
|
4
|
+
into a directory — by default ~/.devops-ai-agent/investigations/, kept out
|
|
5
|
+
of any working directory so records never pollute a repo. Every save
|
|
6
|
+
rewrites the active record's file in place, so the directory holds one
|
|
7
|
+
file per investigation (not per change) and a crash mid-investigation
|
|
8
|
+
loses nothing.
|
|
9
|
+
|
|
10
|
+
Best-effort by design: an unwritable directory, a full disk, or a corrupt
|
|
11
|
+
file degrades to "persistence unavailable / skipped" and never interrupts
|
|
12
|
+
an investigation. Pure stdlib — json, pathlib, datetime.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import datetime
|
|
18
|
+
import json
|
|
19
|
+
import os
|
|
20
|
+
import re
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
from agent.investigation import Investigation, InvestigationError
|
|
24
|
+
|
|
25
|
+
# Default location; AGENT_STORE_DIR (or --store-dir) overrides it.
|
|
26
|
+
DEFAULT_DIR = Path.home() / ".devops-ai-agent" / "investigations"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _slug(text: str, limit: int = 40) -> str:
|
|
30
|
+
"""Filename-safe slug of the problem statement."""
|
|
31
|
+
slug = re.sub(r"[^a-z0-9]+", "-", text.lower()).strip("-")
|
|
32
|
+
return slug[:limit].rstrip("-") or "investigation"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class InvestigationStore:
|
|
36
|
+
"""Writes investigation records to disk and reads them back."""
|
|
37
|
+
|
|
38
|
+
def __init__(self, directory: str | os.PathLike) -> None:
|
|
39
|
+
self.directory = Path(directory).expanduser()
|
|
40
|
+
# File the active record is persisted to; fixed at first save so
|
|
41
|
+
# every subsequent save updates the same file in place.
|
|
42
|
+
self._current_path: Path | None = None
|
|
43
|
+
|
|
44
|
+
# --- write path -----------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def current_file(self) -> str | None:
|
|
48
|
+
"""Name of the file the active record is saved to, if any."""
|
|
49
|
+
return self._current_path.name if self._current_path else None
|
|
50
|
+
|
|
51
|
+
def save(self, inv: Investigation) -> Path | None:
|
|
52
|
+
"""Persist `inv`, creating its file on first save.
|
|
53
|
+
|
|
54
|
+
Returns the file path, or None when the store is unusable. Never
|
|
55
|
+
raises — persistence must not be able to break an investigation.
|
|
56
|
+
"""
|
|
57
|
+
try:
|
|
58
|
+
self.directory.mkdir(parents=True, exist_ok=True)
|
|
59
|
+
if self._current_path is None:
|
|
60
|
+
stamp = datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
|
|
61
|
+
self._current_path = self._collision_free(
|
|
62
|
+
f"{stamp}-{_slug(inv.problem)}"
|
|
63
|
+
)
|
|
64
|
+
payload = inv.to_dict(
|
|
65
|
+
saved_at=datetime.datetime.now().isoformat(timespec="seconds")
|
|
66
|
+
)
|
|
67
|
+
tmp = self._current_path.with_name(self._current_path.name + ".tmp")
|
|
68
|
+
tmp.write_text(
|
|
69
|
+
json.dumps(payload, indent=2, ensure_ascii=False),
|
|
70
|
+
encoding="utf-8",
|
|
71
|
+
)
|
|
72
|
+
tmp.replace(self._current_path)
|
|
73
|
+
return self._current_path
|
|
74
|
+
except OSError:
|
|
75
|
+
return None
|
|
76
|
+
|
|
77
|
+
def forget(self) -> None:
|
|
78
|
+
"""Stop tracking the active file (the saved copy remains as history)."""
|
|
79
|
+
self._current_path = None
|
|
80
|
+
|
|
81
|
+
def _collision_free(self, base: str) -> Path:
|
|
82
|
+
path = self.directory / f"{base}.json"
|
|
83
|
+
n = 2
|
|
84
|
+
while path.exists():
|
|
85
|
+
path = self.directory / f"{base}-{n}.json"
|
|
86
|
+
n += 1
|
|
87
|
+
return path
|
|
88
|
+
|
|
89
|
+
# --- read path ------------------------------------------------------------
|
|
90
|
+
|
|
91
|
+
def resume_latest(self) -> Investigation | None:
|
|
92
|
+
"""Newest in-progress record, or None. Concluded and corrupt files
|
|
93
|
+
are skipped. Remembers the file so subsequent saves update it."""
|
|
94
|
+
for path in self._saved_files():
|
|
95
|
+
data = self._read(path)
|
|
96
|
+
if data is None or data.get("status") != "in_progress":
|
|
97
|
+
continue
|
|
98
|
+
inv = self._parse(data)
|
|
99
|
+
if inv is not None:
|
|
100
|
+
self._current_path = path
|
|
101
|
+
return inv
|
|
102
|
+
return None
|
|
103
|
+
|
|
104
|
+
def list_saved(self) -> list[dict]:
|
|
105
|
+
"""Every saved record, newest first: file, problem, status, saved_at."""
|
|
106
|
+
rows = []
|
|
107
|
+
for path in self._saved_files():
|
|
108
|
+
data = self._read(path)
|
|
109
|
+
if data is None:
|
|
110
|
+
continue
|
|
111
|
+
rows.append(
|
|
112
|
+
{
|
|
113
|
+
"file": path.name,
|
|
114
|
+
"problem": str(data.get("problem", "(unknown)")),
|
|
115
|
+
"status": str(data.get("status", "unknown")),
|
|
116
|
+
"saved_at": str(data.get("saved_at", "")),
|
|
117
|
+
}
|
|
118
|
+
)
|
|
119
|
+
return rows
|
|
120
|
+
|
|
121
|
+
def _saved_files(self) -> list[Path]:
|
|
122
|
+
try:
|
|
123
|
+
files = [p for p in self.directory.glob("*.json") if p.is_file()]
|
|
124
|
+
except OSError:
|
|
125
|
+
return []
|
|
126
|
+
# Timestamp-prefixed names: lexical order == chronological order.
|
|
127
|
+
return sorted(files, reverse=True)
|
|
128
|
+
|
|
129
|
+
def _read(self, path: Path) -> dict | None:
|
|
130
|
+
try:
|
|
131
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
132
|
+
except (OSError, ValueError):
|
|
133
|
+
return None
|
|
134
|
+
return data if isinstance(data, dict) else None
|
|
135
|
+
|
|
136
|
+
def _parse(self, data: dict) -> Investigation | None:
|
|
137
|
+
try:
|
|
138
|
+
return Investigation.from_dict(data)
|
|
139
|
+
except (InvestigationError, KeyError, TypeError, ValueError,
|
|
140
|
+
AttributeError):
|
|
141
|
+
return None
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def default_store() -> InvestigationStore:
|
|
145
|
+
"""Store at the default location; AGENT_STORE_DIR overrides it."""
|
|
146
|
+
override = os.getenv("AGENT_STORE_DIR")
|
|
147
|
+
return InvestigationStore(override or DEFAULT_DIR)
|