agentic-ops 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.
- agentic_ops/adapters/cac_pull_collector.py +413 -0
- agentic_ops/adapters/datadog_adapter.py +33 -0
- agentic_ops/adapters/kubernetes_adapter.py +73 -0
- agentic_ops/adapters/llm_protocol.py +366 -0
- agentic_ops/adapters/llm_provider_registry.py +510 -0
- agentic_ops/adapters/protocol.py +19 -0
- agentic_ops/adapters/registry_adapter.py +70 -0
- agentic_ops/adapters/runbook_rag.py +21 -0
- agentic_ops/adapters/slack_adapter.py +78 -0
- agentic_ops/cli/compile_taxonomy.py +66 -0
- agentic_ops/cli/main.py +3192 -0
- agentic_ops/cli/trust_ledger.py +150 -0
- agentic_ops/core/__init__.py +1 -0
- agentic_ops/core/approvals.py +137 -0
- agentic_ops/core/approvers.py +32 -0
- agentic_ops/core/circuit_breaker.py +39 -0
- agentic_ops/core/contracts.py +103 -0
- agentic_ops/core/crypto.py +37 -0
- agentic_ops/core/distributed.py +29 -0
- agentic_ops/core/evidence.py +143 -0
- agentic_ops/core/executor.py +181 -0
- agentic_ops/core/facts.py +29 -0
- agentic_ops/core/governance_schemas.py +25 -0
- agentic_ops/core/idempotency.py +52 -0
- agentic_ops/core/infra_specialist.py +52 -0
- agentic_ops/core/model_specialist.py +51 -0
- agentic_ops/core/orchestrator.py +281 -0
- agentic_ops/core/policy.py +68 -0
- agentic_ops/core/policy_engine.py +166 -0
- agentic_ops/core/prompt_guard.py +16 -0
- agentic_ops/core/remediation_agent.py +49 -0
- agentic_ops/core/specialist.py +52 -0
- agentic_ops/core/state.py +20 -0
- agentic_ops/core/telemetry.py +47 -0
- agentic_ops/entrypoints/approval_sweeper.py +6 -0
- agentic_ops/entrypoints/grafana_webhook.py +61 -0
- agentic_ops/entrypoints/mcp_server.py +1149 -0
- agentic_ops/entrypoints/slack_verify.py +24 -0
- agentic_ops/entrypoints/slack_webhook.py +59 -0
- agentic_ops/evidence_store/EP-0001.json +64 -0
- agentic_ops/evidence_store/EP-0002.json +58 -0
- agentic_ops/evidence_store/EP-0003.json +58 -0
- agentic_ops/evidence_store/EP-0004.json +64 -0
- agentic_ops/evidence_store/EP-0005.json +65 -0
- agentic_ops/evidence_store/EP-CHAOS-31c6e006.json +27 -0
- agentic_ops/evidence_store/EP-CHAOS-5950095d.json +28 -0
- agentic_ops/evidence_store/EP-CHAOS-d6c18601.json +28 -0
- agentic_ops/evidence_store/_index.json +4 -0
- agentic_ops/governance/action_catalog.yaml +26 -0
- agentic_ops/governance/approvers.yaml +7 -0
- agentic_ops/governance/autonomy_policy.yaml +23 -0
- agentic_ops/governance/risk_register.yaml +6 -0
- agentic_ops/governance/shadow_policy.yaml +23 -0
- agentic_ops/knowledge_base/runbooks/DATA-042.md +8 -0
- agentic_ops/knowledge_base/runbooks/INFRA-001.md +8 -0
- agentic_ops/schemas/action.schema.json +13 -0
- agentic_ops/schemas/diagnosis.schema.json +11 -0
- agentic_ops/schemas/evidence.schema.json +12 -0
- agentic_ops/schemas/risk.schema.json +14 -0
- agentic_ops/tools/__init__.py +0 -0
- agentic_ops/tools/base.py +80 -0
- agentic_ops/tools/data.py +77 -0
- agentic_ops/tools/github.py +30 -0
- agentic_ops/tools/infra.py +51 -0
- agentic_ops/tools/model.py +29 -0
- agentic_ops/tools/observability.py +56 -0
- agentic_ops-0.1.0.dist-info/METADATA +1489 -0
- agentic_ops-0.1.0.dist-info/RECORD +71 -0
- agentic_ops-0.1.0.dist-info/WHEEL +4 -0
- agentic_ops-0.1.0.dist-info/entry_points.txt +4 -0
- agentic_ops-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
"""
|
|
2
|
+
LLM Adapters
|
|
3
|
+
============
|
|
4
|
+
Définit le Protocol LLMPort (port hexagonal), LiteLLMAdapter (universel),
|
|
5
|
+
OllamaAdapter (local/Tailscale/Cloudflare), FallbackLLMAdapter (chaîne de
|
|
6
|
+
secours automatique), et _ProbeResponse (healthcheck).
|
|
7
|
+
|
|
8
|
+
Métriques Prometheus instrumentées :
|
|
9
|
+
agentic_ops_llm_requests_total (provider, model, status)
|
|
10
|
+
agentic_ops_llm_latency_seconds (provider, model)
|
|
11
|
+
agentic_ops_llm_tokens_total (provider, model, token_type)
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import logging
|
|
17
|
+
import os
|
|
18
|
+
import re
|
|
19
|
+
import time
|
|
20
|
+
from typing import Protocol, TypeVar
|
|
21
|
+
|
|
22
|
+
import httpx
|
|
23
|
+
from pydantic import BaseModel
|
|
24
|
+
from prometheus_client import Counter, Histogram
|
|
25
|
+
from tenacity import (
|
|
26
|
+
retry,
|
|
27
|
+
retry_if_exception_type,
|
|
28
|
+
stop_after_attempt,
|
|
29
|
+
wait_exponential,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
logger = logging.getLogger("LLMAdapter")
|
|
33
|
+
|
|
34
|
+
T = TypeVar("T", bound=BaseModel)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _strip_think_tags(text: str) -> str:
|
|
38
|
+
"""
|
|
39
|
+
Règle de l'Honnêteté Radicale : Les modèles de raisonnement pur (comme DeepSeek-R1)
|
|
40
|
+
émettent des balises <think>...</think> contenant leur chaîne de pensée.
|
|
41
|
+
Cette fonction élimine ces blocs pour isoler le JSON pur attendu par Pydantic.
|
|
42
|
+
"""
|
|
43
|
+
if not text:
|
|
44
|
+
return text
|
|
45
|
+
# Enlève tout le bloc <think>...</think> (multiligne)
|
|
46
|
+
cleaned = re.sub(r"<think>.*?</think>", "", text, flags=re.DOTALL)
|
|
47
|
+
# Enlève d'éventuels backticks markdown ```json ... ```
|
|
48
|
+
cleaned = re.sub(r"^```(?:json)?\s*", "", cleaned.strip(), flags=re.MULTILINE)
|
|
49
|
+
cleaned = re.sub(r"\s*```$", "", cleaned.strip())
|
|
50
|
+
return cleaned.strip()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# ---------------------------------------------------------------------------
|
|
54
|
+
# Prometheus metrics
|
|
55
|
+
# ---------------------------------------------------------------------------
|
|
56
|
+
|
|
57
|
+
LLM_REQUESTS = Counter(
|
|
58
|
+
"agentic_ops_llm_requests_total",
|
|
59
|
+
"Total LLM requests by role, provider, model and status",
|
|
60
|
+
["role", "provider", "model", "status"],
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
LLM_LATENCY = Histogram(
|
|
64
|
+
"agentic_ops_llm_latency_seconds",
|
|
65
|
+
"LLM request latency in seconds",
|
|
66
|
+
["role", "provider", "model"],
|
|
67
|
+
buckets=[0.5, 1.0, 2.0, 5.0, 10.0, 20.0, 30.0, 60.0],
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
LLM_TOKENS = Counter(
|
|
71
|
+
"agentic_ops_llm_tokens_total",
|
|
72
|
+
"Total LLM tokens consumed by role, provider, model and token type",
|
|
73
|
+
["role", "provider", "model", "token_type"],
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# ---------------------------------------------------------------------------
|
|
78
|
+
# Protocol
|
|
79
|
+
# ---------------------------------------------------------------------------
|
|
80
|
+
|
|
81
|
+
class LLMPort(Protocol):
|
|
82
|
+
def complete(
|
|
83
|
+
self,
|
|
84
|
+
*,
|
|
85
|
+
system: str,
|
|
86
|
+
user: str,
|
|
87
|
+
response_model: type[T],
|
|
88
|
+
temperature: float = 0.0,
|
|
89
|
+
) -> T:
|
|
90
|
+
...
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# ---------------------------------------------------------------------------
|
|
94
|
+
# Probe response model (minimal, used by /api/llm/probe)
|
|
95
|
+
# ---------------------------------------------------------------------------
|
|
96
|
+
|
|
97
|
+
class _ProbeResponse(BaseModel):
|
|
98
|
+
answer: str
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class _ChatResponse(BaseModel):
|
|
102
|
+
answer: str
|
|
103
|
+
action_type: str = "investigate"
|
|
104
|
+
confidence_score: float = 0.95
|
|
105
|
+
rationale: str = ""
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
# ---------------------------------------------------------------------------
|
|
109
|
+
# LiteLLMAdapter — universal adapter (OpenRouter, OpenAI, Gemini, Anthropic,
|
|
110
|
+
# Azure, Mistral, HuggingFace, ...)
|
|
111
|
+
# ---------------------------------------------------------------------------
|
|
112
|
+
|
|
113
|
+
class LiteLLMAdapter:
|
|
114
|
+
"""
|
|
115
|
+
Adaptateur LiteLLM universel avec support de Structured Output via
|
|
116
|
+
Instructor, Prometheus metrics, et retry exponentiel.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
model : Modèle au format LiteLLM (ex: "openrouter/deepseek/deepseek-r1:free")
|
|
120
|
+
temperature : Température de génération (0.0 = déterministe)
|
|
121
|
+
max_tokens : Limite de tokens de sortie
|
|
122
|
+
timeout : Timeout réseau par appel (secondes)
|
|
123
|
+
max_retries : Nombre de tentatives avant exception
|
|
124
|
+
seed : Graine pour la reproductibilité (si supporté par le modèle)
|
|
125
|
+
"""
|
|
126
|
+
|
|
127
|
+
def __init__(
|
|
128
|
+
self,
|
|
129
|
+
model: str = "openrouter/deepseek/deepseek-r1:free",
|
|
130
|
+
temperature: float = 0.0,
|
|
131
|
+
max_tokens: int = 1024,
|
|
132
|
+
timeout: int = 30,
|
|
133
|
+
max_retries: int = 3,
|
|
134
|
+
seed: int | None = None,
|
|
135
|
+
role: str = "global",
|
|
136
|
+
):
|
|
137
|
+
import instructor
|
|
138
|
+
import litellm
|
|
139
|
+
|
|
140
|
+
self.model = model
|
|
141
|
+
self.temperature = temperature
|
|
142
|
+
self.max_tokens = max_tokens
|
|
143
|
+
self.timeout = timeout
|
|
144
|
+
self.max_retries = max_retries
|
|
145
|
+
self.seed = seed
|
|
146
|
+
self.role = role
|
|
147
|
+
|
|
148
|
+
# Derive provider label for Prometheus (first segment before '/')
|
|
149
|
+
self._provider_label = model.split("/")[0] if "/" in model else "unknown"
|
|
150
|
+
# Safe model label (strip :free tag for readability)
|
|
151
|
+
self._model_label = model.split(":")[0].split("/")[-1]
|
|
152
|
+
|
|
153
|
+
litellm.request_timeout = timeout
|
|
154
|
+
self._client = instructor.from_litellm(litellm.completion)
|
|
155
|
+
|
|
156
|
+
def complete(
|
|
157
|
+
self,
|
|
158
|
+
*,
|
|
159
|
+
system: str,
|
|
160
|
+
user: str,
|
|
161
|
+
response_model: type[T],
|
|
162
|
+
temperature: float | None = None,
|
|
163
|
+
) -> T:
|
|
164
|
+
return self._complete_with_metrics(
|
|
165
|
+
system=system,
|
|
166
|
+
user=user,
|
|
167
|
+
response_model=response_model,
|
|
168
|
+
temperature=temperature if temperature is not None else self.temperature,
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
@retry(
|
|
172
|
+
stop=stop_after_attempt(3),
|
|
173
|
+
wait=wait_exponential(multiplier=1, min=2, max=10),
|
|
174
|
+
retry=retry_if_exception_type(Exception),
|
|
175
|
+
reraise=True,
|
|
176
|
+
)
|
|
177
|
+
def _complete_with_metrics(
|
|
178
|
+
self,
|
|
179
|
+
*,
|
|
180
|
+
system: str,
|
|
181
|
+
user: str,
|
|
182
|
+
response_model: type[T],
|
|
183
|
+
temperature: float,
|
|
184
|
+
) -> T:
|
|
185
|
+
t0 = time.monotonic()
|
|
186
|
+
try:
|
|
187
|
+
kwargs: dict = dict(
|
|
188
|
+
model=self.model,
|
|
189
|
+
response_model=response_model,
|
|
190
|
+
temperature=temperature,
|
|
191
|
+
max_tokens=self.max_tokens,
|
|
192
|
+
messages=[
|
|
193
|
+
{"role": "system", "content": system},
|
|
194
|
+
{"role": "user", "content": user},
|
|
195
|
+
],
|
|
196
|
+
)
|
|
197
|
+
if self.seed is not None:
|
|
198
|
+
kwargs["seed"] = self.seed
|
|
199
|
+
|
|
200
|
+
result = self._client.chat.completions.create(**kwargs)
|
|
201
|
+
elapsed = time.monotonic() - t0
|
|
202
|
+
LLM_REQUESTS.labels(
|
|
203
|
+
role=self.role,
|
|
204
|
+
provider=self._provider_label,
|
|
205
|
+
model=self._model_label,
|
|
206
|
+
status="success",
|
|
207
|
+
).inc()
|
|
208
|
+
LLM_LATENCY.labels(
|
|
209
|
+
role=self.role, provider=self._provider_label, model=self._model_label
|
|
210
|
+
).observe(elapsed)
|
|
211
|
+
return result
|
|
212
|
+
except Exception as exc:
|
|
213
|
+
elapsed = time.monotonic() - t0
|
|
214
|
+
LLM_REQUESTS.labels(
|
|
215
|
+
role=self.role,
|
|
216
|
+
provider=self._provider_label,
|
|
217
|
+
model=self._model_label,
|
|
218
|
+
status="error",
|
|
219
|
+
).inc()
|
|
220
|
+
LLM_LATENCY.labels(
|
|
221
|
+
role=self.role, provider=self._provider_label, model=self._model_label
|
|
222
|
+
).observe(elapsed)
|
|
223
|
+
logger.warning(
|
|
224
|
+
"LiteLLMAdapter error on model=%s: %s", self.model, exc
|
|
225
|
+
)
|
|
226
|
+
raise
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
# ---------------------------------------------------------------------------
|
|
230
|
+
# OllamaAdapter — appel direct httpx (local, Tailscale, Cloudflare, Vast.ai)
|
|
231
|
+
# ---------------------------------------------------------------------------
|
|
232
|
+
|
|
233
|
+
class OllamaAdapter:
|
|
234
|
+
"""
|
|
235
|
+
Adaptateur Ollama léger via httpx.
|
|
236
|
+
|
|
237
|
+
Fonctionne avec :
|
|
238
|
+
- Ollama local : OLLAMA_BASE_URL=http://localhost:11434
|
|
239
|
+
- Ollama via Tailscale : OLLAMA_BASE_URL=http://100.X.X.X:11434
|
|
240
|
+
- Ollama via Cloudflare Tunnel : OLLAMA_BASE_URL=https://ollama.mondomaine.com
|
|
241
|
+
- Ollama sur Vast.ai / VPS : OLLAMA_BASE_URL=http://IP_PUBLIQUE:11434
|
|
242
|
+
|
|
243
|
+
Aucune clé API requise.
|
|
244
|
+
"""
|
|
245
|
+
|
|
246
|
+
def __init__(
|
|
247
|
+
self,
|
|
248
|
+
base_url: str = "http://localhost:11434",
|
|
249
|
+
model: str = "llama3.2:3b",
|
|
250
|
+
timeout: int = 60,
|
|
251
|
+
role: str = "global",
|
|
252
|
+
):
|
|
253
|
+
self.base_url = base_url.rstrip("/")
|
|
254
|
+
self.model = model
|
|
255
|
+
self.timeout = timeout
|
|
256
|
+
self.role = role
|
|
257
|
+
self._provider_label = "ollama"
|
|
258
|
+
self._model_label = model.split(":")[0]
|
|
259
|
+
|
|
260
|
+
def complete(
|
|
261
|
+
self,
|
|
262
|
+
*,
|
|
263
|
+
system: str,
|
|
264
|
+
user: str,
|
|
265
|
+
response_model: type[T],
|
|
266
|
+
temperature: float = 0.0,
|
|
267
|
+
) -> T:
|
|
268
|
+
import json
|
|
269
|
+
|
|
270
|
+
payload = {
|
|
271
|
+
"model": self.model,
|
|
272
|
+
"messages": [
|
|
273
|
+
{"role": "system", "content": system},
|
|
274
|
+
{"role": "user", "content": user},
|
|
275
|
+
],
|
|
276
|
+
"stream": False,
|
|
277
|
+
"options": {"temperature": temperature},
|
|
278
|
+
"format": "json",
|
|
279
|
+
}
|
|
280
|
+
t0 = time.monotonic()
|
|
281
|
+
try:
|
|
282
|
+
with httpx.Client(timeout=self.timeout) as client:
|
|
283
|
+
resp = client.post(
|
|
284
|
+
f"{self.base_url}/api/chat",
|
|
285
|
+
json=payload,
|
|
286
|
+
)
|
|
287
|
+
resp.raise_for_status()
|
|
288
|
+
data = resp.json()
|
|
289
|
+
content = data.get("message", {}).get("content", "")
|
|
290
|
+
# [Honnêteté Radicale / Modèles de Raisonnement R1]
|
|
291
|
+
# Nettoyer les balises <think>...</think> avant parsing JSON
|
|
292
|
+
content = _strip_think_tags(content)
|
|
293
|
+
# Parse JSON and validate against response_model
|
|
294
|
+
parsed = json.loads(content)
|
|
295
|
+
result = response_model.model_validate(parsed)
|
|
296
|
+
elapsed = time.monotonic() - t0
|
|
297
|
+
LLM_REQUESTS.labels(
|
|
298
|
+
role=self.role, provider="ollama", model=self._model_label, status="success"
|
|
299
|
+
).inc()
|
|
300
|
+
LLM_LATENCY.labels(
|
|
301
|
+
role=self.role, provider="ollama", model=self._model_label
|
|
302
|
+
).observe(elapsed)
|
|
303
|
+
return result
|
|
304
|
+
except Exception as exc:
|
|
305
|
+
elapsed = time.monotonic() - t0
|
|
306
|
+
LLM_REQUESTS.labels(
|
|
307
|
+
role=self.role, provider="ollama", model=self._model_label, status="error"
|
|
308
|
+
).inc()
|
|
309
|
+
LLM_LATENCY.labels(
|
|
310
|
+
role=self.role, provider="ollama", model=self._model_label
|
|
311
|
+
).observe(elapsed)
|
|
312
|
+
logger.warning("OllamaAdapter error on %s/%s: %s", self.base_url, self.model, exc)
|
|
313
|
+
raise
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
# ---------------------------------------------------------------------------
|
|
317
|
+
# FallbackLLMAdapter — chaîne de secours automatique
|
|
318
|
+
# ---------------------------------------------------------------------------
|
|
319
|
+
|
|
320
|
+
class FallbackLLMAdapter:
|
|
321
|
+
"""
|
|
322
|
+
Chaîne de secours : si le modèle primaire échoue (RateLimitError, timeout,
|
|
323
|
+
erreur 5xx), tente automatiquement chaque modèle de la liste de fallback
|
|
324
|
+
dans l'ordre jusqu'à obtenir une réponse valide.
|
|
325
|
+
|
|
326
|
+
Prometheus : chaque tentative est instrumentée individuellement.
|
|
327
|
+
"""
|
|
328
|
+
|
|
329
|
+
def __init__(
|
|
330
|
+
self,
|
|
331
|
+
primary: LiteLLMAdapter | OllamaAdapter,
|
|
332
|
+
fallbacks: list[LiteLLMAdapter | OllamaAdapter] | None = None,
|
|
333
|
+
):
|
|
334
|
+
self.primary = primary
|
|
335
|
+
self.fallbacks = fallbacks or []
|
|
336
|
+
|
|
337
|
+
def complete(
|
|
338
|
+
self,
|
|
339
|
+
*,
|
|
340
|
+
system: str,
|
|
341
|
+
user: str,
|
|
342
|
+
response_model: type[T],
|
|
343
|
+
temperature: float = 0.0,
|
|
344
|
+
) -> T:
|
|
345
|
+
chain = [self.primary] + self.fallbacks
|
|
346
|
+
last_exc: Exception | None = None
|
|
347
|
+
for i, adapter in enumerate(chain):
|
|
348
|
+
label = getattr(adapter, "model", "unknown")
|
|
349
|
+
try:
|
|
350
|
+
logger.debug("FallbackLLMAdapter: trying adapter %d (%s)", i, label)
|
|
351
|
+
return adapter.complete(
|
|
352
|
+
system=system,
|
|
353
|
+
user=user,
|
|
354
|
+
response_model=response_model,
|
|
355
|
+
temperature=temperature,
|
|
356
|
+
)
|
|
357
|
+
except Exception as exc:
|
|
358
|
+
last_exc = exc
|
|
359
|
+
logger.warning(
|
|
360
|
+
"FallbackLLMAdapter: adapter %d (%s) failed: %s — trying next.",
|
|
361
|
+
i, label, exc,
|
|
362
|
+
)
|
|
363
|
+
raise RuntimeError(
|
|
364
|
+
f"All {len(chain)} LLM adapters in the fallback chain failed. "
|
|
365
|
+
f"Last error: {last_exc}"
|
|
366
|
+
) from last_exc
|