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/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)