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.
Files changed (95) hide show
  1. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/PKG-INFO +1 -1
  2. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/devops_bot_sdk.egg-info/PKG-INFO +1 -1
  3. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/__init__.py +2 -2
  4. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/agents/inventory.py +1 -0
  5. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/agents/prompt_studio.py +51 -0
  6. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/client.py +57 -0
  7. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/config.py +4 -1
  8. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/ui/api.py +89 -7
  9. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/updater.py +3 -3
  10. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/README.md +0 -0
  11. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/devops_bot_sdk.egg-info/SOURCES.txt +0 -0
  12. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/devops_bot_sdk.egg-info/dependency_links.txt +0 -0
  13. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/devops_bot_sdk.egg-info/entry_points.txt +0 -0
  14. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/devops_bot_sdk.egg-info/requires.txt +0 -0
  15. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/devops_bot_sdk.egg-info/top_level.txt +0 -0
  16. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/pyproject.toml +0 -0
  17. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/agentd.py +0 -0
  18. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/agents/__init__.py +0 -0
  19. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/agents/builder.py +0 -0
  20. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/agents/chat.py +0 -0
  21. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/agents/connectors.py +0 -0
  22. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/agents/engines/__init__.py +0 -0
  23. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/agents/engines/claude_code.py +0 -0
  24. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/agents/engines/http.py +0 -0
  25. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/agents/engines/mcp.py +0 -0
  26. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/agents/engines/n8n.py +0 -0
  27. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/agents/engines/shell.py +0 -0
  28. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/agents/integrations.py +0 -0
  29. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/agents/library/__init__.py +0 -0
  30. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/agents/manifest.py +0 -0
  31. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/agents/prompt_template.py +0 -0
  32. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/agents/prompt_vars.py +0 -0
  33. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/agents/run_log.py +0 -0
  34. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/agents/secrets.py +0 -0
  35. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/agents/usage.py +0 -0
  36. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/agents/varstore.py +0 -0
  37. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/cli.py +0 -0
  38. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/collectors/__init__.py +0 -0
  39. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/collectors/files.py +0 -0
  40. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/collectors/process.py +0 -0
  41. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/collectors/screenshot.py +0 -0
  42. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/crucial.py +0 -0
  43. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/design_verify/__init__.py +0 -0
  44. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/design_verify/assets.py +0 -0
  45. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/design_verify/bootstrap.py +0 -0
  46. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/design_verify/browser.py +0 -0
  47. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/design_verify/compare.py +0 -0
  48. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/design_verify/loop.py +0 -0
  49. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/exceptions.py +0 -0
  50. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/git_ops.py +0 -0
  51. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/graphify.py +0 -0
  52. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/hooks/__init__.py +0 -0
  53. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/hooks/crucial_guard.py +0 -0
  54. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/ipc/__init__.py +0 -0
  55. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/ipc/electron_bridge.py +0 -0
  56. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/ipc/handlers.py +0 -0
  57. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/local_exec.py +0 -0
  58. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/models/__init__.py +0 -0
  59. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/models/envelope.py +0 -0
  60. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/models/requests.py +0 -0
  61. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/models/responses.py +0 -0
  62. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/models/snapshots.py +0 -0
  63. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/platform_compat.py +0 -0
  64. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/py.typed +0 -0
  65. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/run_auto.py +0 -0
  66. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/sse.py +0 -0
  67. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/test.py +0 -0
  68. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/test_pipeline.py +0 -0
  69. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/ui/__init__.py +0 -0
  70. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/ui/dashboard.py +0 -0
  71. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/ui/health.py +0 -0
  72. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/ui/ip_allowlist.py +0 -0
  73. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/ui/jobs.py +0 -0
  74. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/ui/models.py +0 -0
  75. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/ui/runner.py +0 -0
  76. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/ui/security.py +0 -0
  77. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/ui/server.py +0 -0
  78. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/ui/service.py +0 -0
  79. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/ui/setup_manager.py +0 -0
  80. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/ui/static/agents.html +0 -0
  81. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/ui/static/app.js +0 -0
  82. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/ui/static/custom-agents.html +0 -0
  83. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/ui/static/default-agents.html +0 -0
  84. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/ui/static/index.html +0 -0
  85. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/ui/static/runs.html +0 -0
  86. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/ui/static/setup.html +0 -0
  87. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/ui/static/styles.css +0 -0
  88. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/sdk/ui/static/tickets.html +0 -0
  89. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/setup.cfg +0 -0
  90. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/tests/test_agent_chat_features.py +0 -0
  91. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/tests/test_agent_prompt_models.py +0 -0
  92. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/tests/test_agents_connectors_builder.py +0 -0
  93. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/tests/test_git_action.py +0 -0
  94. {devops_bot_sdk-1.4.149 → devops_bot_sdk-1.4.151}/tests/test_health.py +0 -0
  95. {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
  Metadata-Version: 2.4
2
2
  Name: devops-bot-sdk
3
- Version: 1.4.149
3
+ Version: 1.4.151
4
4
  Summary: DevOps Bot Desktop SDK — thin client for the AgentOS Electron desktop app
5
5
  Author: noumanaziz2128
6
6
  License-Expression: LicenseRef-Proprietary
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devops-bot-sdk
3
- Version: 1.4.149
3
+ Version: 1.4.151
4
4
  Summary: DevOps Bot Desktop SDK — thin client for the AgentOS Electron desktop app
5
5
  Author: noumanaziz2128
6
6
  License-Expression: LicenseRef-Proprietary
@@ -1,6 +1,6 @@
1
1
  """AgentOS Desktop SDK — thin HTTPS/SSE client for the Electron app.
2
2
 
3
- Version: 1.4.149
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.149" # SINGLE SOURCE OF TRUTH — bump on every change; pyproject,
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
- BE_DEFAULT_URL = "https://devopsbot-be.apiswagger.co.uk"
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=_time.monotonic() - started,
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=_time.monotonic() - started,
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 {"agent_name": agent_name, "agent_type": agent_type, "engine": "claude_code", **parsed}
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 {"agent_name": body.agent_name,
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 → devopsbot-be.apiswagger.co.uk
136
- return base.replace("devopsbot-ai", "devopsbot-be")
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://devopsbot-be.apiswagger.co.uk"
138
+ return "https://gateway-ml.apiswagger.co.uk"
139
139
 
140
140
 
141
141
  def _env_github_token() -> str | None: