devopsiq 0.1.0__tar.gz → 0.1.1__tar.gz

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 (43) hide show
  1. {devopsiq-0.1.0/devopsiq.egg-info → devopsiq-0.1.1}/PKG-INFO +49 -2
  2. {devopsiq-0.1.0 → devopsiq-0.1.1}/README.md +48 -1
  3. {devopsiq-0.1.0 → devopsiq-0.1.1}/agent/agent.py +23 -11
  4. {devopsiq-0.1.0 → devopsiq-0.1.1/devopsiq.egg-info}/PKG-INFO +49 -2
  5. {devopsiq-0.1.0 → devopsiq-0.1.1}/main.py +7 -0
  6. {devopsiq-0.1.0 → devopsiq-0.1.1}/pyproject.toml +1 -1
  7. {devopsiq-0.1.0 → devopsiq-0.1.1}/tests/test_automation.py +53 -0
  8. {devopsiq-0.1.0 → devopsiq-0.1.1}/LICENSE +0 -0
  9. {devopsiq-0.1.0 → devopsiq-0.1.1}/agent/__init__.py +0 -0
  10. {devopsiq-0.1.0 → devopsiq-0.1.1}/agent/investigation.py +0 -0
  11. {devopsiq-0.1.0 → devopsiq-0.1.1}/agent/prompts.py +0 -0
  12. {devopsiq-0.1.0 → devopsiq-0.1.1}/agent/store.py +0 -0
  13. {devopsiq-0.1.0 → devopsiq-0.1.1}/devopsiq.egg-info/SOURCES.txt +0 -0
  14. {devopsiq-0.1.0 → devopsiq-0.1.1}/devopsiq.egg-info/dependency_links.txt +0 -0
  15. {devopsiq-0.1.0 → devopsiq-0.1.1}/devopsiq.egg-info/entry_points.txt +0 -0
  16. {devopsiq-0.1.0 → devopsiq-0.1.1}/devopsiq.egg-info/requires.txt +0 -0
  17. {devopsiq-0.1.0 → devopsiq-0.1.1}/devopsiq.egg-info/top_level.txt +0 -0
  18. {devopsiq-0.1.0 → devopsiq-0.1.1}/setup.cfg +0 -0
  19. {devopsiq-0.1.0 → devopsiq-0.1.1}/tests/test_phase2.py +0 -0
  20. {devopsiq-0.1.0 → devopsiq-0.1.1}/tests/test_phase3.py +0 -0
  21. {devopsiq-0.1.0 → devopsiq-0.1.1}/tests/test_phase4.py +0 -0
  22. {devopsiq-0.1.0 → devopsiq-0.1.1}/tests/test_phase5.py +0 -0
  23. {devopsiq-0.1.0 → devopsiq-0.1.1}/tests/test_phase7.py +0 -0
  24. {devopsiq-0.1.0 → devopsiq-0.1.1}/tests/test_phase8.py +0 -0
  25. {devopsiq-0.1.0 → devopsiq-0.1.1}/tests/test_phase9.py +0 -0
  26. {devopsiq-0.1.0 → devopsiq-0.1.1}/tools/__init__.py +0 -0
  27. {devopsiq-0.1.0 → devopsiq-0.1.1}/tools/ansible.py +0 -0
  28. {devopsiq-0.1.0 → devopsiq-0.1.1}/tools/argocd.py +0 -0
  29. {devopsiq-0.1.0 → devopsiq-0.1.1}/tools/base.py +0 -0
  30. {devopsiq-0.1.0 → devopsiq-0.1.1}/tools/cloud.py +0 -0
  31. {devopsiq-0.1.0 → devopsiq-0.1.1}/tools/docker.py +0 -0
  32. {devopsiq-0.1.0 → devopsiq-0.1.1}/tools/git_ci.py +0 -0
  33. {devopsiq-0.1.0 → devopsiq-0.1.1}/tools/helm.py +0 -0
  34. {devopsiq-0.1.0 → devopsiq-0.1.1}/tools/investigation.py +0 -0
  35. {devopsiq-0.1.0 → devopsiq-0.1.1}/tools/istio.py +0 -0
  36. {devopsiq-0.1.0 → devopsiq-0.1.1}/tools/kubernetes.py +0 -0
  37. {devopsiq-0.1.0 → devopsiq-0.1.1}/tools/monitoring.py +0 -0
  38. {devopsiq-0.1.0 → devopsiq-0.1.1}/tools/newrelic.py +0 -0
  39. {devopsiq-0.1.0 → devopsiq-0.1.1}/tools/preflight.py +0 -0
  40. {devopsiq-0.1.0 → devopsiq-0.1.1}/tools/registry.py +0 -0
  41. {devopsiq-0.1.0 → devopsiq-0.1.1}/tools/system.py +0 -0
  42. {devopsiq-0.1.0 → devopsiq-0.1.1}/tools/terraform.py +0 -0
  43. {devopsiq-0.1.0 → devopsiq-0.1.1}/tools/trivy.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devopsiq
