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.
- {devopsiq-0.1.0/devopsiq.egg-info → devopsiq-0.1.1}/PKG-INFO +49 -2
- {devopsiq-0.1.0 → devopsiq-0.1.1}/README.md +48 -1
- {devopsiq-0.1.0 → devopsiq-0.1.1}/agent/agent.py +23 -11
- {devopsiq-0.1.0 → devopsiq-0.1.1/devopsiq.egg-info}/PKG-INFO +49 -2
- {devopsiq-0.1.0 → devopsiq-0.1.1}/main.py +7 -0
- {devopsiq-0.1.0 → devopsiq-0.1.1}/pyproject.toml +1 -1
- {devopsiq-0.1.0 → devopsiq-0.1.1}/tests/test_automation.py +53 -0
- {devopsiq-0.1.0 → devopsiq-0.1.1}/LICENSE +0 -0
- {devopsiq-0.1.0 → devopsiq-0.1.1}/agent/__init__.py +0 -0
- {devopsiq-0.1.0 → devopsiq-0.1.1}/agent/investigation.py +0 -0
- {devopsiq-0.1.0 → devopsiq-0.1.1}/agent/prompts.py +0 -0
- {devopsiq-0.1.0 → devopsiq-0.1.1}/agent/store.py +0 -0
- {devopsiq-0.1.0 → devopsiq-0.1.1}/devopsiq.egg-info/SOURCES.txt +0 -0
- {devopsiq-0.1.0 → devopsiq-0.1.1}/devopsiq.egg-info/dependency_links.txt +0 -0
- {devopsiq-0.1.0 → devopsiq-0.1.1}/devopsiq.egg-info/entry_points.txt +0 -0
- {devopsiq-0.1.0 → devopsiq-0.1.1}/devopsiq.egg-info/requires.txt +0 -0
- {devopsiq-0.1.0 → devopsiq-0.1.1}/devopsiq.egg-info/top_level.txt +0 -0
- {devopsiq-0.1.0 → devopsiq-0.1.1}/setup.cfg +0 -0
- {devopsiq-0.1.0 → devopsiq-0.1.1}/tests/test_phase2.py +0 -0
- {devopsiq-0.1.0 → devopsiq-0.1.1}/tests/test_phase3.py +0 -0
- {devopsiq-0.1.0 → devopsiq-0.1.1}/tests/test_phase4.py +0 -0
- {devopsiq-0.1.0 → devopsiq-0.1.1}/tests/test_phase5.py +0 -0
- {devopsiq-0.1.0 → devopsiq-0.1.1}/tests/test_phase7.py +0 -0
- {devopsiq-0.1.0 → devopsiq-0.1.1}/tests/test_phase8.py +0 -0
- {devopsiq-0.1.0 → devopsiq-0.1.1}/tests/test_phase9.py +0 -0
- {devopsiq-0.1.0 → devopsiq-0.1.1}/tools/__init__.py +0 -0
- {devopsiq-0.1.0 → devopsiq-0.1.1}/tools/ansible.py +0 -0
- {devopsiq-0.1.0 → devopsiq-0.1.1}/tools/argocd.py +0 -0
- {devopsiq-0.1.0 → devopsiq-0.1.1}/tools/base.py +0 -0
- {devopsiq-0.1.0 → devopsiq-0.1.1}/tools/cloud.py +0 -0
- {devopsiq-0.1.0 → devopsiq-0.1.1}/tools/docker.py +0 -0
- {devopsiq-0.1.0 → devopsiq-0.1.1}/tools/git_ci.py +0 -0
- {devopsiq-0.1.0 → devopsiq-0.1.1}/tools/helm.py +0 -0
- {devopsiq-0.1.0 → devopsiq-0.1.1}/tools/investigation.py +0 -0
- {devopsiq-0.1.0 → devopsiq-0.1.1}/tools/istio.py +0 -0
- {devopsiq-0.1.0 → devopsiq-0.1.1}/tools/kubernetes.py +0 -0
- {devopsiq-0.1.0 → devopsiq-0.1.1}/tools/monitoring.py +0 -0
- {devopsiq-0.1.0 → devopsiq-0.1.1}/tools/newrelic.py +0 -0
- {devopsiq-0.1.0 → devopsiq-0.1.1}/tools/preflight.py +0 -0
- {devopsiq-0.1.0 → devopsiq-0.1.1}/tools/registry.py +0 -0
- {devopsiq-0.1.0 → devopsiq-0.1.1}/tools/system.py +0 -0
- {devopsiq-0.1.0 → devopsiq-0.1.1}/tools/terraform.py +0 -0
- {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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
89
|
-
|
|
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
|
-
|
|
95
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|