devops-bot-sdk 1.4.149__tar.gz → 1.4.151__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.
- {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/PKG-INFO +1 -1
- {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/devops_bot_sdk.egg-info/PKG-INFO +1 -1
- {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/__init__.py +2 -2
- {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/agents/inventory.py +1 -0
- {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/agents/prompt_studio.py +51 -0
- {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/client.py +57 -0
- {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/config.py +4 -1
- {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/ui/api.py +89 -7
- {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/updater.py +3 -3
- {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/README.md +0 -0
- {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/devops_bot_sdk.egg-info/SOURCES.txt +0 -0
- {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/devops_bot_sdk.egg-info/dependency_links.txt +0 -0
- {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/devops_bot_sdk.egg-info/entry_points.txt +0 -0
- {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/devops_bot_sdk.egg-info/requires.txt +0 -0
- {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/devops_bot_sdk.egg-info/top_level.txt +0 -0
- {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/pyproject.toml +0 -0
- {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/agentd.py +0 -0
- {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/agents/__init__.py +0 -0
- {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/agents/builder.py +0 -0
- {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/agents/chat.py +0 -0
- {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/agents/connectors.py +0 -0
- {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/agents/engines/__init__.py +0 -0
- {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/agents/engines/claude_code.py +0 -0
- {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/agents/engines/http.py +0 -0
- {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/agents/engines/mcp.py +0 -0
- {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/agents/engines/n8n.py +0 -0
- {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/agents/engines/shell.py +0 -0
- {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/agents/integrations.py +0 -0
- {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/agents/library/__init__.py +0 -0
- {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/agents/manifest.py +0 -0
- {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/agents/prompt_template.py +0 -0
- {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/agents/prompt_vars.py +0 -0
- {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/agents/run_log.py +0 -0
- {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/agents/secrets.py +0 -0
- {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/agents/usage.py +0 -0
- {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/agents/varstore.py +0 -0
- {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/cli.py +0 -0
- {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/collectors/__init__.py +0 -0
- {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/collectors/files.py +0 -0
- {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/collectors/process.py +0 -0
- {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/collectors/screenshot.py +0 -0
- {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/crucial.py +0 -0
- {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/design_verify/__init__.py +0 -0
- {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/design_verify/assets.py +0 -0
- {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/design_verify/bootstrap.py +0 -0
- {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/design_verify/browser.py +0 -0
- {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/design_verify/compare.py +0 -0
- {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/design_verify/loop.py +0 -0
- {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/exceptions.py +0 -0
- {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/git_ops.py +0 -0
- {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/graphify.py +0 -0
- {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/hooks/__init__.py +0 -0
- {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/hooks/crucial_guard.py +0 -0
- {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/ipc/__init__.py +0 -0
- {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/ipc/electron_bridge.py +0 -0
- {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/ipc/handlers.py +0 -0
- {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/local_exec.py +0 -0
- {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/models/__init__.py +0 -0
- {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/models/envelope.py +0 -0
- {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/models/requests.py +0 -0
- {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/models/responses.py +0 -0
- {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/models/snapshots.py +0 -0
- {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/platform_compat.py +0 -0
- {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/py.typed +0 -0
- {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/run_auto.py +0 -0
- {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/sse.py +0 -0
- {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/test.py +0 -0
- {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/test_pipeline.py +0 -0
- {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/ui/__init__.py +0 -0
- {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/ui/dashboard.py +0 -0
- {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/ui/health.py +0 -0
- {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/ui/ip_allowlist.py +0 -0
- {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/ui/jobs.py +0 -0
- {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/ui/models.py +0 -0
- {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/ui/runner.py +0 -0
- {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/ui/security.py +0 -0
- {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/ui/server.py +0 -0
- {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/ui/service.py +0 -0
- {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/ui/setup_manager.py +0 -0
- {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/ui/static/agents.html +0 -0
- {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/ui/static/app.js +0 -0
- {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/ui/static/custom-agents.html +0 -0
- {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/ui/static/default-agents.html +0 -0
- {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/ui/static/index.html +0 -0
- {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/ui/static/runs.html +0 -0
- {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/ui/static/setup.html +0 -0
- {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/ui/static/styles.css +0 -0
- {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/ui/static/tickets.html +0 -0
- {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/setup.cfg +0 -0
- {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/tests/test_agent_chat_features.py +0 -0
- {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/tests/test_agent_prompt_models.py +0 -0
- {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/tests/test_agents_connectors_builder.py +0 -0
- {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/tests/test_git_action.py +0 -0
- {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/tests/test_health.py +0 -0
- {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/tests/test_updater_extras.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""AgentOS Desktop SDK — thin HTTPS/SSE client for the Electron app.
|
|
2
2
|
|
|
3
|
-
Version: 1.4.
|
|
3
|
+
Version: 1.4.151
|
|
4
4
|
|
|
5
5
|
Public surface:
|
|
6
6
|
BackendClient.from_config() — create client from ~/.agentos/config.toml
|
|
@@ -30,7 +30,7 @@ Rules:
|
|
|
30
30
|
- All data egress through submit_webhook only
|
|
31
31
|
"""
|
|
32
32
|
|
|
33
|
-
__version__ = "1.4.
|
|
33
|
+
__version__ = "1.4.151" # SINGLE SOURCE OF TRUTH — bump on every change; pyproject,
|
|
34
34
|
__author__ = "AgentOS" # sdk.client.SDK_VERSION and the UI server version all read this.
|
|
35
35
|
|
|
36
36
|
from sdk.client import BackendClient
|
|
@@ -26,6 +26,7 @@ def _map_backend_agent(row: dict, source: str) -> dict:
|
|
|
26
26
|
sys_prompt = prompt.get("system_prompt", "") if isinstance(prompt, dict) else ""
|
|
27
27
|
pvars = prompt.get("variables", []) if isinstance(prompt, dict) else []
|
|
28
28
|
return {
|
|
29
|
+
"id": row.get("id"), # backend agentId — used for activity webhook
|
|
29
30
|
"name": row.get("name"),
|
|
30
31
|
"description": sys_prompt[:200],
|
|
31
32
|
"variables": [v.get("name") for v in pvars if isinstance(v, dict) and v.get("name")],
|
|
@@ -164,6 +164,57 @@ def build_regenerate_prompt(system_prompt: str, feedback: str = "",
|
|
|
164
164
|
)
|
|
165
165
|
|
|
166
166
|
|
|
167
|
+
_VALIDATE_INPUTS_INSTRUCTION = """You are screening the inputs a user typed to CREATE an AI work agent, BEFORE a system prompt is generated. Decide whether each field is appropriate and meaningful.
|
|
168
|
+
|
|
169
|
+
Mark a field invalid when it is:
|
|
170
|
+
- offensive, abusive, hateful, harassing, or sexual;
|
|
171
|
+
- gibberish, random characters, or meaningless filler;
|
|
172
|
+
- clearly not what the field is for — in particular a JOB that does not describe an actual task/objective (e.g. a personal message like "shabana I love u", a person's name, or random words), an AGENT NAME that is not a plausible name for an agent, or an AGENT TYPE that is not a sensible category (e.g. developer, documentation, qa, support, data).
|
|
173
|
+
|
|
174
|
+
Ignore any field whose value is empty. Be reasonable: a short-but-genuine job (e.g. "summarise a repo") is VALID. Only flag inputs that are clearly wrong.
|
|
175
|
+
|
|
176
|
+
Return ONLY a JSON object, no prose:
|
|
177
|
+
{"valid": true|false, "issues": {"agent_name": "<short polite fix instruction>", "agent_type": "<...>", "job": "<...>"}}
|
|
178
|
+
Include a key in "issues" ONLY for a field you marked invalid; each message is a short, polite instruction to the user (e.g. "Kindly update the Job — it should describe the task the agent will perform."). If everything is fine, return {"valid": true, "issues": {}}.
|
|
179
|
+
|
|
180
|
+
INPUTS TO SCREEN:
|
|
181
|
+
AGENT NAME: __NAME__
|
|
182
|
+
AGENT TYPE: __TYPE__
|
|
183
|
+
MAIN JOB: __JOB__
|
|
184
|
+
"""
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def build_validate_inputs_prompt(agent_name: str, agent_type: str, job: str) -> str:
|
|
188
|
+
"""Prompt that asks the engine to screen agent_name/agent_type/job for
|
|
189
|
+
offensive or meaningless input before generation/regeneration."""
|
|
190
|
+
return (
|
|
191
|
+
_VALIDATE_INPUTS_INSTRUCTION
|
|
192
|
+
.replace("__NAME__", (agent_name or "").strip() or "(empty)")
|
|
193
|
+
.replace("__TYPE__", (agent_type or "").strip() or "(empty)")
|
|
194
|
+
.replace("__JOB__", (job or "").strip() or "(empty)")
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def parse_validation(model_output: str) -> dict:
|
|
199
|
+
"""Parse the input-screening reply into ``{"valid": bool, "issues": {field: msg}}``.
|
|
200
|
+
|
|
201
|
+
Tolerant of fences/prose. Only the three known fields are kept. Defaults to
|
|
202
|
+
VALID on an unparseable reply so the caller can fail-open (proceed to generate).
|
|
203
|
+
"""
|
|
204
|
+
data = _extract_json(model_output)
|
|
205
|
+
if not isinstance(data, dict):
|
|
206
|
+
return {"valid": True, "issues": {}}
|
|
207
|
+
raw_issues = data.get("issues") if isinstance(data.get("issues"), dict) else {}
|
|
208
|
+
issues = {
|
|
209
|
+
k: str(v).strip()
|
|
210
|
+
for k, v in raw_issues.items()
|
|
211
|
+
if k in ("agent_name", "agent_type", "job") and str(v or "").strip()
|
|
212
|
+
}
|
|
213
|
+
# Trust an explicit False, but also treat "issues present" as invalid.
|
|
214
|
+
valid = bool(data.get("valid", True)) and not issues
|
|
215
|
+
return {"valid": valid, "issues": issues}
|
|
216
|
+
|
|
217
|
+
|
|
167
218
|
def parse_generation(model_output: str) -> dict:
|
|
168
219
|
"""Parse a generate/regenerate model reply into ``{system_prompt, variables}``.
|
|
169
220
|
|
|
@@ -37,6 +37,11 @@ from sdk.sse import _check_status, stream_with_reconnect
|
|
|
37
37
|
|
|
38
38
|
logger = logging.getLogger(__name__)
|
|
39
39
|
|
|
40
|
+
# Process-lifetime cache of agent NAME → backend agentId (""=known non-backend).
|
|
41
|
+
# Used by resolve_agent_id so the agent-activity webhook fires only for backend
|
|
42
|
+
# agents without a lookup on every run.
|
|
43
|
+
_AGENT_ID_CACHE: dict[str, str] = {}
|
|
44
|
+
|
|
40
45
|
# ── Ticket status-change observers ────────────────────────────────────────
|
|
41
46
|
# Best-effort, synchronous hooks fired when the client changes a ticket's backend
|
|
42
47
|
# status (In Progress / Backlog / To Do). The CLI loop registers one to track
|
|
@@ -1036,6 +1041,58 @@ class BackendClient:
|
|
|
1036
1041
|
logger.warning("client.log_activity error err=%s", exc)
|
|
1037
1042
|
return False
|
|
1038
1043
|
|
|
1044
|
+
async def log_agent_activity(self, activity: str, agent_id: str) -> bool:
|
|
1045
|
+
"""POST {ML_GATEWAY_URL}/ml-api/agent-activity — record an agent-activity
|
|
1046
|
+
event for a **backend** agent (body ``{activity, agentId}``). Best-effort;
|
|
1047
|
+
never raises. No-op when ``agent_id`` is empty (local-only agents are not
|
|
1048
|
+
tracked — the backend is the source of truth for agents).
|
|
1049
|
+
"""
|
|
1050
|
+
from sdk.config import ML_GATEWAY_URL
|
|
1051
|
+
|
|
1052
|
+
aid = (agent_id or "").strip()
|
|
1053
|
+
if not aid:
|
|
1054
|
+
return False
|
|
1055
|
+
url = f"{ML_GATEWAY_URL.rstrip('/')}/ml-api/agent-activity"
|
|
1056
|
+
headers = {**self._headers, "accept": "*/*", "developer-name": "null", "product": ""}
|
|
1057
|
+
try:
|
|
1058
|
+
async with httpx.AsyncClient(timeout=10.0) as client:
|
|
1059
|
+
resp = await client.post(
|
|
1060
|
+
url, json={"activity": str(activity), "agentId": aid}, headers=headers,
|
|
1061
|
+
)
|
|
1062
|
+
if resp.status_code >= 400:
|
|
1063
|
+
logger.warning("client.log_agent_activity failed status=%s body=%s",
|
|
1064
|
+
resp.status_code, resp.text[:200])
|
|
1065
|
+
return False
|
|
1066
|
+
return True
|
|
1067
|
+
except Exception as exc: # noqa: BLE001
|
|
1068
|
+
logger.warning("client.log_agent_activity error err=%s", exc)
|
|
1069
|
+
return False
|
|
1070
|
+
|
|
1071
|
+
async def resolve_agent_id(self, name: str) -> str | None:
|
|
1072
|
+
"""Best-effort backend ``agentId`` for an agent NAME (custom + department).
|
|
1073
|
+
|
|
1074
|
+
Returns None when the name isn't a backend agent (e.g. a local-only manifest)
|
|
1075
|
+
— the caller then skips activity emit, so only backend agents are tracked.
|
|
1076
|
+
Cached per process (the id↔name mapping is stable within a session).
|
|
1077
|
+
"""
|
|
1078
|
+
nm = (name or "").strip()
|
|
1079
|
+
if not nm:
|
|
1080
|
+
return None
|
|
1081
|
+
if nm in _AGENT_ID_CACHE:
|
|
1082
|
+
return _AGENT_ID_CACHE[nm] or None
|
|
1083
|
+
rows: list[dict] = []
|
|
1084
|
+
for fetch in (self.list_custom_agents, self.list_department_agents):
|
|
1085
|
+
try:
|
|
1086
|
+
rows += await fetch()
|
|
1087
|
+
except Exception: # noqa: BLE001 — best-effort across both sources
|
|
1088
|
+
pass
|
|
1089
|
+
for r in rows:
|
|
1090
|
+
if r.get("name") == nm and r.get("id"):
|
|
1091
|
+
_AGENT_ID_CACHE[nm] = str(r["id"])
|
|
1092
|
+
return _AGENT_ID_CACHE[nm]
|
|
1093
|
+
_AGENT_ID_CACHE[nm] = "" # negative-cache: not a backend agent
|
|
1094
|
+
return None
|
|
1095
|
+
|
|
1039
1096
|
async def notify_whatsapp(
|
|
1040
1097
|
self,
|
|
1041
1098
|
task_id: str,
|
|
@@ -25,7 +25,10 @@ _CONFIG_FILE = _CONFIG_DIR / "config.toml"
|
|
|
25
25
|
_SENTINEL = "co_"
|
|
26
26
|
|
|
27
27
|
AI_DEFAULT_URL = "https://devopsbot-ai.apiswagger.co.uk"
|
|
28
|
-
|
|
28
|
+
# ML backend / gateway — all /ml-api, /tasks-ml, /approvals-ml, agent-activity, etc.
|
|
29
|
+
# now go through the ML gateway (was devopsbot-be.apiswagger.co.uk).
|
|
30
|
+
BE_DEFAULT_URL = "https://gateway-ml.apiswagger.co.uk"
|
|
31
|
+
ML_GATEWAY_URL = "https://gateway-ml.apiswagger.co.uk"
|
|
29
32
|
|
|
30
33
|
|
|
31
34
|
# ── Machine-derived Fernet key ────────────────────────────────────────
|
|
@@ -304,6 +304,28 @@ async def logs_stream() -> StreamingResponse:
|
|
|
304
304
|
)
|
|
305
305
|
|
|
306
306
|
|
|
307
|
+
# ── Agent-activity webhook ──────────────────────────────────────────────────
|
|
308
|
+
# Every custom/default agent run/chat records to the LOCAL usage ledger AND emits
|
|
309
|
+
# a `{activity, agentId}` event to the ML gateway (POST /ml-api/agent-activity),
|
|
310
|
+
# which stores it for RBAC-scoped reporting via the chat endpoint. Only agents that
|
|
311
|
+
# exist in the backend (have an agentId) are emitted — local-only agents are skipped.
|
|
312
|
+
# Best-effort: never raises into the run.
|
|
313
|
+
|
|
314
|
+
async def _emit_agent_activity(name: str, kind: str, result: dict, duration_s: float = 0.0) -> None:
|
|
315
|
+
try:
|
|
316
|
+
from sdk.client import BackendClient
|
|
317
|
+
client = BackendClient.from_config()
|
|
318
|
+
agent_id = await client.resolve_agent_id(name)
|
|
319
|
+
if not agent_id: # local-only agent → not tracked
|
|
320
|
+
return
|
|
321
|
+
verb = "Chatted with" if kind == "chat" else "Ran"
|
|
322
|
+
status = "ok" if result.get("ok") else "error"
|
|
323
|
+
detail = f" ({round(duration_s, 1)}s)" if duration_s else ""
|
|
324
|
+
await client.log_agent_activity(f"{verb} agent '{name}' — {status}{detail}", agent_id)
|
|
325
|
+
except Exception: # noqa: BLE001 — activity emit must never break a run
|
|
326
|
+
pass
|
|
327
|
+
|
|
328
|
+
|
|
307
329
|
# ── Agents: catalog, creation, chat, default agents ────────────────────────
|
|
308
330
|
|
|
309
331
|
@router.get("/connectors", dependencies=[_auth])
|
|
@@ -689,12 +711,14 @@ async def agent_chat(body: AgentChatRequest) -> dict:
|
|
|
689
711
|
started = _time.monotonic()
|
|
690
712
|
result = await _run()
|
|
691
713
|
|
|
714
|
+
elapsed = _time.monotonic() - started
|
|
692
715
|
usage_mod.record(
|
|
693
|
-
name, kind="chat", duration_s=
|
|
716
|
+
name, kind="chat", duration_s=elapsed,
|
|
694
717
|
input_tokens=result.get("input_tokens", 0),
|
|
695
718
|
output_tokens=result.get("output_tokens", 0),
|
|
696
719
|
cost_usd=result.get("cost_usd", 0.0), ok=bool(result.get("ok")),
|
|
697
720
|
)
|
|
721
|
+
await _emit_agent_activity(name, "chat", result, elapsed)
|
|
698
722
|
return result
|
|
699
723
|
|
|
700
724
|
# Run detached from this request: navigating away can't kill it. Poll
|
|
@@ -732,12 +756,14 @@ async def agent_run(body: AgentRunRequest) -> dict:
|
|
|
732
756
|
started = _time.monotonic()
|
|
733
757
|
result = await _run()
|
|
734
758
|
|
|
759
|
+
elapsed = _time.monotonic() - started
|
|
735
760
|
usage_mod.record(
|
|
736
|
-
name, kind="run", duration_s=
|
|
761
|
+
name, kind="run", duration_s=elapsed,
|
|
737
762
|
input_tokens=result.get("input_tokens", 0),
|
|
738
763
|
output_tokens=result.get("output_tokens", 0),
|
|
739
764
|
cost_usd=result.get("cost_usd", 0.0), ok=bool(result.get("ok")),
|
|
740
765
|
)
|
|
766
|
+
await _emit_agent_activity(name, "run", result, elapsed)
|
|
741
767
|
return result
|
|
742
768
|
|
|
743
769
|
return {"job_id": jobs.start(work())}
|
|
@@ -929,6 +955,41 @@ async def _run_prompt_engine(prompt: str) -> str:
|
|
|
929
955
|
return result.get("result", "") or ""
|
|
930
956
|
|
|
931
957
|
|
|
958
|
+
# ── Unified revision format ───────────────────────────────────────────────────
|
|
959
|
+
# Missing / empty required fields → HTTP 422 (Pydantic on the request model,
|
|
960
|
+
# unchanged). When the inputs are PRESENT but need human revision (offensive /
|
|
961
|
+
# meaningless input, or a prompt missing required sections) EVERY endpoint returns
|
|
962
|
+
# the SAME shape — all messages collected into one ``ai_comments`` list:
|
|
963
|
+
# {"status": "needs_revision", "engine": "claude_code",
|
|
964
|
+
# "ai_comments": ["<message>", "<message>", ...]}
|
|
965
|
+
# Success responses carry status="ok" and ai_comments=[] so the client always reads
|
|
966
|
+
# the same two fields.
|
|
967
|
+
|
|
968
|
+
def _needs_revision(ai_comments: list[str]) -> dict:
|
|
969
|
+
"""The single revision envelope used by every agent-prompt endpoint."""
|
|
970
|
+
return {"status": "needs_revision", "engine": "claude_code", "ai_comments": ai_comments}
|
|
971
|
+
|
|
972
|
+
|
|
973
|
+
async def _screen_agent_inputs(agent_name: str, agent_type: str, job: str) -> list[str]:
|
|
974
|
+
"""LLM screen for offensive / meaningless inputs → list of comment messages
|
|
975
|
+
(empty when the inputs are fine).
|
|
976
|
+
|
|
977
|
+
FAIL-OPEN: any engine/parse error → ``[]`` (proceed), so a validator hiccup
|
|
978
|
+
never blocks a legitimate request.
|
|
979
|
+
"""
|
|
980
|
+
from sdk.agents.prompt_studio import build_validate_inputs_prompt, parse_validation
|
|
981
|
+
|
|
982
|
+
try:
|
|
983
|
+
verdict = parse_validation(
|
|
984
|
+
await _run_prompt_engine(build_validate_inputs_prompt(agent_name, agent_type, job))
|
|
985
|
+
)
|
|
986
|
+
except Exception: # noqa: BLE001 — fail-open
|
|
987
|
+
return []
|
|
988
|
+
if verdict.get("valid", True):
|
|
989
|
+
return []
|
|
990
|
+
return [str(msg).strip() for msg in (verdict.get("issues") or {}).values() if str(msg or "").strip()]
|
|
991
|
+
|
|
992
|
+
|
|
932
993
|
@router.post("/agent-prompt/generate", dependencies=[_auth])
|
|
933
994
|
async def agent_prompt_generate(body: AgentPromptGenerateRequest) -> dict:
|
|
934
995
|
"""name + type + job → a structured system prompt + {{variables}} (claude_code).
|
|
@@ -938,17 +999,24 @@ async def agent_prompt_generate(body: AgentPromptGenerateRequest) -> dict:
|
|
|
938
999
|
"""
|
|
939
1000
|
from sdk.agents.prompt_studio import build_generate_prompt, parse_generation
|
|
940
1001
|
|
|
1002
|
+
# Empty agent_name/agent_type/job already 422'd by the request model.
|
|
941
1003
|
agent_name = body.agent_name
|
|
942
1004
|
agent_type = body.agent_type
|
|
943
1005
|
job = body.job
|
|
944
1006
|
|
|
1007
|
+
# Semantic screen (offensive / meaningless) → unified needs_revision. Fail-open.
|
|
1008
|
+
comments = await _screen_agent_inputs(agent_name, agent_type, job)
|
|
1009
|
+
if comments:
|
|
1010
|
+
return _needs_revision(comments)
|
|
1011
|
+
|
|
945
1012
|
text = await _run_prompt_engine(
|
|
946
1013
|
build_generate_prompt(agent_name, agent_type, job, body.context)
|
|
947
1014
|
)
|
|
948
1015
|
parsed = parse_generation(text)
|
|
949
1016
|
if not parsed["system_prompt"]:
|
|
950
1017
|
raise HTTPException(status_code=502, detail="generation returned no system_prompt")
|
|
951
|
-
return {"
|
|
1018
|
+
return {"status": "ok", "engine": "claude_code", "ai_comments": [],
|
|
1019
|
+
"agent_name": agent_name, "agent_type": agent_type, **parsed}
|
|
952
1020
|
|
|
953
1021
|
|
|
954
1022
|
@router.post("/agent-prompt/regenerate", dependencies=[_auth])
|
|
@@ -960,6 +1028,12 @@ async def agent_prompt_regenerate(body: PromptRegenerateRequest) -> dict:
|
|
|
960
1028
|
"""
|
|
961
1029
|
from sdk.agents.prompt_studio import build_regenerate_prompt, parse_generation
|
|
962
1030
|
|
|
1031
|
+
# Empty system_prompt already 422'd by the request model. Screen the agent
|
|
1032
|
+
# metadata (blank fields ignored by the screener; fail-open).
|
|
1033
|
+
comments = await _screen_agent_inputs(body.agent_name, body.agent_type, body.job)
|
|
1034
|
+
if comments:
|
|
1035
|
+
return _needs_revision(comments)
|
|
1036
|
+
|
|
963
1037
|
text = await _run_prompt_engine(build_regenerate_prompt(
|
|
964
1038
|
body.system_prompt, body.feedback,
|
|
965
1039
|
body.agent_name, body.agent_type, body.job,
|
|
@@ -967,9 +1041,8 @@ async def agent_prompt_regenerate(body: PromptRegenerateRequest) -> dict:
|
|
|
967
1041
|
parsed = parse_generation(text)
|
|
968
1042
|
if not parsed["system_prompt"]:
|
|
969
1043
|
raise HTTPException(status_code=502, detail="regeneration returned no system_prompt")
|
|
970
|
-
return {"
|
|
971
|
-
"agent_type": body.agent_type,
|
|
972
|
-
"engine": "claude_code", **parsed}
|
|
1044
|
+
return {"status": "ok", "engine": "claude_code", "ai_comments": [],
|
|
1045
|
+
"agent_name": body.agent_name, "agent_type": body.agent_type, **parsed}
|
|
973
1046
|
|
|
974
1047
|
|
|
975
1048
|
@router.post("/agent-prompt/verify", dependencies=[_auth])
|
|
@@ -985,6 +1058,7 @@ async def agent_prompt_verify(body: PromptVerifyRequest) -> dict:
|
|
|
985
1058
|
from sdk.agents.prompt_template import check_system_prompt
|
|
986
1059
|
from sdk.agents.prompt_vars import extract_variables
|
|
987
1060
|
|
|
1061
|
+
# Too-short system_prompt already 422'd by the request model.
|
|
988
1062
|
system_prompt = body.system_prompt
|
|
989
1063
|
|
|
990
1064
|
standards = check_system_prompt(system_prompt)
|
|
@@ -1008,13 +1082,21 @@ async def agent_prompt_verify(body: PromptVerifyRequest) -> dict:
|
|
|
1008
1082
|
"type": str(m.get("type", "string")).strip() or "string",
|
|
1009
1083
|
})
|
|
1010
1084
|
|
|
1085
|
+
# Structural gaps → the SAME unified ai_comments list (all messages together).
|
|
1086
|
+
ai_comments = (
|
|
1087
|
+
[f"Missing required section: {s}." for s in standards["missing_required"]]
|
|
1088
|
+
+ [f"Recommended section missing: {s}." for s in standards["missing_recommended"]]
|
|
1089
|
+
)
|
|
1090
|
+
|
|
1011
1091
|
return {
|
|
1092
|
+
"status": "needs_revision" if standards["missing_required"] else "ok",
|
|
1093
|
+
"engine": "claude_code",
|
|
1094
|
+
"ai_comments": ai_comments,
|
|
1012
1095
|
"valid": not standards["missing_required"],
|
|
1013
1096
|
"missing_required": standards["missing_required"],
|
|
1014
1097
|
"missing_recommended": standards["missing_recommended"],
|
|
1015
1098
|
"variables": variables,
|
|
1016
1099
|
"secrets": analysis.get("secrets", []),
|
|
1017
|
-
"engine": "claude_code",
|
|
1018
1100
|
}
|
|
1019
1101
|
|
|
1020
1102
|
|
|
@@ -132,10 +132,10 @@ def _ml_api_base() -> str:
|
|
|
132
132
|
try:
|
|
133
133
|
from sdk.config import get_base_url
|
|
134
134
|
base = get_base_url()
|
|
135
|
-
# devopsbot-ai.apiswagger.co.uk →
|
|
136
|
-
return base.replace("devopsbot-ai", "
|
|
135
|
+
# devopsbot-ai.apiswagger.co.uk → gateway-ml.apiswagger.co.uk
|
|
136
|
+
return base.replace("devopsbot-ai.apiswagger.co.uk", "gateway-ml.apiswagger.co.uk")
|
|
137
137
|
except Exception: # noqa: BLE001
|
|
138
|
-
return "https://
|
|
138
|
+
return "https://gateway-ml.apiswagger.co.uk"
|
|
139
139
|
|
|
140
140
|
|
|
141
141
|
def _env_github_token() -> str | None:
|
|
File without changes
|
|
File without changes
|
{devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/devops_bot_sdk.egg-info/dependency_links.txt
RENAMED
|
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
|
|
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
|
|
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
|