3
- Version: 0.1.0
3
+ Version: 0.1.1
4
4
  Summary: A from-scratch, read-only AI agent that investigates real DevOps problems — Kubernetes, Docker, Helm, Argo CD, Istio, Terraform, GitHub Actions, cloud, New Relic and more.
5
5
  Author: DevOpsAbhii
6
6
  License-Expression: MIT
@@ -400,6 +400,30 @@ Swapping to another OpenRouter model later is a one-line change (an env var
400
400
  today); moving to any other OpenAI-compatible provider changes only
401
401
  `agent/agent.py` configuration.
402
402
 
403
+ **Your model, your choice.** The default is baked in as a fallback, never a
404
+ restriction — set `OPENROUTER_MODEL` (shell or `.env`) to any model on
405
+ OpenRouter:
406
+
407
+ ```bash
408
+ export OPENROUTER_MODEL=anthropic/claude-sonnet-5 # or openai/gpt-5.2, google/gemini-2.5-pro, ...
409
+ ```
410
+
411
+ Two things to weigh when picking: the agent is a tool-use loop, so choose a
412
+ model with solid function-calling (a chat-only model will answer from
413
+ imagination instead of gathering evidence); and an investigation makes
414
+ several model calls per run, so price-per-call multiplies — that is why the
415
+ default is a cheap, reliable tool-caller rather than the biggest model.
416
+
417
+ **No OpenRouter at all?** `OPENROUTER_BASE_URL` points the same client at
418
+ any OpenAI-compatible endpoint — including a local one. Ollama, free and
419
+ offline:
420
+
421
+ ```bash
422
+ export OPENROUTER_BASE_URL=http://localhost:11434/v1
423
+ export OPENROUTER_MODEL=llama3.2 # any Ollama model that does tools
424
+ devopsiq "docker container checkout keeps exiting, investigate"
425
+ ```
426
+
403
427
  ## 5. Installation
404
428
 
405
429
  Three ways — pick one. Whatever you choose, the agent also needs the CLIs of
@@ -463,6 +487,27 @@ Recommended CLIs, by domain:
463
487
  | New Relic | `curl` + `NEW_RELIC_API_KEY` / `NEW_RELIC_ACCOUNT_ID` env vars |
464
488
  | Ansible | `ansible-inventory`, `ansible-playbook` |
465
489
 
490
+ ### No API key? You still have the tools
491
+
492
+ The model is the only part that needs a key — the agent builds without one
493
+ (model-less mode), and two things keep working:
494
+
495
+ - **Record commands**: `devopsiq /report`, `devopsiq /investigations`, and
496
+ the same commands inside the REPL, all work with no key configured. Only
497
+ actual questions exit 1 with the setup message naming `OPENROUTER_API_KEY`.
498
+ - **The whole 58-tool layer** via the Python library, with no model and no
499
+ key (see INTEGRATION.md):
500
+
501
+ ```python
502
+ import agent.agent # registers every tool
503
+ from tools.registry import execute_tool
504
+
505
+ print(execute_tool("k8s_pods", '{"namespace": "prod"}'))
506
+ ```
507
+
508
+ And the fully key-free *agent* path is a local model (Ollama example in
509
+ section 4 above) — no cloud account needed at all.
510
+
466
511
  ## 6. Environment setup
