devops-bot-sdk 1.4.158__tar.gz → 1.4.159__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.158 → devops_bot_sdk-1.4.159}/PKG-INFO +2 -1
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.4.159}/devops_bot_sdk.egg-info/PKG-INFO +2 -1
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.4.159}/devops_bot_sdk.egg-info/SOURCES.txt +5 -1
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.4.159}/devops_bot_sdk.egg-info/requires.txt +1 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.4.159}/pyproject.toml +5 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.4.159}/sdk/__init__.py +1 -1
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.4.159}/sdk/agentd.py +18 -0
- devops_bot_sdk-1.4.159/sdk/agents/activity.py +110 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.4.159}/sdk/cli.py +12 -1
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.4.159}/sdk/client.py +29 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.4.159}/sdk/ui/api.py +41 -22
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.4.159}/sdk/ui/server.py +34 -1
- devops_bot_sdk-1.4.159/sdk/vps_health.py +264 -0
- devops_bot_sdk-1.4.159/tests/test_agent_activity.py +93 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.4.159}/tests/test_otp_login.py +8 -0
- devops_bot_sdk-1.4.159/tests/test_vps_health.py +209 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.4.159}/README.md +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.4.159}/devops_bot_sdk.egg-info/dependency_links.txt +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.4.159}/devops_bot_sdk.egg-info/entry_points.txt +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.4.159}/devops_bot_sdk.egg-info/top_level.txt +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.4.159}/sdk/agents/__init__.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.4.159}/sdk/agents/builder.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.4.159}/sdk/agents/chat.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.4.159}/sdk/agents/connectors.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.4.159}/sdk/agents/engines/__init__.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.4.159}/sdk/agents/engines/claude_code.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.4.159}/sdk/agents/engines/http.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.4.159}/sdk/agents/engines/mcp.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.4.159}/sdk/agents/engines/n8n.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.4.159}/sdk/agents/engines/shell.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.4.159}/sdk/agents/integrations.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.4.159}/sdk/agents/inventory.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.4.159}/sdk/agents/library/__init__.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.4.159}/sdk/agents/manifest.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.4.159}/sdk/agents/prompt_studio.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.4.159}/sdk/agents/prompt_template.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.4.159}/sdk/agents/prompt_vars.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.4.159}/sdk/agents/run_log.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.4.159}/sdk/agents/secrets.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.4.159}/sdk/agents/ticket_run.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.4.159}/sdk/agents/usage.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.4.159}/sdk/agents/varstore.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.4.159}/sdk/collectors/__init__.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.4.159}/sdk/collectors/files.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.4.159}/sdk/collectors/process.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.4.159}/sdk/collectors/screenshot.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.4.159}/sdk/config.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.4.159}/sdk/crucial.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.4.159}/sdk/design_verify/__init__.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.4.159}/sdk/design_verify/assets.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.4.159}/sdk/design_verify/bootstrap.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.4.159}/sdk/design_verify/browser.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.4.159}/sdk/design_verify/compare.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.4.159}/sdk/design_verify/loop.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.4.159}/sdk/exceptions.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.4.159}/sdk/git_ops.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.4.159}/sdk/graphify.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.4.159}/sdk/hooks/__init__.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.4.159}/sdk/hooks/crucial_guard.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.4.159}/sdk/ipc/__init__.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.4.159}/sdk/ipc/electron_bridge.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.4.159}/sdk/ipc/handlers.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.4.159}/sdk/local_exec.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.4.159}/sdk/models/__init__.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.4.159}/sdk/models/envelope.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.4.159}/sdk/models/requests.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.4.159}/sdk/models/responses.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.4.159}/sdk/models/snapshots.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.4.159}/sdk/platform_compat.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.4.159}/sdk/py.typed +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.4.159}/sdk/run_auto.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.4.159}/sdk/sse.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.4.159}/sdk/test.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.4.159}/sdk/test_pipeline.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.4.159}/sdk/ui/__init__.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.4.159}/sdk/ui/dashboard.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.4.159}/sdk/ui/health.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.4.159}/sdk/ui/ip_allowlist.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.4.159}/sdk/ui/jobs.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.4.159}/sdk/ui/models.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.4.159}/sdk/ui/runner.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.4.159}/sdk/ui/security.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.4.159}/sdk/ui/service.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.4.159}/sdk/ui/session.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.4.159}/sdk/ui/setup_manager.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.4.159}/sdk/ui/static/agents.html +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.4.159}/sdk/ui/static/app.js +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.4.159}/sdk/ui/static/custom-agents.html +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.4.159}/sdk/ui/static/default-agents.html +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.4.159}/sdk/ui/static/index.html +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.4.159}/sdk/ui/static/login.html +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.4.159}/sdk/ui/static/runs.html +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.4.159}/sdk/ui/static/setup.html +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.4.159}/sdk/ui/static/styles.css +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.4.159}/sdk/ui/static/tickets.html +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.4.159}/sdk/updater.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.4.159}/setup.cfg +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.4.159}/tests/test_agent_chat_features.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.4.159}/tests/test_agent_prompt_async.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.4.159}/tests/test_agent_prompt_models.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.4.159}/tests/test_agent_run_ticket.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.4.159}/tests/test_agents_connectors_builder.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.4.159}/tests/test_git_action.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.4.159}/tests/test_health.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.4.159}/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.
|
|
3
|
+
Version: 1.4.159
|
|
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
|
|
@@ -20,6 +20,7 @@ Requires-Dist: psutil>=5.9; extra == "process"
|
|
|
20
20
|
Provides-Extra: ui
|
|
21
21
|
Requires-Dist: fastapi>=0.110; extra == "ui"
|
|
22
22
|
Requires-Dist: uvicorn>=0.29; extra == "ui"
|
|
23
|
+
Requires-Dist: psutil>=5.9; extra == "ui"
|
|
23
24
|
Provides-Extra: all
|
|
24
25
|
Requires-Dist: Pillow>=10.0; extra == "all"
|
|
25
26
|
Requires-Dist: psutil>=5.9; extra == "all"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: devops-bot-sdk
|
|
3
|
-
Version: 1.4.
|
|
3
|
+
Version: 1.4.159
|
|
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
|
|
@@ -20,6 +20,7 @@ Requires-Dist: psutil>=5.9; extra == "process"
|
|
|
20
20
|
Provides-Extra: ui
|
|
21
21
|
Requires-Dist: fastapi>=0.110; extra == "ui"
|
|
22
22
|
Requires-Dist: uvicorn>=0.29; extra == "ui"
|
|
23
|
+
Requires-Dist: psutil>=5.9; extra == "ui"
|
|
23
24
|
Provides-Extra: all
|
|
24
25
|
Requires-Dist: Pillow>=10.0; extra == "all"
|
|
25
26
|
Requires-Dist: psutil>=5.9; extra == "all"
|
|
@@ -23,7 +23,9 @@ sdk/sse.py
|
|
|
23
23
|
sdk/test.py
|
|
24
24
|
sdk/test_pipeline.py
|
|
25
25
|
sdk/updater.py
|
|
26
|
+
sdk/vps_health.py
|
|
26
27
|
sdk/agents/__init__.py
|
|
28
|
+
sdk/agents/activity.py
|
|
27
29
|
sdk/agents/builder.py
|
|
28
30
|
sdk/agents/chat.py
|
|
29
31
|
sdk/agents/connectors.py
|
|
@@ -88,6 +90,7 @@ sdk/ui/static/runs.html
|
|
|
88
90
|
sdk/ui/static/setup.html
|
|
89
91
|
sdk/ui/static/styles.css
|
|
90
92
|
sdk/ui/static/tickets.html
|
|
93
|
+
tests/test_agent_activity.py
|
|
91
94
|
tests/test_agent_chat_features.py
|
|
92
95
|
tests/test_agent_prompt_async.py
|
|
93
96
|
tests/test_agent_prompt_models.py
|
|
@@ -96,4 +99,5 @@ tests/test_agents_connectors_builder.py
|
|
|
96
99
|
tests/test_git_action.py
|
|
97
100
|
tests/test_health.py
|
|
98
101
|
tests/test_otp_login.py
|
|
99
|
-
tests/test_updater_extras.py
|
|
102
|
+
tests/test_updater_extras.py
|
|
103
|
+
tests/test_vps_health.py
|
|
@@ -31,6 +31,11 @@ process = [
|
|
|
31
31
|
ui = [
|
|
32
32
|
"fastapi>=0.110", # sdk/ui — control-panel web app
|
|
33
33
|
"uvicorn>=0.29", # ASGI server for `agentos-ui`
|
|
34
|
+
"psutil>=5.9", # sdk/ui/health.py — CPU/RAM/disk/uptime for GET /api/ui/health
|
|
35
|
+
# AND the 2-hourly allocation PATCH (sdk/vps_health.py). The
|
|
36
|
+
# bootstrap installs `[ui]` only, so without it here every
|
|
37
|
+
# VPS reports "resources.available: False" and the health
|
|
38
|
+
# cron can only send status + IP.
|
|
34
39
|
]
|
|
35
40
|
all = [
|
|
36
41
|
"Pillow>=10.0",
|
|
@@ -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.159" # 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
|
|
@@ -85,6 +85,7 @@ from typing import TYPE_CHECKING, Any
|
|
|
85
85
|
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
|
86
86
|
from apscheduler.triggers.cron import CronTrigger
|
|
87
87
|
|
|
88
|
+
from sdk.agents import activity
|
|
88
89
|
from sdk.client import BackendClient
|
|
89
90
|
from sdk.models.requests import OrchestrateRequest
|
|
90
91
|
|
|
@@ -354,6 +355,9 @@ async def _dispatch_agent_run(
|
|
|
354
355
|
|
|
355
356
|
policy = manifest.approval.policy
|
|
356
357
|
handled: set[str] = set() # session/task ids already round-tripped by this dispatch
|
|
358
|
+
# How this run was triggered — carried into the `agent_activities` line so the
|
|
359
|
+
# feed distinguishes a cron firing from a ticket trigger from a human "Run".
|
|
360
|
+
_activity_kind = "run-ticket" if auto_approve else ("ticket_label" if task else "scheduled")
|
|
357
361
|
|
|
358
362
|
async def _on_status_update(status: str, state: dict) -> None:
|
|
359
363
|
if status != "awaiting_approval":
|
|
@@ -402,11 +406,21 @@ async def _dispatch_agent_run(
|
|
|
402
406
|
async def _tee(ev: dict) -> None:
|
|
403
407
|
run_log.log_event(_log_path, ev)
|
|
404
408
|
|
|
409
|
+
_started = time.monotonic()
|
|
405
410
|
local_result = await claude_code_engine.run(
|
|
406
411
|
manifest, skill_text, mcp_config, extra_env=effective_secrets or None,
|
|
407
412
|
on_event=_tee,
|
|
408
413
|
)
|
|
409
414
|
run_log.finish_run(_log_path, local_result)
|
|
415
|
+
# Local ledger + platform `agent_activities` feed. EVERY trigger path
|
|
416
|
+
# reaches this function (cron schedule, ticket label, on-demand
|
|
417
|
+
# run-on-ticket from the UI / CLI / desktop IPC), so a VPS agent run is
|
|
418
|
+
# recorded exactly once regardless of origin — see sdk.agents.activity.
|
|
419
|
+
await activity.record_run(
|
|
420
|
+
manifest.name, _activity_kind, local_result,
|
|
421
|
+
time.monotonic() - _started, client=client,
|
|
422
|
+
detail=f"task {task_id}" if task_id else "",
|
|
423
|
+
)
|
|
410
424
|
outcome.update({
|
|
411
425
|
"task_id": task_id,
|
|
412
426
|
"result": local_result.get("result", ""),
|
|
@@ -418,6 +432,10 @@ async def _dispatch_agent_run(
|
|
|
418
432
|
manifest.name, task_id, exc,
|
|
419
433
|
)
|
|
420
434
|
outcome["error"] = str(exc)
|
|
435
|
+
await activity.record_run(
|
|
436
|
+
manifest.name, _activity_kind, {"ok": False, "error": str(exc)},
|
|
437
|
+
0.0, client=client, detail=f"task {task_id}" if task_id else "",
|
|
438
|
+
)
|
|
421
439
|
return
|
|
422
440
|
|
|
423
441
|
# FINALIZE — same field names sdk.local_exec.run_claude_local's result
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""Agent-activity reporting — the ONE place a custom / department agent run is
|
|
2
|
+
recorded, for both sinks:
|
|
3
|
+
|
|
4
|
+
* the LOCAL usage ledger (``sdk.agents.usage`` → ``~/.agentos/usage.jsonl``),
|
|
5
|
+
which backs ``GET /api/ui/agents/usage`` and, through it, the AI backend
|
|
6
|
+
report's "VPS Agent Logs" section, and
|
|
7
|
+
* the PLATFORM activity feed — ``POST {ML_GATEWAY}/ml-api/agent-activity``
|
|
8
|
+
with ``{activity, agentId}``, which the ML backend writes into the Postgres
|
|
9
|
+
``agent_activities`` table (docs/DATABASE_SCHEMA.md #3: ``user_id``,
|
|
10
|
+
``activity``, ``agent_id``) and the AI backend reads back RBAC-scoped for
|
|
11
|
+
reporting + chat.
|
|
12
|
+
|
|
13
|
+
Every VPS-side agent run goes through here no matter how it was triggered — the
|
|
14
|
+
control panel (chat / run / run-on-ticket), an ``agentd`` cron schedule, an
|
|
15
|
+
``agentd`` ticket-label trigger, the CLI, or the desktop IPC bridge. Before this
|
|
16
|
+
module only the control-panel paths emitted, so scheduled and ticket-triggered
|
|
17
|
+
runs never reached ``agent_activities``.
|
|
18
|
+
|
|
19
|
+
``agent_activities.agent_id`` is an FK onto ``agents.id``, so only agents that
|
|
20
|
+
exist in the BACKEND emit — ``BackendClient.resolve_agent_id`` returns None for
|
|
21
|
+
a local-only manifest and the emit is skipped (the local ledger still records
|
|
22
|
+
it). Both custom and department-default agents resolve, so both are tracked.
|
|
23
|
+
|
|
24
|
+
Best-effort throughout: nothing in here raises into a run.
|
|
25
|
+
"""
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import logging
|
|
29
|
+
from typing import TYPE_CHECKING, Any
|
|
30
|
+
|
|
31
|
+
if TYPE_CHECKING: # pragma: no cover — typing only
|
|
32
|
+
from sdk.client import BackendClient
|
|
33
|
+
|
|
34
|
+
logger = logging.getLogger("sdk.agents.activity")
|
|
35
|
+
|
|
36
|
+
# How a run reached us → the verb that opens the activity line. Keep these short:
|
|
37
|
+
# the feed is rendered as one line per row in reports and in chat answers.
|
|
38
|
+
_VERB = {
|
|
39
|
+
"chat": "Chatted with",
|
|
40
|
+
"run": "Ran",
|
|
41
|
+
"run-ticket": "Ran",
|
|
42
|
+
"scheduled": "Ran (schedule)",
|
|
43
|
+
"ticket_label": "Ran (ticket trigger)",
|
|
44
|
+
"cli": "Ran (cli)",
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def describe(name: str, kind: str, result: dict | None, duration_s: float = 0.0,
|
|
49
|
+
detail: str = "") -> str:
|
|
50
|
+
"""The human-readable ``activity`` string stored in ``agent_activities``."""
|
|
51
|
+
res = result or {}
|
|
52
|
+
verb = _VERB.get(kind, "Ran")
|
|
53
|
+
status = "ok" if res.get("ok") else "error"
|
|
54
|
+
line = f"{verb} agent '{name}' — {status}"
|
|
55
|
+
if duration_s:
|
|
56
|
+
line += f" ({round(float(duration_s), 1)}s)"
|
|
57
|
+
err = str(res.get("error") or "").strip()
|
|
58
|
+
if status == "error" and err:
|
|
59
|
+
line += f": {err[:200]}"
|
|
60
|
+
if detail:
|
|
61
|
+
line += f" — {detail}"
|
|
62
|
+
return line
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
async def emit(name: str, kind: str, result: dict | None, duration_s: float = 0.0,
|
|
66
|
+
*, client: "BackendClient | None" = None, detail: str = "") -> bool:
|
|
67
|
+
"""Emit ONE ``{activity, agentId}`` event for a finished agent run.
|
|
68
|
+
|
|
69
|
+
Returns True when the webhook accepted it, False otherwise (unknown agent,
|
|
70
|
+
no token, backend down) — never raises.
|
|
71
|
+
"""
|
|
72
|
+
try:
|
|
73
|
+
if client is None:
|
|
74
|
+
from sdk.client import BackendClient
|
|
75
|
+
client = BackendClient.from_config()
|
|
76
|
+
agent_id = await client.resolve_agent_id(name)
|
|
77
|
+
if not agent_id: # local-only agent → not tracked
|
|
78
|
+
return False
|
|
79
|
+
return await client.log_agent_activity(
|
|
80
|
+
describe(name, kind, result, duration_s, detail), agent_id,
|
|
81
|
+
)
|
|
82
|
+
except Exception as exc: # noqa: BLE001 — activity emit must never break a run
|
|
83
|
+
logger.debug("activity emit failed agent=%s err=%s", name, exc)
|
|
84
|
+
return False
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
async def record_run(name: str, kind: str, result: dict | None, duration_s: float = 0.0,
|
|
88
|
+
*, client: "BackendClient | None" = None, detail: str = "") -> None:
|
|
89
|
+
"""Record a finished run to BOTH sinks: local ledger, then platform feed.
|
|
90
|
+
|
|
91
|
+
The ledger write happens first and independently of the webhook, so a
|
|
92
|
+
backend outage still leaves per-agent utilisation on the VPS (which is what
|
|
93
|
+
``/api/ui/agents/usage`` and the report's "VPS Agent Logs" read).
|
|
94
|
+
"""
|
|
95
|
+
res: dict[str, Any] = result or {}
|
|
96
|
+
try:
|
|
97
|
+
from sdk.agents import usage as usage_mod
|
|
98
|
+
usage_mod.record(
|
|
99
|
+
name,
|
|
100
|
+
kind=kind,
|
|
101
|
+
duration_s=duration_s,
|
|
102
|
+
input_tokens=res.get("input_tokens", 0) or 0,
|
|
103
|
+
output_tokens=res.get("output_tokens", 0) or 0,
|
|
104
|
+
cost_usd=res.get("cost_usd", 0.0) or 0.0,
|
|
105
|
+
ok=bool(res.get("ok")),
|
|
106
|
+
)
|
|
107
|
+
except Exception as exc: # noqa: BLE001 — ledger write must never break a run
|
|
108
|
+
logger.debug("activity ledger write failed agent=%s err=%s", name, exc)
|
|
109
|
+
|
|
110
|
+
await emit(name, kind, res, duration_s, client=client, detail=detail)
|
|
@@ -167,10 +167,21 @@ def _agent_command(args: list[str]) -> None:
|
|
|
167
167
|
|
|
168
168
|
|
|
169
169
|
def _agent_chat(name: str, message: str) -> None:
|
|
170
|
+
import time as _time
|
|
171
|
+
|
|
172
|
+
from sdk.agents import activity
|
|
170
173
|
from sdk.agents.chat import chat_with_agent
|
|
171
174
|
|
|
175
|
+
async def _run() -> dict:
|
|
176
|
+
started = _time.monotonic()
|
|
177
|
+
res = await chat_with_agent(name, message)
|
|
178
|
+
# Same two sinks as the control panel: local usage ledger + the platform
|
|
179
|
+
# `agent_activities` feed. A CLI chat is as much an agent run as a UI one.
|
|
180
|
+
await activity.record_run(name, "chat", res, _time.monotonic() - started)
|
|
181
|
+
return res
|
|
182
|
+
|
|
172
183
|
try:
|
|
173
|
-
result = asyncio.run(
|
|
184
|
+
result = asyncio.run(_run())
|
|
174
185
|
except LookupError as exc:
|
|
175
186
|
print(f"error: {exc}")
|
|
176
187
|
sys.exit(1)
|
|
@@ -1093,6 +1093,35 @@ class BackendClient:
|
|
|
1093
1093
|
_AGENT_ID_CACHE[nm] = "" # negative-cache: not a backend agent
|
|
1094
1094
|
return None
|
|
1095
1095
|
|
|
1096
|
+
async def report_vps_allocation(self, payload: dict) -> bool:
|
|
1097
|
+
"""PATCH ``{ML_GATEWAY_URL}/ml-api/contabo/allocation`` — this machine's health.
|
|
1098
|
+
|
|
1099
|
+
The allocation row is resolved SERVER-side from the ``co_`` token (one row
|
|
1100
|
+
per user — ``contabo_allocation``, docs/DATABASE_SCHEMA.md #18), so the body
|
|
1101
|
+
carries metrics only: ``status``, ``cpuUsage``, ``ramUsage``, ``usedRam``,
|
|
1102
|
+
``totalRam``, ``diskUsage``, ``totalDisk``, ``bandwidthUsed``, ``ipAddress``,
|
|
1103
|
+
``uptimeSeconds``, ``lastPingedAt``.
|
|
1104
|
+
|
|
1105
|
+
Driven by the SDK-side health cron (``sdk.vps_health``) — deliberately NOT by
|
|
1106
|
+
the AI backend, so VPS liveness reporting does not depend on that service.
|
|
1107
|
+
Best-effort: never raises; a failed ping is logged and the next tick retries.
|
|
1108
|
+
"""
|
|
1109
|
+
from sdk.config import ML_GATEWAY_URL
|
|
1110
|
+
|
|
1111
|
+
url = f"{ML_GATEWAY_URL.rstrip('/')}/ml-api/contabo/allocation"
|
|
1112
|
+
headers = {**self._headers, "accept": "*/*", "developer-name": "null", "product": ""}
|
|
1113
|
+
try:
|
|
1114
|
+
async with httpx.AsyncClient(timeout=20.0) as client:
|
|
1115
|
+
resp = await client.patch(url, json=payload, headers=headers)
|
|
1116
|
+
if resp.status_code >= 400:
|
|
1117
|
+
logger.warning("client.report_vps_allocation failed status=%s body=%s",
|
|
1118
|
+
resp.status_code, resp.text[:200])
|
|
1119
|
+
return False
|
|
1120
|
+
return True
|
|
1121
|
+
except Exception as exc: # noqa: BLE001
|
|
1122
|
+
logger.warning("client.report_vps_allocation error err=%s", exc)
|
|
1123
|
+
return False
|
|
1124
|
+
|
|
1096
1125
|
async def notify_whatsapp(
|
|
1097
1126
|
self,
|
|
1098
1127
|
task_id: str,
|
|
@@ -180,6 +180,33 @@ async def health() -> dict:
|
|
|
180
180
|
return collect_health()
|
|
181
181
|
|
|
182
182
|
|
|
183
|
+
@router.get("/vps/allocation", dependencies=[_auth])
|
|
184
|
+
async def vps_allocation_status() -> dict:
|
|
185
|
+
"""State of the SDK-side health cron: enabled?, interval, last ping attempt.
|
|
186
|
+
|
|
187
|
+
The cron (``sdk.vps_health``, started by this server's lifespan) PATCHes
|
|
188
|
+
``/ml-api/contabo/allocation`` every 2h. This is the read-only view of it —
|
|
189
|
+
useful when a ``contabo_allocation`` row looks stale.
|
|
190
|
+
"""
|
|
191
|
+
from sdk import vps_health
|
|
192
|
+
|
|
193
|
+
return vps_health.last_report()
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
@router.post("/vps/allocation/report", dependencies=[_auth])
|
|
197
|
+
async def vps_allocation_report() -> dict:
|
|
198
|
+
"""Force ONE health ping now (out of band from the 2h cron) and report the result.
|
|
199
|
+
|
|
200
|
+
Same payload the cron sends — for QA / first-boot verification without waiting
|
|
201
|
+
two hours. Returns ``{ok, payload}``; ``ok=False`` on a rejected or unreachable
|
|
202
|
+
PATCH (the reason is in ``GET /vps/allocation``).
|
|
203
|
+
"""
|
|
204
|
+
from sdk import vps_health
|
|
205
|
+
|
|
206
|
+
ok = await vps_health.report_once()
|
|
207
|
+
return {"ok": ok, **vps_health.last_report()}
|
|
208
|
+
|
|
209
|
+
|
|
183
210
|
# ── IP allowlist management ─────────────────────────────────────────────────
|
|
184
211
|
# Whitelist a client IP for THIS SDK. A whitelisted IP passes the IP gate for
|
|
185
212
|
# EVERY path (all /api/ui/* endpoints + the UI + /docs); data endpoints still
|
|
@@ -379,23 +406,19 @@ async def logs_stream() -> StreamingResponse:
|
|
|
379
406
|
# ── Agent-activity webhook ──────────────────────────────────────────────────
|
|
380
407
|
# Every custom/default agent run/chat records to the LOCAL usage ledger AND emits
|
|
381
408
|
# a `{activity, agentId}` event to the ML gateway (POST /ml-api/agent-activity),
|
|
382
|
-
# which stores it for RBAC-scoped reporting
|
|
383
|
-
# exist in the backend (have an agentId) are emitted — local-only
|
|
384
|
-
# Best-effort: never raises into the run.
|
|
409
|
+
# which stores it in Postgres `agent_activities` for RBAC-scoped reporting + chat.
|
|
410
|
+
# Only agents that exist in the backend (have an agentId) are emitted — local-only
|
|
411
|
+
# agents are ledger-only. Best-effort: never raises into the run.
|
|
412
|
+
#
|
|
413
|
+
# The composition + emit itself lives in `sdk.agents.activity` so the agentd
|
|
414
|
+
# trigger paths (cron schedule, ticket label) report identically; this is a thin
|
|
415
|
+
# wrapper kept for the panel's direct chat/run endpoints, which already write the
|
|
416
|
+
# usage ledger themselves.
|
|
385
417
|
|
|
386
418
|
async def _emit_agent_activity(name: str, kind: str, result: dict, duration_s: float = 0.0) -> None:
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
agent_id = await client.resolve_agent_id(name)
|
|
391
|
-
if not agent_id: # local-only agent → not tracked
|
|
392
|
-
return
|
|
393
|
-
verb = "Chatted with" if kind == "chat" else "Ran"
|
|
394
|
-
status = "ok" if result.get("ok") else "error"
|
|
395
|
-
detail = f" ({round(duration_s, 1)}s)" if duration_s else ""
|
|
396
|
-
await client.log_agent_activity(f"{verb} agent '{name}' — {status}{detail}", agent_id)
|
|
397
|
-
except Exception: # noqa: BLE001 — activity emit must never break a run
|
|
398
|
-
pass
|
|
419
|
+
from sdk.agents import activity
|
|
420
|
+
|
|
421
|
+
await activity.emit(name, kind, result, duration_s)
|
|
399
422
|
|
|
400
423
|
|
|
401
424
|
# ── Agents: catalog, creation, chat, default agents ────────────────────────
|
|
@@ -863,21 +886,17 @@ async def agent_run_ticket(body: AgentTicketRunRequest) -> dict:
|
|
|
863
886
|
department_id = body.department_id
|
|
864
887
|
|
|
865
888
|
async def work() -> dict:
|
|
866
|
-
import time as _time
|
|
867
|
-
|
|
868
889
|
from sdk.agents import ticket_run
|
|
869
890
|
from sdk.client import BackendClient
|
|
870
891
|
|
|
871
892
|
client = BackendClient.from_config()
|
|
872
|
-
started = _time.monotonic()
|
|
873
893
|
outcome = await ticket_run.run_on_ticket(
|
|
874
894
|
client, name, ticket_key, scope=scope, department_id=department_id,
|
|
875
895
|
variables=variables, secrets=secrets,
|
|
876
896
|
)
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
)
|
|
897
|
+
# No activity/ledger write here: this path runs through
|
|
898
|
+
# `agentd._dispatch_agent_run`, which records the run itself (kind
|
|
899
|
+
# "run-ticket") — emitting again would double-count it.
|
|
881
900
|
return {"ticket_key": ticket_key, **(outcome if isinstance(outcome, dict) else {})}
|
|
882
901
|
|
|
883
902
|
return {"job_id": jobs.start(work())}
|
|
@@ -23,6 +23,9 @@ _STATIC_DIR = Path(__file__).parent / "static"
|
|
|
23
23
|
|
|
24
24
|
def create_app():
|
|
25
25
|
"""Build the FastAPI app (importable for tests)."""
|
|
26
|
+
import asyncio
|
|
27
|
+
import contextlib
|
|
28
|
+
|
|
26
29
|
from fastapi import FastAPI, Request
|
|
27
30
|
from fastapi.responses import FileResponse, RedirectResponse
|
|
28
31
|
from fastapi.staticfiles import StaticFiles
|
|
@@ -31,12 +34,42 @@ def create_app():
|
|
|
31
34
|
from sdk.ui.api import router as api_router
|
|
32
35
|
from sdk.ui.ip_allowlist import IPAllowlistMiddleware
|
|
33
36
|
|
|
37
|
+
@contextlib.asynccontextmanager
|
|
38
|
+
async def lifespan(_app):
|
|
39
|
+
"""Own the VPS health cron for the life of the server.
|
|
40
|
+
|
|
41
|
+
This process is the one thing that runs 24/7 on a provisioned VPS (the
|
|
42
|
+
bootstrap installs it as a systemd/launchd/scheduled-task service), so the
|
|
43
|
+
2-hourly ``PATCH /ml-api/contabo/allocation`` ping rides along here rather
|
|
44
|
+
than in ``agentd`` (optional) or the AI backend (would need inbound reach).
|
|
45
|
+
Opt out with ``AGENTOS_VPS_HEALTH=0``. See ``sdk.vps_health``.
|
|
46
|
+
"""
|
|
47
|
+
from sdk import vps_health
|
|
48
|
+
|
|
49
|
+
task = None
|
|
50
|
+
try:
|
|
51
|
+
task = vps_health.start()
|
|
52
|
+
except Exception: # noqa: BLE001 — the panel must serve even if the cron won't start
|
|
53
|
+
task = None
|
|
54
|
+
try:
|
|
55
|
+
yield
|
|
56
|
+
finally:
|
|
57
|
+
if task is not None:
|
|
58
|
+
task.cancel()
|
|
59
|
+
# CancelledError is a BaseException — suppress it explicitly, or the
|
|
60
|
+
# cancellation we just requested surfaces as a failed shutdown.
|
|
61
|
+
try:
|
|
62
|
+
await task
|
|
63
|
+
except (asyncio.CancelledError, Exception): # noqa: BLE001
|
|
64
|
+
pass
|
|
65
|
+
|
|
34
66
|
# Swagger at /docs (ReDoc /redoc, schema /openapi.json). By default the whole
|
|
35
67
|
# server — docs included — is locked by the IP allowlist below. With
|
|
36
68
|
# AGENTOS_UI_PUBLIC=1 the IP gate is lifted: /docs is browsable by anyone and
|
|
37
69
|
# /api/ui/* is guarded solely by the co_ token (paste it in Swagger's Authorize).
|
|
38
70
|
from sdk import __version__ as _sdk_version
|
|
39
|
-
app = FastAPI(title="AgentOS SDK", version=_sdk_version, docs_url="/docs",
|
|
71
|
+
app = FastAPI(title="AgentOS SDK", version=_sdk_version, docs_url="/docs",
|
|
72
|
+
redoc_url="/redoc", lifespan=lifespan)
|
|
40
73
|
# Inbound half of the two-way link: unless AGENTOS_UI_PUBLIC=1, only loopback,
|
|
41
74
|
# this host's own IPs, and whitelisted IPs (the AI backend) may reach the server
|
|
42
75
|
# AT ALL. Configure via AGENTOS_UI_ALLOWED_IPS or the config.toml `allowed_ips` key.
|