devops-bot-sdk 1.4.150__tar.gz → 1.4.153__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.150 → devops_bot_sdk-1.4.153}/PKG-INFO +1 -1
- {devops_bot_sdk-1.4.150 → devops_bot_sdk-1.4.153}/devops_bot_sdk.egg-info/PKG-INFO +1 -1
- {devops_bot_sdk-1.4.150 → devops_bot_sdk-1.4.153}/devops_bot_sdk.egg-info/SOURCES.txt +1 -0
- {devops_bot_sdk-1.4.150 → devops_bot_sdk-1.4.153}/sdk/__init__.py +2 -2
- {devops_bot_sdk-1.4.150 → devops_bot_sdk-1.4.153}/sdk/agents/inventory.py +1 -0
- {devops_bot_sdk-1.4.150 → devops_bot_sdk-1.4.153}/sdk/client.py +57 -0
- {devops_bot_sdk-1.4.150 → devops_bot_sdk-1.4.153}/sdk/config.py +14 -2
- {devops_bot_sdk-1.4.150 → devops_bot_sdk-1.4.153}/sdk/ui/api.py +114 -69
- {devops_bot_sdk-1.4.150 → devops_bot_sdk-1.4.153}/sdk/updater.py +3 -3
- devops_bot_sdk-1.4.153/tests/test_agent_prompt_async.py +74 -0
- {devops_bot_sdk-1.4.150 → devops_bot_sdk-1.4.153}/README.md +0 -0
- {devops_bot_sdk-1.4.150 → devops_bot_sdk-1.4.153}/devops_bot_sdk.egg-info/dependency_links.txt +0 -0
- {devops_bot_sdk-1.4.150 → devops_bot_sdk-1.4.153}/devops_bot_sdk.egg-info/entry_points.txt +0 -0
- {devops_bot_sdk-1.4.150 → devops_bot_sdk-1.4.153}/devops_bot_sdk.egg-info/requires.txt +0 -0
- {devops_bot_sdk-1.4.150 → devops_bot_sdk-1.4.153}/devops_bot_sdk.egg-info/top_level.txt +0 -0
- {devops_bot_sdk-1.4.150 → devops_bot_sdk-1.4.153}/pyproject.toml +0 -0
- {devops_bot_sdk-1.4.150 → devops_bot_sdk-1.4.153}/sdk/agentd.py +0 -0
- {devops_bot_sdk-1.4.150 → devops_bot_sdk-1.4.153}/sdk/agents/__init__.py +0 -0
- {devops_bot_sdk-1.4.150 → devops_bot_sdk-1.4.153}/sdk/agents/builder.py +0 -0
- {devops_bot_sdk-1.4.150 → devops_bot_sdk-1.4.153}/sdk/agents/chat.py +0 -0
- {devops_bot_sdk-1.4.150 → devops_bot_sdk-1.4.153}/sdk/agents/connectors.py +0 -0
- {devops_bot_sdk-1.4.150 → devops_bot_sdk-1.4.153}/sdk/agents/engines/__init__.py +0 -0
- {devops_bot_sdk-1.4.150 → devops_bot_sdk-1.4.153}/sdk/agents/engines/claude_code.py +0 -0
- {devops_bot_sdk-1.4.150 → devops_bot_sdk-1.4.153}/sdk/agents/engines/http.py +0 -0
- {devops_bot_sdk-1.4.150 → devops_bot_sdk-1.4.153}/sdk/agents/engines/mcp.py +0 -0
- {devops_bot_sdk-1.4.150 → devops_bot_sdk-1.4.153}/sdk/agents/engines/n8n.py +0 -0
- {devops_bot_sdk-1.4.150 → devops_bot_sdk-1.4.153}/sdk/agents/engines/shell.py +0 -0
- {devops_bot_sdk-1.4.150 → devops_bot_sdk-1.4.153}/sdk/agents/integrations.py +0 -0
- {devops_bot_sdk-1.4.150 → devops_bot_sdk-1.4.153}/sdk/agents/library/__init__.py +0 -0
- {devops_bot_sdk-1.4.150 → devops_bot_sdk-1.4.153}/sdk/agents/manifest.py +0 -0
- {devops_bot_sdk-1.4.150 → devops_bot_sdk-1.4.153}/sdk/agents/prompt_studio.py +0 -0
- {devops_bot_sdk-1.4.150 → devops_bot_sdk-1.4.153}/sdk/agents/prompt_template.py +0 -0
- {devops_bot_sdk-1.4.150 → devops_bot_sdk-1.4.153}/sdk/agents/prompt_vars.py +0 -0
- {devops_bot_sdk-1.4.150 → devops_bot_sdk-1.4.153}/sdk/agents/run_log.py +0 -0
- {devops_bot_sdk-1.4.150 → devops_bot_sdk-1.4.153}/sdk/agents/secrets.py +0 -0
- {devops_bot_sdk-1.4.150 → devops_bot_sdk-1.4.153}/sdk/agents/usage.py +0 -0
- {devops_bot_sdk-1.4.150 → devops_bot_sdk-1.4.153}/sdk/agents/varstore.py +0 -0
- {devops_bot_sdk-1.4.150 → devops_bot_sdk-1.4.153}/sdk/cli.py +0 -0
- {devops_bot_sdk-1.4.150 → devops_bot_sdk-1.4.153}/sdk/collectors/__init__.py +0 -0
- {devops_bot_sdk-1.4.150 → devops_bot_sdk-1.4.153}/sdk/collectors/files.py +0 -0
- {devops_bot_sdk-1.4.150 → devops_bot_sdk-1.4.153}/sdk/collectors/process.py +0 -0
- {devops_bot_sdk-1.4.150 → devops_bot_sdk-1.4.153}/sdk/collectors/screenshot.py +0 -0
- {devops_bot_sdk-1.4.150 → devops_bot_sdk-1.4.153}/sdk/crucial.py +0 -0
- {devops_bot_sdk-1.4.150 → devops_bot_sdk-1.4.153}/sdk/design_verify/__init__.py +0 -0
- {devops_bot_sdk-1.4.150 → devops_bot_sdk-1.4.153}/sdk/design_verify/assets.py +0 -0
- {devops_bot_sdk-1.4.150 → devops_bot_sdk-1.4.153}/sdk/design_verify/bootstrap.py +0 -0
- {devops_bot_sdk-1.4.150 → devops_bot_sdk-1.4.153}/sdk/design_verify/browser.py +0 -0
- {devops_bot_sdk-1.4.150 → devops_bot_sdk-1.4.153}/sdk/design_verify/compare.py +0 -0
- {devops_bot_sdk-1.4.150 → devops_bot_sdk-1.4.153}/sdk/design_verify/loop.py +0 -0
- {devops_bot_sdk-1.4.150 → devops_bot_sdk-1.4.153}/sdk/exceptions.py +0 -0
- {devops_bot_sdk-1.4.150 → devops_bot_sdk-1.4.153}/sdk/git_ops.py +0 -0
- {devops_bot_sdk-1.4.150 → devops_bot_sdk-1.4.153}/sdk/graphify.py +0 -0
- {devops_bot_sdk-1.4.150 → devops_bot_sdk-1.4.153}/sdk/hooks/__init__.py +0 -0
- {devops_bot_sdk-1.4.150 → devops_bot_sdk-1.4.153}/sdk/hooks/crucial_guard.py +0 -0
- {devops_bot_sdk-1.4.150 → devops_bot_sdk-1.4.153}/sdk/ipc/__init__.py +0 -0
- {devops_bot_sdk-1.4.150 → devops_bot_sdk-1.4.153}/sdk/ipc/electron_bridge.py +0 -0
- {devops_bot_sdk-1.4.150 → devops_bot_sdk-1.4.153}/sdk/ipc/handlers.py +0 -0
- {devops_bot_sdk-1.4.150 → devops_bot_sdk-1.4.153}/sdk/local_exec.py +0 -0
- {devops_bot_sdk-1.4.150 → devops_bot_sdk-1.4.153}/sdk/models/__init__.py +0 -0
- {devops_bot_sdk-1.4.150 → devops_bot_sdk-1.4.153}/sdk/models/envelope.py +0 -0
- {devops_bot_sdk-1.4.150 → devops_bot_sdk-1.4.153}/sdk/models/requests.py +0 -0
- {devops_bot_sdk-1.4.150 → devops_bot_sdk-1.4.153}/sdk/models/responses.py +0 -0
- {devops_bot_sdk-1.4.150 → devops_bot_sdk-1.4.153}/sdk/models/snapshots.py +0 -0
- {devops_bot_sdk-1.4.150 → devops_bot_sdk-1.4.153}/sdk/platform_compat.py +0 -0
- {devops_bot_sdk-1.4.150 → devops_bot_sdk-1.4.153}/sdk/py.typed +0 -0
- {devops_bot_sdk-1.4.150 → devops_bot_sdk-1.4.153}/sdk/run_auto.py +0 -0
- {devops_bot_sdk-1.4.150 → devops_bot_sdk-1.4.153}/sdk/sse.py +0 -0
- {devops_bot_sdk-1.4.150 → devops_bot_sdk-1.4.153}/sdk/test.py +0 -0
- {devops_bot_sdk-1.4.150 → devops_bot_sdk-1.4.153}/sdk/test_pipeline.py +0 -0
- {devops_bot_sdk-1.4.150 → devops_bot_sdk-1.4.153}/sdk/ui/__init__.py +0 -0
- {devops_bot_sdk-1.4.150 → devops_bot_sdk-1.4.153}/sdk/ui/dashboard.py +0 -0
- {devops_bot_sdk-1.4.150 → devops_bot_sdk-1.4.153}/sdk/ui/health.py +0 -0
- {devops_bot_sdk-1.4.150 → devops_bot_sdk-1.4.153}/sdk/ui/ip_allowlist.py +0 -0
- {devops_bot_sdk-1.4.150 → devops_bot_sdk-1.4.153}/sdk/ui/jobs.py +0 -0
- {devops_bot_sdk-1.4.150 → devops_bot_sdk-1.4.153}/sdk/ui/models.py +0 -0
- {devops_bot_sdk-1.4.150 → devops_bot_sdk-1.4.153}/sdk/ui/runner.py +0 -0
- {devops_bot_sdk-1.4.150 → devops_bot_sdk-1.4.153}/sdk/ui/security.py +0 -0
- {devops_bot_sdk-1.4.150 → devops_bot_sdk-1.4.153}/sdk/ui/server.py +0 -0
- {devops_bot_sdk-1.4.150 → devops_bot_sdk-1.4.153}/sdk/ui/service.py +0 -0
- {devops_bot_sdk-1.4.150 → devops_bot_sdk-1.4.153}/sdk/ui/setup_manager.py +0 -0
- {devops_bot_sdk-1.4.150 → devops_bot_sdk-1.4.153}/sdk/ui/static/agents.html +0 -0
- {devops_bot_sdk-1.4.150 → devops_bot_sdk-1.4.153}/sdk/ui/static/app.js +0 -0
- {devops_bot_sdk-1.4.150 → devops_bot_sdk-1.4.153}/sdk/ui/static/custom-agents.html +0 -0
- {devops_bot_sdk-1.4.150 → devops_bot_sdk-1.4.153}/sdk/ui/static/default-agents.html +0 -0
- {devops_bot_sdk-1.4.150 → devops_bot_sdk-1.4.153}/sdk/ui/static/index.html +0 -0
- {devops_bot_sdk-1.4.150 → devops_bot_sdk-1.4.153}/sdk/ui/static/runs.html +0 -0
- {devops_bot_sdk-1.4.150 → devops_bot_sdk-1.4.153}/sdk/ui/static/setup.html +0 -0
- {devops_bot_sdk-1.4.150 → devops_bot_sdk-1.4.153}/sdk/ui/static/styles.css +0 -0
- {devops_bot_sdk-1.4.150 → devops_bot_sdk-1.4.153}/sdk/ui/static/tickets.html +0 -0
- {devops_bot_sdk-1.4.150 → devops_bot_sdk-1.4.153}/setup.cfg +0 -0
- {devops_bot_sdk-1.4.150 → devops_bot_sdk-1.4.153}/tests/test_agent_chat_features.py +0 -0
- {devops_bot_sdk-1.4.150 → devops_bot_sdk-1.4.153}/tests/test_agent_prompt_models.py +0 -0
- {devops_bot_sdk-1.4.150 → devops_bot_sdk-1.4.153}/tests/test_agents_connectors_builder.py +0 -0
- {devops_bot_sdk-1.4.150 → devops_bot_sdk-1.4.153}/tests/test_git_action.py +0 -0
- {devops_bot_sdk-1.4.150 → devops_bot_sdk-1.4.153}/tests/test_health.py +0 -0
- {devops_bot_sdk-1.4.150 → devops_bot_sdk-1.4.153}/tests/test_updater_extras.py +0 -0
|
@@ -86,6 +86,7 @@ sdk/ui/static/setup.html
|
|
|
86
86
|
sdk/ui/static/styles.css
|
|
87
87
|
sdk/ui/static/tickets.html
|
|
88
88
|
tests/test_agent_chat_features.py
|
|
89
|
+
tests/test_agent_prompt_async.py
|
|
89
90
|
tests/test_agent_prompt_models.py
|
|
90
91
|
tests/test_agents_connectors_builder.py
|
|
91
92
|
tests/test_git_action.py
|
|
@@ -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.153
|
|
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.153" # 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")],
|
|
@@ -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 ────────────────────────────────────────
|
|
@@ -146,6 +149,13 @@ def _dotenv_value(key: str) -> str:
|
|
|
146
149
|
return ""
|
|
147
150
|
|
|
148
151
|
|
|
152
|
+
# Built-in permanent allowlist — the central AI backend's IP(s). Baked in so the
|
|
153
|
+
# backend can reach EVERY VPS's /api/ui/* out of the box (no per-VPS .env needed);
|
|
154
|
+
# always allowed, treated as permanent (not removable via the API). Extend/override
|
|
155
|
+
# with AGENTOS_UI_ALLOWED_IPS in .env or the process env.
|
|
156
|
+
_DEFAULT_ALLOWED_IPS: list[str] = ["20.12.201.148"]
|
|
157
|
+
|
|
158
|
+
|
|
149
159
|
def _parse_ip_list(raw: str) -> list[str]:
|
|
150
160
|
"""Split an IP allowlist string, tolerant of JSON-ish list syntax.
|
|
151
161
|
|
|
@@ -178,6 +188,7 @@ def get_allowed_ips() -> list[str]:
|
|
|
178
188
|
change. CIDR/prefix entries are supported by the middleware.
|
|
179
189
|
"""
|
|
180
190
|
sources = [
|
|
191
|
+
",".join(_DEFAULT_ALLOWED_IPS), # built-in backend IP(s) — always allowed
|
|
181
192
|
_dotenv_value("AGENTOS_UI_ALLOWED_IPS"),
|
|
182
193
|
os.environ.get("AGENTOS_UI_ALLOWED_IPS", ""),
|
|
183
194
|
_load_raw().get("allowed_ips", ""),
|
|
@@ -202,7 +213,8 @@ def permanent_allowed_ips() -> list[str]:
|
|
|
202
213
|
through the API — change them by editing ``.env`` and restarting.
|
|
203
214
|
"""
|
|
204
215
|
out: list[str] = []
|
|
205
|
-
for src in (
|
|
216
|
+
for src in (",".join(_DEFAULT_ALLOWED_IPS), # built-in backend IP(s) — always permanent
|
|
217
|
+
_dotenv_value("AGENTOS_UI_ALLOWED_IPS"),
|
|
206
218
|
os.environ.get("AGENTOS_UI_ALLOWED_IPS", "")):
|
|
207
219
|
for ip in _parse_ip_list(src):
|
|
208
220
|
if ip not in out:
|
|
@@ -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())}
|
|
@@ -968,55 +994,70 @@ async def _screen_agent_inputs(agent_name: str, agent_type: str, job: str) -> li
|
|
|
968
994
|
async def agent_prompt_generate(body: AgentPromptGenerateRequest) -> dict:
|
|
969
995
|
"""name + type + job → a structured system prompt + {{variables}} (claude_code).
|
|
970
996
|
|
|
971
|
-
|
|
972
|
-
|
|
997
|
+
ASYNC: local Claude Code generation can take a while, so this starts a detached
|
|
998
|
+
job and returns ``{job_id}`` immediately (navigation-safe, and no long request
|
|
999
|
+
held across a proxy/gateway — avoids 504s). Poll ``GET /api/ui/jobs/{job_id}``
|
|
1000
|
+
for the result: ``{status, engine, ai_comments, agent_name, agent_type,
|
|
1001
|
+
system_prompt, variables[]}`` (or ``status="needs_revision"`` / ``"error"``).
|
|
973
1002
|
"""
|
|
974
1003
|
from sdk.agents.prompt_studio import build_generate_prompt, parse_generation
|
|
1004
|
+
from sdk.ui import jobs
|
|
975
1005
|
|
|
976
1006
|
# Empty agent_name/agent_type/job already 422'd by the request model.
|
|
977
1007
|
agent_name = body.agent_name
|
|
978
1008
|
agent_type = body.agent_type
|
|
979
1009
|
job = body.job
|
|
1010
|
+
context = body.context
|
|
1011
|
+
|
|
1012
|
+
async def work():
|
|
1013
|
+
# Semantic screen (offensive / meaningless) → unified needs_revision. Fail-open.
|
|
1014
|
+
comments = await _screen_agent_inputs(agent_name, agent_type, job)
|
|
1015
|
+
if comments:
|
|
1016
|
+
return _needs_revision(comments)
|
|
1017
|
+
text = await _run_prompt_engine(build_generate_prompt(agent_name, agent_type, job, context))
|
|
1018
|
+
parsed = parse_generation(text)
|
|
1019
|
+
if not parsed["system_prompt"]:
|
|
1020
|
+
return {"status": "error", "engine": "claude_code", "ai_comments": [],
|
|
1021
|
+
"error": "generation returned no system_prompt"}
|
|
1022
|
+
return {"status": "ok", "engine": "claude_code", "ai_comments": [],
|
|
1023
|
+
"agent_name": agent_name, "agent_type": agent_type, **parsed}
|
|
980
1024
|
|
|
981
|
-
|
|
982
|
-
comments = await _screen_agent_inputs(agent_name, agent_type, job)
|
|
983
|
-
if comments:
|
|
984
|
-
return _needs_revision(comments)
|
|
985
|
-
|
|
986
|
-
text = await _run_prompt_engine(
|
|
987
|
-
build_generate_prompt(agent_name, agent_type, job, body.context)
|
|
988
|
-
)
|
|
989
|
-
parsed = parse_generation(text)
|
|
990
|
-
if not parsed["system_prompt"]:
|
|
991
|
-
raise HTTPException(status_code=502, detail="generation returned no system_prompt")
|
|
992
|
-
return {"status": "ok", "engine": "claude_code", "ai_comments": [],
|
|
993
|
-
"agent_name": agent_name, "agent_type": agent_type, **parsed}
|
|
1025
|
+
return {"job_id": jobs.start(work())}
|
|
994
1026
|
|
|
995
1027
|
|
|
996
1028
|
@router.post("/agent-prompt/regenerate", dependencies=[_auth])
|
|
997
1029
|
async def agent_prompt_regenerate(body: PromptRegenerateRequest) -> dict:
|
|
998
1030
|
"""Improve an existing prompt (optional feedback) → prompt + {{variables}}.
|
|
999
1031
|
|
|
1000
|
-
|
|
1001
|
-
``
|
|
1032
|
+
ASYNC (same as generate): returns ``{job_id}``; poll ``GET /api/ui/jobs/{job_id}``.
|
|
1033
|
+
Body: ``system_prompt``, optional ``feedback``, ``agent_name``, ``agent_type``, ``job``.
|
|
1002
1034
|
"""
|
|
1003
1035
|
from sdk.agents.prompt_studio import build_regenerate_prompt, parse_generation
|
|
1036
|
+
from sdk.ui import jobs
|
|
1004
1037
|
|
|
1005
|
-
# Empty system_prompt already 422'd by the request model.
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1038
|
+
# Empty system_prompt already 422'd by the request model.
|
|
1039
|
+
agent_name = body.agent_name
|
|
1040
|
+
agent_type = body.agent_type
|
|
1041
|
+
job = body.job
|
|
1042
|
+
system_prompt = body.system_prompt
|
|
1043
|
+
feedback = body.feedback
|
|
1044
|
+
|
|
1045
|
+
async def work():
|
|
1046
|
+
# Screen the agent metadata (blank fields ignored by the screener; fail-open).
|
|
1047
|
+
comments = await _screen_agent_inputs(agent_name, agent_type, job)
|
|
1048
|
+
if comments:
|
|
1049
|
+
return _needs_revision(comments)
|
|
1050
|
+
text = await _run_prompt_engine(build_regenerate_prompt(
|
|
1051
|
+
system_prompt, feedback, agent_name, agent_type, job,
|
|
1052
|
+
))
|
|
1053
|
+
parsed = parse_generation(text)
|
|
1054
|
+
if not parsed["system_prompt"]:
|
|
1055
|
+
return {"status": "error", "engine": "claude_code", "ai_comments": [],
|
|
1056
|
+
"error": "regeneration returned no system_prompt"}
|
|
1057
|
+
return {"status": "ok", "engine": "claude_code", "ai_comments": [],
|
|
1058
|
+
"agent_name": agent_name, "agent_type": agent_type, **parsed}
|
|
1010
1059
|
|
|
1011
|
-
|
|
1012
|
-
body.system_prompt, body.feedback,
|
|
1013
|
-
body.agent_name, body.agent_type, body.job,
|
|
1014
|
-
))
|
|
1015
|
-
parsed = parse_generation(text)
|
|
1016
|
-
if not parsed["system_prompt"]:
|
|
1017
|
-
raise HTTPException(status_code=502, detail="regeneration returned no system_prompt")
|
|
1018
|
-
return {"status": "ok", "engine": "claude_code", "ai_comments": [],
|
|
1019
|
-
"agent_name": body.agent_name, "agent_type": body.agent_type, **parsed}
|
|
1060
|
+
return {"job_id": jobs.start(work())}
|
|
1020
1061
|
|
|
1021
1062
|
|
|
1022
1063
|
@router.post("/agent-prompt/verify", dependencies=[_auth])
|
|
@@ -1025,53 +1066,57 @@ async def agent_prompt_verify(body: PromptVerifyRequest) -> dict:
|
|
|
1025
1066
|
|
|
1026
1067
|
Standards check is deterministic (our Role/Objective/Rules/Workflow/Output
|
|
1027
1068
|
template); secrets + variable metadata are inferred by Claude Code locally.
|
|
1028
|
-
|
|
1029
|
-
variables[], secrets[]}``.
|
|
1069
|
+
ASYNC: returns ``{job_id}``; poll ``GET /api/ui/jobs/{job_id}`` → ``{valid,
|
|
1070
|
+
missing_required, missing_recommended, variables[], secrets[], ...}``.
|
|
1030
1071
|
"""
|
|
1031
1072
|
from sdk.agents.prompt_studio import build_analyze_prompt, parse_analysis
|
|
1032
1073
|
from sdk.agents.prompt_template import check_system_prompt
|
|
1033
1074
|
from sdk.agents.prompt_vars import extract_variables
|
|
1075
|
+
from sdk.ui import jobs
|
|
1034
1076
|
|
|
1035
1077
|
# Too-short system_prompt already 422'd by the request model.
|
|
1036
1078
|
system_prompt = body.system_prompt
|
|
1037
1079
|
|
|
1038
|
-
|
|
1080
|
+
async def work():
|
|
1081
|
+
standards = check_system_prompt(system_prompt)
|
|
1039
1082
|
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1083
|
+
# Enrichment (secrets + variable metadata) via local Claude Code — best-effort:
|
|
1084
|
+
# a failure must not sink the deterministic verdict.
|
|
1085
|
+
analysis: dict = {"inputs": [], "secrets": []}
|
|
1086
|
+
try:
|
|
1087
|
+
analysis = parse_analysis(await _run_prompt_engine(build_analyze_prompt(system_prompt)))
|
|
1088
|
+
except HTTPException:
|
|
1089
|
+
pass
|
|
1090
|
+
|
|
1091
|
+
meta = {str(i.get("name", "")).strip(): i for i in analysis.get("inputs", []) if isinstance(i, dict)}
|
|
1092
|
+
variables = []
|
|
1093
|
+
for name in extract_variables(system_prompt): # {{placeholders}} are authoritative
|
|
1094
|
+
m = meta.get(name, {})
|
|
1095
|
+
variables.append({
|
|
1096
|
+
"name": name,
|
|
1097
|
+
"label": str(m.get("label") or name.replace("_", " ").title()).strip(),
|
|
1098
|
+
"description": str(m.get("description", "")).strip(),
|
|
1099
|
+
"type": str(m.get("type", "string")).strip() or "string",
|
|
1100
|
+
})
|
|
1101
|
+
|
|
1102
|
+
# Structural gaps → the SAME unified ai_comments list (all messages together).
|
|
1103
|
+
ai_comments = (
|
|
1104
|
+
[f"Missing required section: {s}." for s in standards["missing_required"]]
|
|
1105
|
+
+ [f"Recommended section missing: {s}." for s in standards["missing_recommended"]]
|
|
1106
|
+
)
|
|
1047
1107
|
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
"
|
|
1054
|
-
"
|
|
1055
|
-
"
|
|
1056
|
-
"
|
|
1057
|
-
}
|
|
1058
|
-
|
|
1059
|
-
# Structural gaps → the SAME unified ai_comments list (all messages together).
|
|
1060
|
-
ai_comments = (
|
|
1061
|
-
[f"Missing required section: {s}." for s in standards["missing_required"]]
|
|
1062
|
-
+ [f"Recommended section missing: {s}." for s in standards["missing_recommended"]]
|
|
1063
|
-
)
|
|
1108
|
+
return {
|
|
1109
|
+
"status": "needs_revision" if standards["missing_required"] else "ok",
|
|
1110
|
+
"engine": "claude_code",
|
|
1111
|
+
"ai_comments": ai_comments,
|
|
1112
|
+
"valid": not standards["missing_required"],
|
|
1113
|
+
"missing_required": standards["missing_required"],
|
|
1114
|
+
"missing_recommended": standards["missing_recommended"],
|
|
1115
|
+
"variables": variables,
|
|
1116
|
+
"secrets": analysis.get("secrets", []),
|
|
1117
|
+
}
|
|
1064
1118
|
|
|
1065
|
-
return {
|
|
1066
|
-
"status": "needs_revision" if standards["missing_required"] else "ok",
|
|
1067
|
-
"engine": "claude_code",
|
|
1068
|
-
"ai_comments": ai_comments,
|
|
1069
|
-
"valid": not standards["missing_required"],
|
|
1070
|
-
"missing_required": standards["missing_required"],
|
|
1071
|
-
"missing_recommended": standards["missing_recommended"],
|
|
1072
|
-
"variables": variables,
|
|
1073
|
-
"secrets": analysis.get("secrets", []),
|
|
1074
|
-
}
|
|
1119
|
+
return {"job_id": jobs.start(work())}
|
|
1075
1120
|
|
|
1076
1121
|
|
|
1077
1122
|
@router.get("/agents/form", dependencies=[_auth])
|
|
@@ -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:
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Agent-prompt endpoints are async job-based (avoids long requests / 504s).
|
|
2
|
+
|
|
3
|
+
POST /api/ui/agent-prompt/{generate,regenerate,verify} return `{job_id}`
|
|
4
|
+
immediately — the local Claude Code run happens in the background; the result is
|
|
5
|
+
fetched from GET /api/ui/jobs/{job_id}.
|
|
6
|
+
|
|
7
|
+
Note: we assert the async *contract* (an immediate `{job_id}`). Driving the
|
|
8
|
+
background job to completion isn't done via the sync TestClient (each request
|
|
9
|
+
runs its own event loop, so a detached task from the POST doesn't advance across
|
|
10
|
+
later GETs) — that's a harness limitation, not a code one. The job's *result*
|
|
11
|
+
logic is unchanged from the pre-async version (just wrapped in `work()`).
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import re
|
|
16
|
+
from unittest.mock import AsyncMock, patch
|
|
17
|
+
|
|
18
|
+
import pytest
|
|
19
|
+
|
|
20
|
+
fastapi = pytest.importorskip("fastapi")
|
|
21
|
+
from fastapi.testclient import TestClient # noqa: E402
|
|
22
|
+
|
|
23
|
+
from sdk.ui.api import require_token, router # noqa: E402
|
|
24
|
+
|
|
25
|
+
_JOB_RE = re.compile(r"^[0-9a-f]{32}$") # uuid4().hex
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _client():
|
|
29
|
+
app = fastapi.FastAPI()
|
|
30
|
+
app.include_router(router)
|
|
31
|
+
app.dependency_overrides[require_token] = lambda: None
|
|
32
|
+
return TestClient(app)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# Keep the local Claude Code call mocked so a background task (if it runs) never
|
|
36
|
+
# shells out to a real `claude`. Autouse across this module.
|
|
37
|
+
@pytest.fixture(autouse=True)
|
|
38
|
+
def _no_real_claude():
|
|
39
|
+
with patch("sdk.ui.api._run_prompt_engine", AsyncMock(return_value="<raw>")), \
|
|
40
|
+
patch("sdk.ui.api._screen_agent_inputs", AsyncMock(return_value=[])):
|
|
41
|
+
yield
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class TestAsyncContract:
|
|
45
|
+
def test_generate_returns_job_id(self):
|
|
46
|
+
resp = _client().post("/api/ui/agent-prompt/generate", json={
|
|
47
|
+
"agent_name": "Repo Onboarder", "agent_type": "developer",
|
|
48
|
+
"job": "summarise a repo", "context": "", "engine": "claude_code"})
|
|
49
|
+
assert resp.status_code == 200
|
|
50
|
+
body = resp.json()
|
|
51
|
+
assert set(body) == {"job_id"} and _JOB_RE.match(body["job_id"])
|
|
52
|
+
|
|
53
|
+
def test_regenerate_returns_job_id(self):
|
|
54
|
+
resp = _client().post("/api/ui/agent-prompt/regenerate", json={
|
|
55
|
+
"system_prompt": "# Role\n## Objective\nDo X with {{repo_url}}.",
|
|
56
|
+
"feedback": "stricter", "engine": "claude_code"})
|
|
57
|
+
assert resp.status_code == 200
|
|
58
|
+
assert _JOB_RE.match(resp.json()["job_id"])
|
|
59
|
+
|
|
60
|
+
def test_verify_returns_job_id(self):
|
|
61
|
+
resp = _client().post("/api/ui/agent-prompt/verify", json={
|
|
62
|
+
"system_prompt": "# Role\n## Objective\n## Rules\n## Output\nUse {{repo_url}}."})
|
|
63
|
+
assert resp.status_code == 200
|
|
64
|
+
assert _JOB_RE.match(resp.json()["job_id"])
|
|
65
|
+
|
|
66
|
+
def test_verify_still_422_on_short_prompt(self):
|
|
67
|
+
# Validation stays synchronous (before the job) — no job id for bad input.
|
|
68
|
+
resp = _client().post("/api/ui/agent-prompt/verify", json={"system_prompt": "short"})
|
|
69
|
+
assert resp.status_code == 422
|
|
70
|
+
|
|
71
|
+
def test_generate_still_422_on_empty_required(self):
|
|
72
|
+
resp = _client().post("/api/ui/agent-prompt/generate",
|
|
73
|
+
json={"agent_name": "", "agent_type": "x", "job": "y"})
|
|
74
|
+
assert resp.status_code == 422
|
|
File without changes
|
{devops_bot_sdk-1.4.150 → devops_bot_sdk-1.4.153}/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
|
|
File without changes
|