467
512
 
468
513
  ```bash
@@ -490,7 +535,9 @@ NEW_RELIC_ACCOUNT_ID=1234567 # (Phase 9) numeric account
490
535
 
491
536
  `.env` is gitignored; the API key is never hard-coded in Python, printed, or
492
537
  logged. If `OPENROUTER_API_KEY` is already set in your shell, the shell value
493
- wins and `.env` is not consulted. Kubernetes tools use kubectl's own config
538
+ wins and `.env` is not consulted. Without any key the agent still starts in
539
+ model-less mode (record commands + the tool layer — see "No API key?" in
540
+ section 5). Kubernetes tools use kubectl's own config
494
541
  (`~/.kube/config` or `KUBECONFIG`); no agent-side config is needed.
495
542
 
496
543
  ## 7. How to run the agent
@@ -371,6 +371,30 @@ Swapping to another OpenRouter model later is a one-line change (an env var
371
371
  today); moving to any other OpenAI-compatible provider changes only
372
372
  `agent/agent.py` configuration.
373
373
 
374
+ **Your model, your choice.** The default is baked in as a fallback, never a
375
+ restriction — set `OPENROUTER_MODEL` (shell or `.env`) to any model on
376
+ OpenRouter:
377
+
378
+ ```bash
379
+ export OPENROUTER_MODEL=anthropic/claude-sonnet-5 # or openai/gpt-5.2, google/gemini-2.5-pro, ...
380
+ ```
381
+
382
+ Two things to weigh when picking: the agent is a tool-use loop, so choose a
383
+ model with solid function-calling (a chat-only model will answer from
384
+ imagination instead of gathering evidence); and an investigation makes
385
+ several model calls per run, so price-per-call multiplies — that is why the
386
+ default is a cheap, reliable tool-caller rather than the biggest model.
387
+
388
+ **No OpenRouter at all?** `OPENROUTER_BASE_URL` points the same client at
389
+ any OpenAI-compatible endpoint — including a local one. Ollama, free and
390
+ offline:
391
+
392
+ ```bash
393
+ export OPENROUTER_BASE_URL=http://localhost:11434/v1
394
+ export OPENROUTER_MODEL=llama3.2 # any Ollama model that does tools
395
+ devopsiq "docker container checkout keeps exiting, investigate"
396
+ ```
397
+
374
398
  ## 5. Installation
375
399
 
376
400
  Three ways — pick one. Whatever you choose, the agent also needs the CLIs of
@@ -434,6 +458,27 @@ Recommended CLIs, by domain:
434
458
  | New Relic | `curl` + `NEW_RELIC_API_KEY` / `NEW_RELIC_ACCOUNT_ID` env vars |
435
459
  | Ansible | `ansible-inventory`, `ansible-playbook` |
436
460
 
461
+ ### No API key? You still have the tools
462
+
463
+ The model is the only part that needs a key — the agent builds without one
464
+ (model-less mode), and two things keep working:
465
+
466
+ - **Record commands**: `devopsiq /report`, `devopsiq /investigations`, and
467
+ the same commands inside the REPL, all work with no key configured. Only
468
+ actual questions exit 1 with the setup message naming `OPENROUTER_API_KEY`.
469
+ - **The whole 58-tool layer** via the Python library, with no model and no
470
+ key (see INTEGRATION.md):
471
+
472
+ ```python
473
+ import agent.agent # registers every tool
474
+ from tools.registry import execute_tool
475
+
476
+ print(execute_tool("k8s_pods", '{"namespace": "prod"}'))
477
+ ```
478
+
479
+ And the fully key-free *agent* path is a local model (Ollama example in
480
+ section 4 above) — no cloud account needed at all.
481
+
437
482
  ## 6. Environment setup
438
483
 
439
484
  ```bash
