seren-observatory 0.0.0.dev0__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.
- seren_observatory-0.0.0.dev0/PKG-INFO +31 -0
- seren_observatory-0.0.0.dev0/SerenObservatory.pyproj +123 -0
- seren_observatory-0.0.0.dev0/pyproject.toml +86 -0
- seren_observatory-0.0.0.dev0/seren-observatory.service.sample +33 -0
- seren_observatory-0.0.0.dev0/seren-observatory.yaml.sample +22 -0
- seren_observatory-0.0.0.dev0/seren_observatory/__init__.py +23 -0
- seren_observatory-0.0.0.dev0/seren_observatory/__main__.py +37 -0
- seren_observatory-0.0.0.dev0/seren_observatory/_version.py +24 -0
- seren_observatory-0.0.0.dev0/seren_observatory/app.py +172 -0
- seren_observatory-0.0.0.dev0/seren_observatory/auth.py +134 -0
- seren_observatory-0.0.0.dev0/seren_observatory/config.py +153 -0
- seren_observatory-0.0.0.dev0/seren_observatory/lifecycle.py +1072 -0
- seren_observatory-0.0.0.dev0/seren_observatory/manifests.py +154 -0
- seren_observatory-0.0.0.dev0/seren_observatory/request_log.py +137 -0
- seren_observatory-0.0.0.dev0/seren_observatory/requirements.txt +20 -0
- seren_observatory-0.0.0.dev0/seren_observatory/service_routes.py +124 -0
- seren_observatory-0.0.0.dev0/seren_observatory/services/__init__.py +50 -0
- seren_observatory-0.0.0.dev0/seren_observatory/services/comfy.py +110 -0
- seren_observatory-0.0.0.dev0/seren_observatory/services/coral.py +89 -0
- seren_observatory-0.0.0.dev0/seren_observatory/services/kokoro.py +60 -0
- seren_observatory-0.0.0.dev0/seren_observatory/services/llama.py +52 -0
- seren_observatory-0.0.0.dev0/seren_observatory/services/whisper.py +66 -0
- seren_observatory-0.0.0.dev0/seren_observatory/system_routes.py +496 -0
- seren_observatory-0.0.0.dev0/seren_observatory/viewer/ui/body.html +29 -0
- seren_observatory-0.0.0.dev0/seren_observatory/viewer/ui/header_aside.html +6 -0
- seren_observatory-0.0.0.dev0/seren_observatory/viewer/ui/scripts.js +230 -0
- seren_observatory-0.0.0.dev0/seren_observatory/viewer/ui/styles.css +227 -0
- seren_observatory-0.0.0.dev0/seren_observatory/viewer/ui/tabs.html +5 -0
- seren_observatory-0.0.0.dev0/seren_observatory.egg-info/PKG-INFO +31 -0
- seren_observatory-0.0.0.dev0/seren_observatory.egg-info/SOURCES.txt +39 -0
- seren_observatory-0.0.0.dev0/seren_observatory.egg-info/dependency_links.txt +1 -0
- seren_observatory-0.0.0.dev0/seren_observatory.egg-info/entry_points.txt +2 -0
- seren_observatory-0.0.0.dev0/seren_observatory.egg-info/requires.txt +11 -0
- seren_observatory-0.0.0.dev0/seren_observatory.egg-info/top_level.txt +1 -0
- seren_observatory-0.0.0.dev0/setup.cfg +4 -0
- seren_observatory-0.0.0.dev0/tests/__init__.py +0 -0
- seren_observatory-0.0.0.dev0/tests/conftest.py +75 -0
- seren_observatory-0.0.0.dev0/tests/test_app.py +72 -0
- seren_observatory-0.0.0.dev0/tests/test_auth.py +143 -0
- seren_observatory-0.0.0.dev0/tests/test_config.py +110 -0
- seren_observatory-0.0.0.dev0/tests/test_manifests.py +106 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: seren-observatory
|
|
3
|
+
Version: 0.0.0.dev0
|
|
4
|
+
Summary: Per-node management plane for the Seren cluster - manifest-driven service lifecycle, status, and orchestration.
|
|
5
|
+
License: GPL-3.0-or-later
|
|
6
|
+
Project-URL: Homepage, https://github.com/ChadRoesler/SerenObservatory
|
|
7
|
+
Project-URL: Repository, https://github.com/ChadRoesler/SerenObservatory
|
|
8
|
+
Project-URL: Bug Tracker, https://github.com/ChadRoesler/SerenObservatory/issues
|
|
9
|
+
Keywords: seren,jetson,observatory,fastapi,llm
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Environment :: No Input/Output (Daemon)
|
|
12
|
+
Classifier: Framework :: FastAPI
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: System :: Systems Administration
|
|
20
|
+
Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Requires-Dist: fastapi>=0.115.0
|
|
23
|
+
Requires-Dist: uvicorn[standard]>=0.32.0
|
|
24
|
+
Requires-Dist: pydantic>=2.9.0
|
|
25
|
+
Requires-Dist: httpx>=0.27.0
|
|
26
|
+
Requires-Dist: python-multipart>=0.0.12
|
|
27
|
+
Requires-Dist: seren-meninges<2,>=1.0.0
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
30
|
+
Requires-Dist: pytest-asyncio>=0.24; extra == "dev"
|
|
31
|
+
Requires-Dist: httpx2>=0.28; extra == "dev"
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="4.0">
|
|
2
|
+
<PropertyGroup>
|
|
3
|
+
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">10.0</VisualStudioVersion>
|
|
4
|
+
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
|
5
|
+
<SchemaVersion>2.0</SchemaVersion>
|
|
6
|
+
<ProjectGuid>{e20990c9-b0f0-46ff-9f69-6537d00b7fbf}</ProjectGuid>
|
|
7
|
+
<ProjectHome>.</ProjectHome>
|
|
8
|
+
<ProjectTypeGuids>{1b580a1a-fdb3-4b32-83e1-6407eb2722e6};{888888a0-9f3d-457c-b088-3a5042f75d52}</ProjectTypeGuids>
|
|
9
|
+
<StartupFile>seren_observatory/app.py</StartupFile>
|
|
10
|
+
<WorkingDirectory>.</WorkingDirectory>
|
|
11
|
+
<OutputPath>dist\</OutputPath>
|
|
12
|
+
<Name>SerenObservatory</Name>
|
|
13
|
+
</PropertyGroup>
|
|
14
|
+
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
|
15
|
+
<DebugSymbols>true</DebugSymbols>
|
|
16
|
+
</PropertyGroup>
|
|
17
|
+
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
|
|
18
|
+
<DebugSymbols>true</DebugSymbols>
|
|
19
|
+
</PropertyGroup>
|
|
20
|
+
<ItemGroup>
|
|
21
|
+
<Compile Include="seren_observatory\config.py" />
|
|
22
|
+
<Compile Include="seren_observatory\requirements.txt" />
|
|
23
|
+
<Compile Include="seren_observatory\app.py" />
|
|
24
|
+
<Compile Include="seren_observatory\auth.py" />
|
|
25
|
+
<Compile Include="seren_observatory\lifecycle.py" />
|
|
26
|
+
<Compile Include="seren_observatory\manifests.py" />
|
|
27
|
+
<Compile Include="seren_observatory\request_log.py" />
|
|
28
|
+
<Compile Include="seren_observatory\service_routes.py" />
|
|
29
|
+
<Compile Include="seren_observatory\system_routes.py" />
|
|
30
|
+
<Compile Include="seren_observatory\_version.py" />
|
|
31
|
+
<Compile Include="seren_observatory\__init__.py" />
|
|
32
|
+
<Compile Include="seren_observatory\services\comfy.py" />
|
|
33
|
+
<Compile Include="seren_observatory\services\coral.py" />
|
|
34
|
+
<Compile Include="seren_observatory\services\kokoro.py" />
|
|
35
|
+
<Compile Include="seren_observatory\services\llama.py" />
|
|
36
|
+
<Compile Include="seren_observatory\services\whisper.py" />
|
|
37
|
+
<Compile Include="seren_observatory\services\__init__.py" />
|
|
38
|
+
<Compile Include="seren_observatory\__main__.py" />
|
|
39
|
+
<Compile Include="tests\conftest.py" />
|
|
40
|
+
<Compile Include="tests\test_app.py" />
|
|
41
|
+
<Compile Include="tests\test_auth.py" />
|
|
42
|
+
<Compile Include="tests\test_config.py" />
|
|
43
|
+
<Compile Include="tests\test_manifests.py" />
|
|
44
|
+
<Compile Include="tests\__init__.py" />
|
|
45
|
+
</ItemGroup>
|
|
46
|
+
<ItemGroup>
|
|
47
|
+
<None Include="seren_observatory\requirements.txt" />
|
|
48
|
+
<None Include="pyproject.toml" />
|
|
49
|
+
<None Include="README.md" Condition="Exists('README.md')" />
|
|
50
|
+
</ItemGroup>
|
|
51
|
+
<ItemGroup>
|
|
52
|
+
<Folder Include="seren_observatory\" />
|
|
53
|
+
<Folder Include="seren_observatory\services\" />
|
|
54
|
+
<Folder Include="seren_observatory\services\__pycache__\" />
|
|
55
|
+
<Folder Include="seren_observatory\viewer\" />
|
|
56
|
+
<Folder Include="seren_observatory\viewer\ui\" />
|
|
57
|
+
<Folder Include="tests\" />
|
|
58
|
+
</ItemGroup>
|
|
59
|
+
<ItemGroup>
|
|
60
|
+
<Content Include="seren-observatory.service.sample" />
|
|
61
|
+
<Content Include="seren-observatory.yaml.sample" />
|
|
62
|
+
<Content Include="seren_observatory\services\__pycache__\comfy.cpython-311.pyc" />
|
|
63
|
+
<Content Include="seren_observatory\services\__pycache__\coral.cpython-311.pyc" />
|
|
64
|
+
<Content Include="seren_observatory\services\__pycache__\kokoro.cpython-311.pyc" />
|
|
65
|
+
<Content Include="seren_observatory\services\__pycache__\llama.cpython-311.pyc" />
|
|
66
|
+
<Content Include="seren_observatory\services\__pycache__\whisper.cpython-311.pyc" />
|
|
67
|
+
<Content Include="seren_observatory\services\__pycache__\__init__.cpython-311.pyc" />
|
|
68
|
+
<Content Include="seren_observatory\viewer\ui\body.html" />
|
|
69
|
+
<Content Include="seren_observatory\viewer\ui\header_aside.html" />
|
|
70
|
+
<Content Include="seren_observatory\viewer\ui\scripts.js" />
|
|
71
|
+
<Content Include="seren_observatory\viewer\ui\styles.css" />
|
|
72
|
+
<Content Include="seren_observatory\viewer\ui\tabs.html" />
|
|
73
|
+
</ItemGroup>
|
|
74
|
+
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\Python Tools\Microsoft.PythonTools.targets" />
|
|
75
|
+
<!--
|
|
76
|
+
Publish target: runs ONLY when you explicitly invoke it.
|
|
77
|
+
VS: right-click project -> Publish (or build configuration set
|
|
78
|
+
to invoke the Publish target via msbuild /t:Publish)
|
|
79
|
+
Command: msbuild SerenObservatory.pyproj /t:Publish /p:Configuration=Release
|
|
80
|
+
|
|
81
|
+
What it does:
|
|
82
|
+
1. Cleans __pycache__ from observatory/ so they don't end up in the tarball
|
|
83
|
+
2. Runs `tar` (Windows 10+ ships bsdtar at C:\Windows\System32\tar.exe)
|
|
84
|
+
to create $(PublishTarball) with `observatory/` at the top level — no
|
|
85
|
+
wrapping directory, no metadata files. This is exactly the layout
|
|
86
|
+
install_observatory_common in seren-v5/common.sh expects.
|
|
87
|
+
3. Verifies the tarball contains observatory/__init__.py at the top level
|
|
88
|
+
so a half-built artifact doesn't sneak through.
|
|
89
|
+
|
|
90
|
+
Why not AfterBuild: that fires on every build (including F5 to debug),
|
|
91
|
+
which would slow your edit-test loop and produce stale tarballs in
|
|
92
|
+
half-finished states. Publish is a deliberate "I'm ready to ship" step.
|
|
93
|
+
-->
|
|
94
|
+
<Target Name="Publish">
|
|
95
|
+
<Message Text="==> Cleaning __pycache__ from seren_observatory/" Importance="high" />
|
|
96
|
+
<ItemGroup>
|
|
97
|
+
<PycacheDirs Include="seren_observatory\**\__pycache__\**" />
|
|
98
|
+
</ItemGroup>
|
|
99
|
+
<Delete Files="@(PycacheDirs)" />
|
|
100
|
+
<RemoveDir Directories="observatory\__pycache__;observatory\services\__pycache__" />
|
|
101
|
+
<Message Text="==> Ensuring publish dir exists: $(SerenSetupDir)" Importance="high" />
|
|
102
|
+
<MakeDir Directories="$(SerenSetupDir)" Condition="!Exists('$(SerenSetupDir)')" />
|
|
103
|
+
<Message Text="==> Building $(PublishTarball)" Importance="high" />
|
|
104
|
+
<!--
|
|
105
|
+
tar.exe (bsdtar on Win10+) syntax:
|
|
106
|
+
-c create
|
|
107
|
+
-z gzip
|
|
108
|
+
-f output file
|
|
109
|
+
We tar from $(ProjectHome) so the path inside the archive is just
|
|
110
|
+
`seren_observatory/...` with no leading directory.
|
|
111
|
+
-->
|
|
112
|
+
<Exec Command="tar -czf "$(PublishTarball)" seren-observatory" WorkingDirectory="$(ProjectHome)" />
|
|
113
|
+
<Message Text="==> Verifying tarball layout" Importance="high" />
|
|
114
|
+
<!--
|
|
115
|
+
Verify seren_observatory/__init__.py is present (correct package layout).
|
|
116
|
+
Verify seren_observatory/requirements.txt is present so nodes always have it
|
|
117
|
+
available for pip install -r after extract.
|
|
118
|
+
-->
|
|
119
|
+
<Exec Command="tar -tzf "$(PublishTarball)" | findstr /B /L "seren_observatory/__init__.py" > nul" WorkingDirectory="$(ProjectHome)" />
|
|
120
|
+
<Exec Command="tar -tzf "$(PublishTarball)" | findstr /B /L "seren_observatory/requirements.txt" > nul" WorkingDirectory="$(ProjectHome)" />
|
|
121
|
+
<Message Text="==> Published: $(PublishTarball)" Importance="high" />
|
|
122
|
+
</Target>
|
|
123
|
+
</Project>
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=70", "setuptools-scm>=8"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
# ── Project metadata ──────────────────────────────────────────────────────
|
|
6
|
+
[project]
|
|
7
|
+
name = "seren-observatory"
|
|
8
|
+
dynamic = ["version"]
|
|
9
|
+
description = "Per-node management plane for the Seren cluster - manifest-driven service lifecycle, status, and orchestration."
|
|
10
|
+
license = { text = "GPL-3.0-or-later" }
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
keywords = ["seren", "jetson", "observatory", "fastapi", "llm"]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 4 - Beta",
|
|
15
|
+
"Environment :: No Input/Output (Daemon)",
|
|
16
|
+
"Framework :: FastAPI",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"Operating System :: POSIX :: Linux",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3.10",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Topic :: System :: Systems Administration",
|
|
24
|
+
"License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
dependencies = [
|
|
28
|
+
"fastapi>=0.115.0",
|
|
29
|
+
"uvicorn[standard]>=0.32.0",
|
|
30
|
+
"pydantic>=2.9.0",
|
|
31
|
+
"httpx>=0.27.0",
|
|
32
|
+
"python-multipart>=0.0.12",
|
|
33
|
+
# Shared Seren core: get_version + the viewer baseplate (render_from_dir).
|
|
34
|
+
# NOTE: config + auth deliberately do NOT come from Meninges here - the
|
|
35
|
+
# Observatory's bearer is a ~/.seren/secrets.json safety interlock, not a
|
|
36
|
+
# Meninges-resolved config token. No [keyring] extra: this service never
|
|
37
|
+
# writes secrets to the OS keychain.
|
|
38
|
+
"seren-meninges>=1.0.0,<2",
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
[project.optional-dependencies]
|
|
42
|
+
dev = [
|
|
43
|
+
"pytest>=8.0",
|
|
44
|
+
"pytest-asyncio>=0.24",
|
|
45
|
+
"httpx2>=0.28", # Starlette TestClient's successor HTTP backend (Tom Christie / pydantic); silences the httpx-deprecation warning
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
[project.scripts]
|
|
49
|
+
seren-observatory = "seren_observatory.app:main"
|
|
50
|
+
|
|
51
|
+
[project.urls]
|
|
52
|
+
Homepage = "https://github.com/ChadRoesler/SerenObservatory"
|
|
53
|
+
Repository = "https://github.com/ChadRoesler/SerenObservatory"
|
|
54
|
+
"Bug Tracker" = "https://github.com/ChadRoesler/SerenObservatory/issues"
|
|
55
|
+
|
|
56
|
+
# ── setuptools-scm: version from git tags ────────────────────────────────
|
|
57
|
+
# Tags must be the form v1.2.3 on the repo root.
|
|
58
|
+
# `python -m build` with no tags → 0.1.dev0+g<hash>.
|
|
59
|
+
# SETUPTOOLS_SCM_PRETEND_VERSION overrides for local dev without tags.
|
|
60
|
+
[tool.setuptools_scm]
|
|
61
|
+
# Write version into the installed package so importlib.metadata works.
|
|
62
|
+
# The file is gitignored - never commit it.
|
|
63
|
+
version_file = "seren_observatory/_version.py"
|
|
64
|
+
# Strip the leading 'v' so "v1.2.3" → "1.2.3"
|
|
65
|
+
tag_regex = "^v(?P<version>[0-9]+\\.[0-9]+\\.[0-9]+.*)$"
|
|
66
|
+
# Used when no git tags exist (local dev, fresh clone, make test).
|
|
67
|
+
# SETUPTOOLS_SCM_PRETEND_VERSION overrides this when set (CI release builds).
|
|
68
|
+
fallback_version = "0.0.0.dev0"
|
|
69
|
+
|
|
70
|
+
# ── Package discovery ────────────────────────────────────────────────────
|
|
71
|
+
[tool.setuptools.packages.find]
|
|
72
|
+
where = ["."]
|
|
73
|
+
include = ["seren_observatory*"]
|
|
74
|
+
|
|
75
|
+
# Ship the viewer fragment bricks inside the wheel: the leaf files in
|
|
76
|
+
# viewer/ui/ that render_from_dir snaps onto the SerenMeninges baseplate.
|
|
77
|
+
# Without this, pip strips non-.py files and GET /viewer renders an empty
|
|
78
|
+
# shell on an installed copy (works in a dev checkout, silently breaks once
|
|
79
|
+
# installed - the classic package-data gotcha).
|
|
80
|
+
[tool.setuptools.package-data]
|
|
81
|
+
seren_observatory = ["viewer/ui/*.html", "viewer/ui/*.css", "viewer/ui/*.js"]
|
|
82
|
+
|
|
83
|
+
# ── pytest ───────────────────────────────────────────────────────────────
|
|
84
|
+
[tool.pytest.ini_options]
|
|
85
|
+
testpaths = ["tests"]
|
|
86
|
+
asyncio_mode = "auto"
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# SerenMemory systemd unit (sample)
|
|
2
|
+
#
|
|
3
|
+
# Install:
|
|
4
|
+
# 1. Edit __TARGET_USER__ and paths below
|
|
5
|
+
# 2. sudo cp seren-observatory.service /etc/systemd/system/
|
|
6
|
+
# 3. sudo systemctl daemon-reload
|
|
7
|
+
# 4. sudo systemctl enable --now seren-observatory
|
|
8
|
+
#
|
|
9
|
+
# Logs: journalctl -u seren-observatory -f
|
|
10
|
+
|
|
11
|
+
[Unit]
|
|
12
|
+
Description=SerenObservatory - A monitoring observatory for LLMs and tools
|
|
13
|
+
After=network-online.target
|
|
14
|
+
Wants=network-online.target
|
|
15
|
+
|
|
16
|
+
[Service]
|
|
17
|
+
Type=simple
|
|
18
|
+
User=__TARGET_USER__
|
|
19
|
+
WorkingDirectory=/home/__TARGET_USER__/seren-observatory
|
|
20
|
+
|
|
21
|
+
# Use the venv's python. Adjust the venv path to wherever you installed.
|
|
22
|
+
ExecStart=/home/__TARGET_USER__/seren-venvs/observatory/bin/python -m seren_observatory --config /home/__TARGET_USER__/seren-observatory/seren-observatory.yaml
|
|
23
|
+
|
|
24
|
+
# Config can also come from env - uncomment + set as needed:
|
|
25
|
+
# Environment=SEREN_AGENT_PORT=7777
|
|
26
|
+
# Environment=SEREN_AGENT_MODEL_URL=http://localhost:8090/v1
|
|
27
|
+
# Environment=SEREN_AGENT_BEARER_TOKEN=
|
|
28
|
+
|
|
29
|
+
Restart=on-failure
|
|
30
|
+
RestartSec=5
|
|
31
|
+
|
|
32
|
+
[Install]
|
|
33
|
+
WantedBy=multi-user.target
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
2
|
+
# seren-observatory.yaml - config for SerenObservatory
|
|
3
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
4
|
+
#
|
|
5
|
+
# Copy to seren-observatory.yaml, edit, and run:
|
|
6
|
+
# python -m seren_observatory --config seren-observatory.yaml
|
|
7
|
+
#
|
|
8
|
+
# Or just run with no config at all - the defaults below are baked in, so
|
|
9
|
+
# `python -m seren_observatory` works zero-config for a local dev spin.
|
|
10
|
+
#
|
|
11
|
+
# Env vars override file values (handy for Docker/systemd):
|
|
12
|
+
# SEREN_AGENT_PORT, SEREN_AGENT_HOST, SEREN_AGENT_BEARER_TOKEN
|
|
13
|
+
#
|
|
14
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
15
|
+
|
|
16
|
+
server:
|
|
17
|
+
host: 0.0.0.0
|
|
18
|
+
port: 7777
|
|
19
|
+
# Leave empty for no auth (trusted LAN / dev). Set a token to require
|
|
20
|
+
# Authorization: Bearer <token>
|
|
21
|
+
# on every endpoint except / and /health.
|
|
22
|
+
bearer_token: ""
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""
|
|
2
|
+
seren-observatory - per-Jetson management plane.
|
|
3
|
+
|
|
4
|
+
HTTP API exposing manifest-driven discovery and lifecycle of locally-installed
|
|
5
|
+
Seren services (llama, kokoro, comfy, whisper, coral). Consumed by:
|
|
6
|
+
- SerenRuntimeHost (C#) for chat-app workflows
|
|
7
|
+
- SerenCommandCenter (SCC, future) for cluster orchestration
|
|
8
|
+
- The NUC dashboard for monitoring
|
|
9
|
+
|
|
10
|
+
Routes are versioned under /api/v1/. See service_routes.py and system_routes.py.
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
15
|
+
|
|
16
|
+
try:
|
|
17
|
+
__version__: str = version("seren-observatory")
|
|
18
|
+
except PackageNotFoundError:
|
|
19
|
+
# Running from a source checkout without an editable install.
|
|
20
|
+
# Set SETUPTOOLS_SCM_PRETEND_VERSION=0.0.0 and run:
|
|
21
|
+
# pip install -e ".[dev]"
|
|
22
|
+
# to resolve this.
|
|
23
|
+
__version__ = "0.0.0.dev"
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Entry point for `python -m seren_observatory`.
|
|
2
|
+
|
|
3
|
+
Accepts --config / -c to match the SerenMemory convention (Memory leads, the
|
|
4
|
+
rest follow). Host/port come from the resolved config (which itself layers
|
|
5
|
+
defaults < yaml < env). The bearer token is NOT a config concern - it's loaded
|
|
6
|
+
separately from ~/.seren/secrets.json by auth.load_token(). See config.py.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
|
|
12
|
+
import uvicorn
|
|
13
|
+
|
|
14
|
+
from .app import create_app
|
|
15
|
+
from .config import load_config
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def main() -> None:
|
|
19
|
+
parser = argparse.ArgumentParser(
|
|
20
|
+
prog="seren_observatory",
|
|
21
|
+
description="seren-observatory - per-node management plane.")
|
|
22
|
+
parser.add_argument(
|
|
23
|
+
"--config", "-c", default=None,
|
|
24
|
+
help="Path to seren-observatory.yaml (default: $SEREN_AGENT_CONFIG, then "
|
|
25
|
+
"~/seren-observatory/seren-observatory.yaml, falling back to built-in "
|
|
26
|
+
"defaults of 0.0.0.0:7777).")
|
|
27
|
+
args = parser.parse_args()
|
|
28
|
+
|
|
29
|
+
cfg = load_config(args.config)
|
|
30
|
+
app = create_app(cfg)
|
|
31
|
+
|
|
32
|
+
print(f"[seren-observatory] listening on {cfg.host}:{cfg.port}")
|
|
33
|
+
uvicorn.run(app, host=cfg.host, port=cfg.port, log_level="info")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
if __name__ == "__main__":
|
|
37
|
+
main()
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# file generated by vcs-versioning
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"__version__",
|
|
7
|
+
"__version_tuple__",
|
|
8
|
+
"version",
|
|
9
|
+
"version_tuple",
|
|
10
|
+
"__commit_id__",
|
|
11
|
+
"commit_id",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
version: str
|
|
15
|
+
__version__: str
|
|
16
|
+
__version_tuple__: tuple[int | str, ...]
|
|
17
|
+
version_tuple: tuple[int | str, ...]
|
|
18
|
+
commit_id: str | None
|
|
19
|
+
__commit_id__: str | None
|
|
20
|
+
|
|
21
|
+
__version__ = version = '0.0.0.dev0'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 0, 0, 'dev0')
|
|
23
|
+
|
|
24
|
+
__commit_id__ = commit_id = None
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""
|
|
2
|
+
seren-observatory - main FastAPI app.
|
|
3
|
+
|
|
4
|
+
Run with:
|
|
5
|
+
seren-observatory # via the installed console script
|
|
6
|
+
python -m seren_observatory # config-aware entry (--config/-c)
|
|
7
|
+
python -m seren_observatory.app # directly from the source tree
|
|
8
|
+
uvicorn seren_observatory.app:app # ASGI server pointing at module app
|
|
9
|
+
|
|
10
|
+
or via systemd / a launcher. Listens on 0.0.0.0:7777 by default.
|
|
11
|
+
|
|
12
|
+
Config: host/port resolve via config.load_config() (defaults < yaml's server:
|
|
13
|
+
block < env vars). The bearer token is loaded SEPARATELY from
|
|
14
|
+
~/.seren/secrets.json by auth.load_token() - it's a safety interlock, not a
|
|
15
|
+
config field. See config.py for why.
|
|
16
|
+
"""
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import os
|
|
20
|
+
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
from fastapi import FastAPI
|
|
24
|
+
from fastapi.responses import HTMLResponse
|
|
25
|
+
|
|
26
|
+
from . import __version__, manifests
|
|
27
|
+
from .auth import BearerAuthMiddleware, load_token
|
|
28
|
+
from .config import ObservatoryConfig, load_config
|
|
29
|
+
from .request_log import RequestLoggingMiddleware
|
|
30
|
+
from .service_routes import register_all_services
|
|
31
|
+
from .system_routes import router as system_router
|
|
32
|
+
|
|
33
|
+
from seren_meninges import get_version
|
|
34
|
+
from seren_meninges.viewer import render_from_dir
|
|
35
|
+
|
|
36
|
+
# Version via the shared SerenMeninges helper: the installed wheel's
|
|
37
|
+
# setuptools-scm metadata, falling back to the package __version__ for an
|
|
38
|
+
# editable/dev checkout where dist metadata may be absent. get_version never
|
|
39
|
+
# raises - the same one-liner the rest of the family uses, replacing the old
|
|
40
|
+
# hand-rolled importlib.metadata block that drifted from the others.
|
|
41
|
+
APP_VERSION = get_version("seren-observatory", fallback=__version__)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def create_app(cfg: ObservatoryConfig | None = None) -> FastAPI:
|
|
45
|
+
# cfg is accepted so the config-aware entry points (python -m seren_observatory)
|
|
46
|
+
# can pass a resolved config. When called with no arg (e.g. the
|
|
47
|
+
# module-level `app` below, or `uvicorn seren_observatory.app:app`), fall back to
|
|
48
|
+
# load_config() so behaviour is identical either way. cfg currently carries
|
|
49
|
+
# host/port; those are consumed by the caller that runs uvicorn, so the app
|
|
50
|
+
# body doesn't need them - but we resolve it anyway so a future need (e.g.
|
|
51
|
+
# surfacing the bind in the root page) has it on hand without another load.
|
|
52
|
+
cfg = cfg or load_config()
|
|
53
|
+
|
|
54
|
+
app = FastAPI(
|
|
55
|
+
title="seren-observatory",
|
|
56
|
+
version=APP_VERSION,
|
|
57
|
+
description="Per-Jetson management plane. Manifest-driven service "
|
|
58
|
+
"lifecycle, status, and orchestration. Bearer token auth "
|
|
59
|
+
"on everything except /api/v1/system/{ping,version}.",
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
# Request logging - wraps every request, captures timing + status +
|
|
63
|
+
# 500 tracebacks. Logs go to BOTH stderr (journalctl) AND a rotating
|
|
64
|
+
# file at ~/seren-logs/observatory-requests.log (no sudo needed to read).
|
|
65
|
+
#
|
|
66
|
+
# MUST be added BEFORE auth so we log auth-rejected requests too -
|
|
67
|
+
# that's actually one of the most useful debug signals ("dashboard
|
|
68
|
+
# is failing - is the bearer token wrong, or is the route 500ing?")
|
|
69
|
+
#
|
|
70
|
+
# Starlette/FastAPI middleware order: LIFO at request time. So adding
|
|
71
|
+
# RequestLoggingMiddleware FIRST means it runs LAST on the way in
|
|
72
|
+
# (closest to the route), and FIRST on the way out - wrapping the
|
|
73
|
+
# entire chain. Adding auth SECOND means auth runs BEFORE logging on
|
|
74
|
+
# request, which is wrong. We want logging OUTERMOST.
|
|
75
|
+
#
|
|
76
|
+
# Correct stack (request flow top→bottom):
|
|
77
|
+
# RequestLoggingMiddleware (logs everything, including 401s)
|
|
78
|
+
# BearerAuthMiddleware (rejects unauthed, logging sees the rejection)
|
|
79
|
+
# <route handler>
|
|
80
|
+
#
|
|
81
|
+
# FastAPI add_middleware adds in REVERSE order at runtime, so we add
|
|
82
|
+
# auth FIRST (will run inner) and logging SECOND (will run outer).
|
|
83
|
+
token = load_token()
|
|
84
|
+
app.add_middleware(BearerAuthMiddleware, expected_token=token)
|
|
85
|
+
app.add_middleware(RequestLoggingMiddleware)
|
|
86
|
+
|
|
87
|
+
# Root info page - no service data, just links + auth status indicator
|
|
88
|
+
@app.get("/", response_class=HTMLResponse)
|
|
89
|
+
async def root() -> str:
|
|
90
|
+
node = manifests.load_node()
|
|
91
|
+
host = (node or {}).get("hostname", "unknown")
|
|
92
|
+
auth_state = "configured" if token else "DISABLED (no token in ~/.seren/secrets.json)"
|
|
93
|
+
return f"""<!doctype html>
|
|
94
|
+
<html><head><title>seren-observatory - {host}</title></head>
|
|
95
|
+
<body style="font-family: system-ui; max-width: 720px; margin: 2rem auto; padding: 0 1rem;">
|
|
96
|
+
<h1>seren-observatory</h1>
|
|
97
|
+
<p>Per-Jetson management plane for the Seren cluster.</p>
|
|
98
|
+
<dl>
|
|
99
|
+
<dt>Hostname:</dt> <dd>{host}</dd>
|
|
100
|
+
<dt>Observatory version:</dt> <dd>{APP_VERSION}</dd>
|
|
101
|
+
<dt>Auth:</dt> <dd>{auth_state}</dd>
|
|
102
|
+
</dl>
|
|
103
|
+
<h2>Endpoints</h2>
|
|
104
|
+
<ul>
|
|
105
|
+
<li><a href="/docs">/docs</a> - interactive API docs (Swagger)</li>
|
|
106
|
+
<li><a href="/api/v1/system/ping">/api/v1/system/ping</a> - public liveness</li>
|
|
107
|
+
<li><a href="/api/v1/system/version">/api/v1/system/version</a> - public version</li>
|
|
108
|
+
<li>/api/v1/system/{{node, services, health, reclaim}} - auth required</li>
|
|
109
|
+
<li>/api/v1/service/{{name}}/{{start, stop, restart, health, status, logs, manifest}} - auth required</li>
|
|
110
|
+
</ul>
|
|
111
|
+
<p>Source of truth: ~/.seren/services/*.json + ~/.seren/node.json</p>
|
|
112
|
+
</body></html>"""
|
|
113
|
+
|
|
114
|
+
# The Observatory glance - on the shared SerenMeninges baseplate.
|
|
115
|
+
@app.get("/viewer", response_class=HTMLResponse)
|
|
116
|
+
async def viewer() -> str:
|
|
117
|
+
# Node vitals, thermals, health rollup, and the service roster with
|
|
118
|
+
# lifecycle controls. PUBLIC route (the HTML shell needs no auth - see
|
|
119
|
+
# auth.PUBLIC_PATHS); its /api/v1/* fetches carry the token from the
|
|
120
|
+
# shell's key modal. With no token provisioned the read-only glance
|
|
121
|
+
# still works (safe GETs stay open); the action buttons fail closed
|
|
122
|
+
# (503) until ~/.seren/secrets.json exists - the deliberate interlock,
|
|
123
|
+
# surfaced in the UI instead of hidden.
|
|
124
|
+
return render_from_dir(
|
|
125
|
+
Path(__file__).resolve().parent / "viewer" / "ui",
|
|
126
|
+
title="seren-observatory",
|
|
127
|
+
brand="Seren<b>Observatory</b>",
|
|
128
|
+
subtitle=f"v{APP_VERSION} · per-node watch plane",
|
|
129
|
+
accent="#6eff70",
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
# System routes (ping, version, node, services, health, reclaim)
|
|
133
|
+
app.include_router(system_router)
|
|
134
|
+
|
|
135
|
+
# Per-service routes - one router per installed service
|
|
136
|
+
mounted = register_all_services(app)
|
|
137
|
+
print(f"[seren-observatory] mounted services: {mounted}")
|
|
138
|
+
|
|
139
|
+
return app
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
# Module-level app for `uvicorn seren_observatory.app:app`
|
|
143
|
+
app = create_app()
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
if __name__ == "__main__":
|
|
147
|
+
import uvicorn
|
|
148
|
+
|
|
149
|
+
# Direct `python -m seren_observatory.app` path. Resolve config the same way the
|
|
150
|
+
# console script does so host/port behave identically. (`python -m
|
|
151
|
+
# seren_observatory` -> __main__.py is the preferred, --config-aware entry.)
|
|
152
|
+
_cfg = load_config()
|
|
153
|
+
uvicorn.run(app, host=_cfg.host, port=_cfg.port, log_level="info")
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def main() -> None:
|
|
157
|
+
"""Console-script entry point: `seren-observatory` (declared in pyproject.toml).
|
|
158
|
+
|
|
159
|
+
Resolves config (defaults < yaml < env) and runs uvicorn. The CLI --config
|
|
160
|
+
flag is handled by __main__.py (`python -m seren_observatory`); the bare
|
|
161
|
+
console-script reads $SEREN_AGENT_CONFIG / the conventional path / defaults.
|
|
162
|
+
"""
|
|
163
|
+
import uvicorn
|
|
164
|
+
|
|
165
|
+
cfg = load_config()
|
|
166
|
+
uvicorn.run(
|
|
167
|
+
"seren_observatory.app:app",
|
|
168
|
+
host=cfg.host,
|
|
169
|
+
port=cfg.port,
|
|
170
|
+
log_level="info",
|
|
171
|
+
reload=False,
|
|
172
|
+
)
|