devops-bot-sdk 1.4.158__tar.gz → 1.5.0__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.5.0}/PKG-INFO +2 -1
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/devops_bot_sdk.egg-info/PKG-INFO +2 -1
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/devops_bot_sdk.egg-info/SOURCES.txt +7 -1
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/devops_bot_sdk.egg-info/requires.txt +1 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/pyproject.toml +5 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/__init__.py +2 -2
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/agentd.py +18 -0
- devops_bot_sdk-1.5.0/sdk/agents/activity.py +110 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/cli.py +73 -2
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/client.py +107 -10
- devops_bot_sdk-1.5.0/sdk/github_ingest.py +345 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/ui/api.py +75 -22
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/ui/server.py +45 -1
- devops_bot_sdk-1.5.0/sdk/vps_health.py +264 -0
- devops_bot_sdk-1.5.0/tests/test_agent_activity.py +93 -0
- devops_bot_sdk-1.5.0/tests/test_github_ingest.py +327 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/tests/test_otp_login.py +8 -0
- devops_bot_sdk-1.5.0/tests/test_vps_health.py +209 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/README.md +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/devops_bot_sdk.egg-info/dependency_links.txt +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/devops_bot_sdk.egg-info/entry_points.txt +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/devops_bot_sdk.egg-info/top_level.txt +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/agents/__init__.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/agents/builder.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/agents/chat.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/agents/connectors.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/agents/engines/__init__.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/agents/engines/claude_code.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/agents/engines/http.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/agents/engines/mcp.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/agents/engines/n8n.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/agents/engines/shell.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/agents/integrations.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/agents/inventory.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/agents/library/__init__.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/agents/manifest.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/agents/prompt_studio.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/agents/prompt_template.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/agents/prompt_vars.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/agents/run_log.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/agents/secrets.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/agents/ticket_run.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/agents/usage.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/agents/varstore.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/collectors/__init__.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/collectors/files.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/collectors/process.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/collectors/screenshot.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/config.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/crucial.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/design_verify/__init__.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/design_verify/assets.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/design_verify/bootstrap.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/design_verify/browser.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/design_verify/compare.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/design_verify/loop.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/exceptions.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/git_ops.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/graphify.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/hooks/__init__.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/hooks/crucial_guard.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/ipc/__init__.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/ipc/electron_bridge.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/ipc/handlers.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/local_exec.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/models/__init__.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/models/envelope.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/models/requests.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/models/responses.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/models/snapshots.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/platform_compat.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/py.typed +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/run_auto.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/sse.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/test.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/test_pipeline.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/ui/__init__.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/ui/dashboard.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/ui/health.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/ui/ip_allowlist.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/ui/jobs.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/ui/models.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/ui/runner.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/ui/security.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/ui/service.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/ui/session.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/ui/setup_manager.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/ui/static/agents.html +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/ui/static/app.js +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/ui/static/custom-agents.html +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/ui/static/default-agents.html +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/ui/static/index.html +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/ui/static/login.html +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/ui/static/runs.html +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/ui/static/setup.html +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/ui/static/styles.css +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/ui/static/tickets.html +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/sdk/updater.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/setup.cfg +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/tests/test_agent_chat_features.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/tests/test_agent_prompt_async.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/tests/test_agent_prompt_models.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/tests/test_agent_run_ticket.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/tests/test_agents_connectors_builder.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/tests/test_git_action.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/tests/test_health.py +0 -0
- {devops_bot_sdk-1.4.158 → devops_bot_sdk-1.5.0}/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.
|
|
3
|
+
Version: 1.5.0
|
|
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.
|
|
3
|
+
Version: 1.5.0
|
|
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"
|
|
@@ -14,6 +14,7 @@ sdk/config.py
|
|
|
14
14
|
sdk/crucial.py
|
|
15
15
|
sdk/exceptions.py
|
|
16
16
|
sdk/git_ops.py
|
|
17
|
+
sdk/github_ingest.py
|
|
17
18
|
sdk/graphify.py
|
|
18
19
|
sdk/local_exec.py
|
|
19
20
|
sdk/platform_compat.py
|
|
@@ -23,7 +24,9 @@ sdk/sse.py
|
|
|
23
24
|
sdk/test.py
|
|
24
25
|
sdk/test_pipeline.py
|
|
25
26
|
sdk/updater.py
|
|
27
|
+
sdk/vps_health.py
|
|
26
28
|
sdk/agents/__init__.py
|
|
29
|
+
sdk/agents/activity.py
|
|
27
30
|
sdk/agents/builder.py
|
|
28
31
|
sdk/agents/chat.py
|
|
29
32
|
sdk/agents/connectors.py
|
|
@@ -88,12 +91,15 @@ sdk/ui/static/runs.html
|
|
|
88
91
|
sdk/ui/static/setup.html
|
|
89
92
|
sdk/ui/static/styles.css
|
|
90
93
|
sdk/ui/static/tickets.html
|
|
94
|
+
tests/test_agent_activity.py
|
|
91
95
|
tests/test_agent_chat_features.py
|
|
92
96
|
tests/test_agent_prompt_async.py
|
|
93
97
|
tests/test_agent_prompt_models.py
|
|
94
98
|
tests/test_agent_run_ticket.py
|
|
95
99
|
tests/test_agents_connectors_builder.py
|
|
96
100
|
tests/test_git_action.py
|
|
101
|
+
tests/test_github_ingest.py
|
|
97
102
|
tests/test_health.py
|
|
98
103
|
tests/test_otp_login.py
|
|
99
|
-
tests/test_updater_extras.py
|
|
104
|
+
tests/test_updater_extras.py
|
|
105
|
+
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",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""AgentOS Desktop SDK — thin HTTPS/SSE client for the Electron app.
|
|
2
2
|
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.5.0
|
|
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.
|
|
33
|
+
__version__ = "1.5.0" # 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)
|
|
@@ -96,12 +96,16 @@ def main() -> None:
|
|
|
96
96
|
_default_agents_command(rest)
|
|
97
97
|
return
|
|
98
98
|
|
|
99
|
+
if noun == "github":
|
|
100
|
+
_github_command(rest)
|
|
101
|
+
return
|
|
102
|
+
|
|
99
103
|
_print_usage()
|
|
100
104
|
sys.exit(1)
|
|
101
105
|
|
|
102
106
|
|
|
103
107
|
def _print_usage() -> None:
|
|
104
|
-
print("usage: agentos <configure|agent|integration|default-agents|agentd> ...")
|
|
108
|
+
print("usage: agentos <configure|agent|integration|default-agents|agentd|github> ...")
|
|
105
109
|
print()
|
|
106
110
|
print(" agentos configure [--rotate]")
|
|
107
111
|
print(" agentos agent <list|run NAME|chat NAME MSG|logs NAME|enable NAME|disable NAME|rm NAME>")
|
|
@@ -109,6 +113,7 @@ def _print_usage() -> None:
|
|
|
109
113
|
print(" agentos integration <add PATH|test NAME|list|rm NAME>")
|
|
110
114
|
print(" agentos default-agents list [--department-id UUID] # read-only; agents are created in the web app")
|
|
111
115
|
print(" agentos agentd <start [--foreground]|status>")
|
|
116
|
+
print(" agentos github <ingest [--days N]|status> # PR ingestion; --days N backfills N days")
|
|
112
117
|
|
|
113
118
|
|
|
114
119
|
# ── agent ────────────────────────────────────────────────────────────────
|
|
@@ -167,10 +172,21 @@ def _agent_command(args: list[str]) -> None:
|
|
|
167
172
|
|
|
168
173
|
|
|
169
174
|
def _agent_chat(name: str, message: str) -> None:
|
|
175
|
+
import time as _time
|
|
176
|
+
|
|
177
|
+
from sdk.agents import activity
|
|
170
178
|
from sdk.agents.chat import chat_with_agent
|
|
171
179
|
|
|
180
|
+
async def _run() -> dict:
|
|
181
|
+
started = _time.monotonic()
|
|
182
|
+
res = await chat_with_agent(name, message)
|
|
183
|
+
# Same two sinks as the control panel: local usage ledger + the platform
|
|
184
|
+
# `agent_activities` feed. A CLI chat is as much an agent run as a UI one.
|
|
185
|
+
await activity.record_run(name, "chat", res, _time.monotonic() - started)
|
|
186
|
+
return res
|
|
187
|
+
|
|
172
188
|
try:
|
|
173
|
-
result = asyncio.run(
|
|
189
|
+
result = asyncio.run(_run())
|
|
174
190
|
except LookupError as exc:
|
|
175
191
|
print(f"error: {exc}")
|
|
176
192
|
sys.exit(1)
|
|
@@ -579,5 +595,60 @@ def _agentd_command(args: list[str]) -> None:
|
|
|
579
595
|
sys.exit(1)
|
|
580
596
|
|
|
581
597
|
|
|
598
|
+
# ── github ───────────────────────────────────────────────────────────────
|
|
599
|
+
|
|
600
|
+
def _github_command(args: list[str]) -> None:
|
|
601
|
+
"""`agentos github ingest [--days N]` / `agentos github status`.
|
|
602
|
+
|
|
603
|
+
The daily ingestion normally runs by itself inside `agentos-ui` (see
|
|
604
|
+
sdk.github_ingest). This is the manual handle: `ingest` runs one now,
|
|
605
|
+
`--days N` forces an N-day window — that is the backfill.
|
|
606
|
+
"""
|
|
607
|
+
from sdk import github_ingest as gi
|
|
608
|
+
|
|
609
|
+
verb = args[0] if args else None
|
|
610
|
+
|
|
611
|
+
if verb == "status":
|
|
612
|
+
st = gi.status()
|
|
613
|
+
state = st.get("state") or {}
|
|
614
|
+
print(f"enabled : {st['enabled']}")
|
|
615
|
+
print(f"daily slot : {st['slot_utc']} UTC")
|
|
616
|
+
print(f"catch-up cap : {st['lookback_days']} days")
|
|
617
|
+
print(f"first-run seed : {st['backfill_days']} days"
|
|
618
|
+
f"{'' if st['backfilled'] else ' ← not run yet; the next run backfills'}")
|
|
619
|
+
print(f"last success : {state.get('last_success_at') or '(never)'}")
|
|
620
|
+
print(f"last status : {state.get('last_status') or '(none)'}")
|
|
621
|
+
if state.get("last_job_id"):
|
|
622
|
+
print(f"last job : {state['last_job_id']} {state.get('last_status_url', '')}")
|
|
623
|
+
if state.get("last_error"):
|
|
624
|
+
print(f"last error : {state['last_error']}")
|
|
625
|
+
return
|
|
626
|
+
|
|
627
|
+
if verb == "ingest":
|
|
628
|
+
days = None
|
|
629
|
+
if "--days" in args:
|
|
630
|
+
try:
|
|
631
|
+
days = int(args[args.index("--days") + 1])
|
|
632
|
+
except (IndexError, ValueError):
|
|
633
|
+
print("error: --days needs a whole number of days, e.g. --days 50")
|
|
634
|
+
sys.exit(1)
|
|
635
|
+
result = asyncio.run(gi.run_once(days=days))
|
|
636
|
+
status = result.get("status")
|
|
637
|
+
if status == "queued":
|
|
638
|
+
print(f"queued: PRs since {result['since']} job_id={result.get('job_id') or '(none)'}")
|
|
639
|
+
if result.get("status_url"):
|
|
640
|
+
print(f"poll : {result['status_url']} (on the AI backend)")
|
|
641
|
+
return
|
|
642
|
+
if status == "no_github_link":
|
|
643
|
+
print("nothing to do: this account has no GitHub connected.")
|
|
644
|
+
return
|
|
645
|
+
print(f"failed: {result.get('reason') or 'unknown error'}")
|
|
646
|
+
sys.exit(1)
|
|
647
|
+
|
|
648
|
+
print("usage: agentos github ingest [--days N] # N-day backfill; omit for the normal window")
|
|
649
|
+
print(" agentos github status")
|
|
650
|
+
sys.exit(1)
|
|
651
|
+
|
|
652
|
+
|
|
582
653
|
if __name__ == "__main__":
|
|
583
654
|
main()
|
|
@@ -763,13 +763,18 @@ class BackendClient:
|
|
|
763
763
|
except Exception:
|
|
764
764
|
return "pending"
|
|
765
765
|
|
|
766
|
-
async def
|
|
767
|
-
"""GET /ml-api/github/token → the
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
766
|
+
async def github_token_record(self) -> dict | None:
|
|
767
|
+
"""GET /ml-api/github/token → the caller's WHOLE GitHub link record.
|
|
768
|
+
|
|
769
|
+
Returns the ``data`` object — ``{id, userId, githubId, username,
|
|
770
|
+
accessToken, scope, …}`` — or None when this user has no GitHub link, the
|
|
771
|
+
token is rejected, or the backend is unreachable. Inherently per-user: the
|
|
772
|
+
ML backend resolves WHICH link to return from the ``co_`` token, so there
|
|
773
|
+
is no list-all form.
|
|
774
|
+
|
|
775
|
+
``github_token()`` below is the accessToken-only view of this. The daily
|
|
776
|
+
PR-ingest cron (``sdk.github_ingest``) needs ``username`` too, which is why
|
|
777
|
+
the full record is exposed. Best-effort — never raises.
|
|
773
778
|
"""
|
|
774
779
|
from sdk.config import BE_DEFAULT_URL
|
|
775
780
|
|
|
@@ -785,9 +790,72 @@ class BackendClient:
|
|
|
785
790
|
resp = await client.get(url, headers=headers)
|
|
786
791
|
if resp.status_code >= 400:
|
|
787
792
|
return None
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
793
|
+
return (resp.json() or {}).get("data") or None
|
|
794
|
+
except Exception: # noqa: BLE001
|
|
795
|
+
return None
|
|
796
|
+
|
|
797
|
+
async def github_token(self) -> str | None:
|
|
798
|
+
"""GET /ml-api/github/token → the user's GitHub access token (gho_…).
|
|
799
|
+
|
|
800
|
+
Used by the LOCAL agent to authenticate git/gh for clone/push/PR. The
|
|
801
|
+
endpoint lives on the ML backend (devopsbot-be), authorised by the same
|
|
802
|
+
co_ token. Best-effort: returns None on any failure so a run without a
|
|
803
|
+
connected GitHub account still proceeds (clone/PR steps just won't auth).
|
|
804
|
+
"""
|
|
805
|
+
record = await self.github_token_record()
|
|
806
|
+
return (record or {}).get("accessToken") or None
|
|
807
|
+
|
|
808
|
+
async def ingest_github_prs(
|
|
809
|
+
self,
|
|
810
|
+
*,
|
|
811
|
+
github_username: str,
|
|
812
|
+
github_access_token: str,
|
|
813
|
+
since: str | None = None,
|
|
814
|
+
user_id: str = "",
|
|
815
|
+
) -> dict | None:
|
|
816
|
+
"""POST /api/v1/github/prs — queue PR ingestion for THIS user on the backend.
|
|
817
|
+
|
|
818
|
+
This is the daily GitHub ingestion trigger. It runs on the AI backend
|
|
819
|
+
(``devopsbot-ai.apiswagger.co.uk`` — ``self._url``, the same base every other
|
|
820
|
+
``/api/v1`` call uses), NOT the ML gateway: the backend owns the LLM
|
|
821
|
+
summarise, the downstream daily-summary forward, and the Qdrant upsert.
|
|
822
|
+
The SDK only decides WHEN, because only this machine knows this user's
|
|
823
|
+
schedule and its own last-success watermark.
|
|
824
|
+
|
|
825
|
+
``since`` is an ISO date (``YYYY-MM-DD``) → the backend's
|
|
826
|
+
``GetUserPRsRequest.date_from``. Omitted means "today", which is what a
|
|
827
|
+
never-missed daily run wants; ``sdk.github_ingest`` sends the catch-up
|
|
828
|
+
watermark instead so a machine that was off does not silently lose days.
|
|
829
|
+
|
|
830
|
+
The credentials are sent in the body because that is what the endpoint
|
|
831
|
+
accepts today. The ``Authorization: co_`` header goes with every request
|
|
832
|
+
regardless, so once the backend resolves the GitHub link server-side from
|
|
833
|
+
that token (see docs/GITHUB_SCHEDULING_SDK_MIGRATION.md §3 step 1) it can
|
|
834
|
+
ignore the body fields without breaking this client.
|
|
835
|
+
|
|
836
|
+
Returns the response JSON (``{job_id, …}``) or None on any failure —
|
|
837
|
+
never raises into the cron.
|
|
838
|
+
"""
|
|
839
|
+
payload = [{
|
|
840
|
+
"userId": user_id or self._user_id_from_token(),
|
|
841
|
+
"githubUsername": github_username,
|
|
842
|
+
"githubAccessToken": github_access_token,
|
|
843
|
+
"apiTokenHash": self._token, # downstream daily-summary Authorization
|
|
844
|
+
}]
|
|
845
|
+
if since:
|
|
846
|
+
payload[0]["date_from"] = since
|
|
847
|
+
try:
|
|
848
|
+
async with httpx.AsyncClient(timeout=60.0) as client:
|
|
849
|
+
resp = await client.post(
|
|
850
|
+
self._url("/api/v1/github/prs"), json=payload, headers=self._headers,
|
|
851
|
+
)
|
|
852
|
+
if resp.status_code >= 400:
|
|
853
|
+
logger.warning("client.ingest_github_prs failed status=%s body=%s",
|
|
854
|
+
resp.status_code, resp.text[:200])
|
|
855
|
+
return None
|
|
856
|
+
return resp.json()
|
|
857
|
+
except Exception as exc: # noqa: BLE001
|
|
858
|
+
logger.warning("client.ingest_github_prs error err=%s", exc)
|
|
791
859
|
return None
|
|
792
860
|
|
|
793
861
|
async def move_to_backlog(self, task_id: str) -> bool:
|
|
@@ -1093,6 +1161,35 @@ class BackendClient:
|
|
|
1093
1161
|
_AGENT_ID_CACHE[nm] = "" # negative-cache: not a backend agent
|
|
1094
1162
|
return None
|
|
1095
1163
|
|
|
1164
|
+
async def report_vps_allocation(self, payload: dict) -> bool:
|
|
1165
|
+
"""PATCH ``{ML_GATEWAY_URL}/ml-api/contabo/allocation`` — this machine's health.
|
|
1166
|
+
|
|
1167
|
+
The allocation row is resolved SERVER-side from the ``co_`` token (one row
|
|
1168
|
+
per user — ``contabo_allocation``, docs/DATABASE_SCHEMA.md #18), so the body
|
|
1169
|
+
carries metrics only: ``status``, ``cpuUsage``, ``ramUsage``, ``usedRam``,
|
|
1170
|
+
``totalRam``, ``diskUsage``, ``totalDisk``, ``bandwidthUsed``, ``ipAddress``,
|
|
1171
|
+
``uptimeSeconds``, ``lastPingedAt``.
|
|
1172
|
+
|
|
1173
|
+
Driven by the SDK-side health cron (``sdk.vps_health``) — deliberately NOT by
|
|
1174
|
+
the AI backend, so VPS liveness reporting does not depend on that service.
|
|
1175
|
+
Best-effort: never raises; a failed ping is logged and the next tick retries.
|
|
1176
|
+
"""
|
|
1177
|
+
from sdk.config import ML_GATEWAY_URL
|
|
1178
|
+
|
|
1179
|
+
url = f"{ML_GATEWAY_URL.rstrip('/')}/ml-api/contabo/allocation"
|
|
1180
|
+
headers = {**self._headers, "accept": "*/*", "developer-name": "null", "product": ""}
|
|
1181
|
+
try:
|
|
1182
|
+
async with httpx.AsyncClient(timeout=20.0) as client:
|
|
1183
|
+
resp = await client.patch(url, json=payload, headers=headers)
|
|
1184
|
+
if resp.status_code >= 400:
|
|
1185
|
+
logger.warning("client.report_vps_allocation failed status=%s body=%s",
|
|
1186
|
+
resp.status_code, resp.text[:200])
|
|
1187
|
+
return False
|
|
1188
|
+
return True
|
|
1189
|
+
except Exception as exc: # noqa: BLE001
|
|
1190
|
+
logger.warning("client.report_vps_allocation error err=%s", exc)
|
|
1191
|
+
return False
|
|
1192
|
+
|
|
1096
1193
|
async def notify_whatsapp(
|
|
1097
1194
|
self,
|
|
1098
1195
|
task_id: str,
|