@@ -461,7 +506,9 @@ NEW_RELIC_ACCOUNT_ID=1234567 # (Phase 9) numeric account
461
506
 
462
507
  `.env` is gitignored; the API key is never hard-coded in Python, printed, or
463
508
  logged. If `OPENROUTER_API_KEY` is already set in your shell, the shell value
464
- wins and `.env` is not consulted. Kubernetes tools use kubectl's own config
509
+ wins and `.env` is not consulted. Without any key the agent still starts in
510
+ model-less mode (record commands + the tool layer — see "No API key?" in
511
+ section 5). Kubernetes tools use kubectl's own config
465
512
  (`~/.kube/config` or `KUBECONFIG`); no agent-side config is needed.
466
513
 
467
514
  ## 7. How to run the agent
@@ -80,23 +80,30 @@ class DevOpsAgent:
80
80
  ) -> None:
81
81
  # Configuration resolution order: explicit argument > environment > default.
82
82
  self.api_key = api_key or os.getenv("OPENROUTER_API_KEY")
83
+
84
+ self.model = model or os.getenv("OPENROUTER_MODEL", DEFAULT_MODEL)
85
+ self.base_url = base_url or os.getenv("OPENROUTER_BASE_URL", DEFAULT_BASE_URL)
86
+
87
+ # Model-less mode: a missing (or placeholder) key no longer blocks
88
+ # construction. Record operations (the slash commands) and the tool
89
+ # layer never call the model, so the agent builds with client=None
90
+ # and only the chat path (_complete) demands a working key.
91
+ self.client = None
92
+ self.model_error: str | None = None
83
93
  if not self.api_key:
84
- raise ValueError(
94
+ self.model_error = (
85
95
  "OPENROUTER_API_KEY is not set. Copy .env.example to .env and "
86
96
  "fill in your key, or export OPENROUTER_API_KEY in your shell."
87
97
  )
88
- if self.api_key.strip().lower() == PLACEHOLDER_KEY:
89
- raise ValueError(
98
+ elif self.api_key.strip().lower() == PLACEHOLDER_KEY:
99
+ self.model_error = (
90
100
  "OPENROUTER_API_KEY still has the placeholder value. Edit .env "
91
101
  "and replace `your_key_here` with your real key."
92
102
  )
93
-
94
- self.model = model or os.getenv("OPENROUTER_MODEL", DEFAULT_MODEL)
95
- self.base_url = base_url or os.getenv("OPENROUTER_BASE_URL", DEFAULT_BASE_URL)
96
-
97
- # OpenRouter exposes an OpenAI-compatible API, so the official OpenAI
98
- # SDK is a drop-in client — just pointed at OpenRouter's base URL.
99
- self.client = OpenAI(api_key=self.api_key, base_url=self.base_url)
103
+ else:
104
+ # OpenRouter exposes an OpenAI-compatible API, so the official
105
+ # OpenAI SDK is a drop-in client — just pointed at the base URL.
106
+ self.client = OpenAI(api_key=self.api_key, base_url=self.base_url)
100
107
 
101
108
  # Short-term conversation history. Starts with the system prompt;
102
109
  # grows as user, model, and tool results exchange turns.
@@ -110,7 +117,8 @@ class DevOpsAgent:
110
117
 
111
118
  The message, any tool exchanges, and the final assistant reply are all
112
119
  kept in this session's history so the model retains context across
113
- turns. Raises ValueError on empty input.
120
+ turns. Raises ValueError on empty input, or when no API key is
121
+ configured (model-less mode).
114
122
  """
115
123
  message = user_message.strip()
116
124
  if not message:
@@ -180,6 +188,10 @@ class DevOpsAgent:
180
188
  model again. Stops when the model answers in plain text, or when the
181
189
  iteration cap is reached.
182
190
  """
191
+ if self.client is None:
192
+ # Model-less construction: the first chat attempt is where the
193
+ # missing key finally surfaces, as a normal caught ValueError.
194
+ raise ValueError(self.model_error)
183
195
  for _ in range(MAX_TOOL_ITERATIONS):
184
196
  request: dict = {"model": self.model, "messages": self.messages}
185
197
  if self.tools:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devopsiq
3
- Version: 0.1.0
3
+ Version: 0.1.1
4
4
  Summary: A from-scratch, read-only AI agent that investigates real DevOps problems — Kubernetes, Docker, Helm, Argo CD, Istio, Terraform, GitHub Actions, cloud, New Relic and more.
5
5
  Author: DevOpsAbhii
6
6
  License-Expression: MIT
@@ -400,6 +400,30 @@ Swapping to another OpenRouter model later is a one-line change (an env var
400
400
  today); moving to any other OpenAI-compatible provider changes only
401
401
  `agent/agent.py` configuration.
402
402
 
403
+ **Your model, your choice.** The default is baked in as a fallback, never a
404
+ restriction — set `OPENROUTER_MODEL` (shell or `.env`) to any model on
405
+ OpenRouter:
406
+
407
+ ```bash
408
+ export OPENROUTER_MODEL=anthropic/claude-sonnet-5 # or openai/gpt-5.2, google/gemini-2.5-pro, ...
409
+ ```
410
+
411
+ Two things to weigh when picking: the agent is a tool-use loop, so choose a
412
+ model with solid function-calling (a chat-only model will answer from
413
+ imagination instead of gathering evidence); and an investigation makes
414
+ several model calls per run, so price-per-call multiplies — that is why the
415
+ default is a cheap, reliable tool-caller rather than the biggest model.
416
+
417
+ **No OpenRouter at all?** `OPENROUTER_BASE_URL` points the same client at
418
+ any OpenAI-compatible endpoint — including a local one. Ollama, free and
419
+ offline:
420
+
421
+ ```bash
422
+ export OPENROUTER_BASE_URL=http://localhost:11434/v1
423
+ export OPENROUTER_MODEL=llama3.2 # any Ollama model that does tools
424
+ devopsiq "docker container checkout keeps exiting, investigate"
425
+ ```
426
+
403
427
  ## 5. Installation
404
428
 
405
429
  Three ways — pick one. Whatever you choose, the agent also needs the CLIs of
@@ -463,6 +487,27 @@ Recommended CLIs, by domain:
463
487
  | New Relic | `curl` + `NEW_RELIC_API_KEY` / `NEW_RELIC_ACCOUNT_ID` env vars |
464
488
  | Ansible | `ansible-inventory`, `ansible-playbook` |
465
489
 
490
+ ### No API key? You still have the tools
491
+
492
+ The model is the only part that needs a key — the agent builds without one
493
+ (model-less mode), and two things keep working:
494
+
495
+ - **Record commands**: `devopsiq /report`, `devopsiq /investigations`, and
496
+ the same commands inside the REPL, all work with no key configured. Only
497
+ actual questions exit 1 with the setup message naming `OPENROUTER_API_KEY`.
498
+ - **The whole 58-tool layer** via the Python library, with no model and no
499
+ key (see INTEGRATION.md):
500
+
501
+ ```python
502
+ import agent.agent # registers every tool
503
+ from tools.registry import execute_tool
504
+
505
+ print(execute_tool("k8s_pods", '{"namespace": "prod"}'))
506
+ ```
507
+
508
+ And the fully key-free *agent* path is a local model (Ollama example in
509
+ section 4 above) — no cloud account needed at all.
510
+
466
511
  ## 6. Environment setup
467
512
 
468
513
  ```bash
@@ -490,7 +535,9 @@ NEW_RELIC_ACCOUNT_ID=1234567 # (Phase 9) numeric account
490
535
 
491
536
  `.env` is gitignored; the API key is never hard-coded in Python, printed, or
492
537
  logged. If `OPENROUTER_API_KEY` is already set in your shell, the shell value
493
- wins and `.env` is not consulted. Kubernetes tools use kubectl's own config
538
+ wins and `.env` is not consulted. Without any key the agent still starts in
539
+ model-less mode (record commands + the tool layer — see "No API key?" in
540
+ section 5). Kubernetes tools use kubectl's own config
494
541
  (`~/.kube/config` or `KUBECONFIG`); no agent-side config is needed.
495
542
 
496
543
  ## 7. How to run the agent
@@ -253,6 +253,13 @@ def main() -> int:
253
253
  print(f"[setup] {exc}", file=sys.stderr)
254
254
  return 1
255
255
 
256
+ if agent.client is None:
257
+ # Model-less mode: the REPL still starts — record commands (slash
258
+ # commands) work, only questions to the model are unavailable.
259
+ print(f"[setup] {agent.model_error}", file=sys.stderr)
260
+ print("[setup] Starting without a model — slash commands still work; "
261
+ "questions need OPENROUTER_API_KEY.", file=sys.stderr)
262
+
256
263
  tool_names = ", ".join(sorted(tool.name for tool in agent.tools)) or "none"
257
264
 
258
265
  print("DevOps AI Agent (Phase 9 — 58 read-only tools, persistent investigations)")
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "devopsiq"
7
- version = "0.1.0"
7
+ version = "0.1.1"
8
8
  description = "A from-scratch, read-only AI agent that investigates real DevOps problems — Kubernetes, Docker, Helm, Argo CD, Istio, Terraform, GitHub Actions, cloud, New Relic and more."
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -247,5 +247,58 @@ class OneShotCliTests(unittest.TestCase):
247
247
  json.loads(out) # the entire stdout must be one valid JSON document
248
248
 
249
249
 
250
+ class KeylessTests(unittest.TestCase):
251
+ """Model-less mode: no API key configured.
252
+
253
+ The agent must still construct (record commands + tool layer never call
254
+ the model); only the chat path raises, and the CLI reports it loudly.
255
+ """
256
+
257
+ def setUp(self):
258
+ self._old_key = os.environ.get("OPENROUTER_API_KEY")
259
+ os.environ.pop("OPENROUTER_API_KEY", None)
260
+ self.addCleanup(self._restore_key)
261
+ inv_tools.set_store(None)
262
+
263
+ def _restore_key(self):
264
+ os.environ.pop("OPENROUTER_API_KEY", None)
265
+ if self._old_key is not None:
266
+ os.environ["OPENROUTER_API_KEY"] = self._old_key
267
+ inv_tools.finish_investigation()
268
+ inv_tools.set_store(None)
269
+
270
+ def test_agent_constructs_without_a_key(self):
271
+ agent = DevOpsAgent()
272
+ self.assertIsNone(agent.client)
273
+ self.assertIn("OPENROUTER_API_KEY", agent.model_error)
274
+ self.assertTrue(agent.tools) # the tool layer is fully usable
275
+
276
+ def test_placeholder_key_is_also_model_less(self):
277
+ os.environ["OPENROUTER_API_KEY"] = "your_key_here"
278
+ agent = DevOpsAgent()
279
+ self.assertIsNone(agent.client)
280
+ self.assertIn("placeholder", agent.model_error)
281
+
282
+ def test_ask_without_key_raises_loud(self):
283
+ agent = DevOpsAgent()
284
+ with self.assertRaises(ValueError) as ctx:
285
+ agent.ask("why is it down?")
286
+ self.assertIn("OPENROUTER_API_KEY", str(ctx.exception))
287
+
288
+ def test_oneshot_slash_command_works_without_key(self):
289
+ out = io.StringIO()
290
+ with contextlib.redirect_stdout(out):
291
+ code = main_module.run_one_shot("/report")
292
+ self.assertEqual(code, 0)
293
+ self.assertIn("(no investigation recorded)", out.getvalue())
294
+
295
+ def test_oneshot_question_without_key_exits_1(self):
296
+ err = io.StringIO()
297
+ with contextlib.redirect_stdout(err), contextlib.redirect_stderr(err):
298
+ code = main_module.run_one_shot("why is api-5d6f crash-looping?")
299
+ self.assertEqual(code, 1)
300
+ self.assertIn("OPENROUTER_API_KEY", err.getvalue())
301
+
302
+
250
303
  if __name__ == "__main__":
251
304
  unittest.main()
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes