Initial commit: Agentic OS troubleshooting platform
Self-hosted, Docker-based agentic troubleshooting platform: FastAPI backend + LangGraph agent, Next.js UI, tiered LLM routing (local Ollama -> Gemini -> DeepSeek -> OpenRouter), MCP server manager, encrypted device credentials, RBAC, audit log, project-memory + Obsidian integrations, and editable troubleshooting decision rules tuned for the GeneseasX vessel stack. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
78
backend/app/services/vessel_devices.py
Normal file
78
backend/app/services/vessel_devices.py
Normal file
@@ -0,0 +1,78 @@
|
||||
"""Helpers for building vessel device rows from catalog selections."""
|
||||
from __future__ import annotations
|
||||
|
||||
from app.core.crypto import decrypt_secret, encrypt_secret
|
||||
from app.agent.asterisk_profiles import ASTERISK_CATALOG_KEYS
|
||||
from app.inventory_catalog import CatalogEntry, catalog_by_key
|
||||
from app.models.inventory import Device, Vessel
|
||||
from app.schemas import DeviceOut, VesselDeviceSelection
|
||||
from app.services.device_defaults import merge_selection_with_defaults
|
||||
|
||||
|
||||
def device_to_out(d: Device) -> DeviceOut:
|
||||
out = DeviceOut.model_validate(d)
|
||||
out.has_secret = bool(d.secret_enc)
|
||||
return out
|
||||
|
||||
|
||||
def _resolve_address(vessel: Vessel, sel: VesselDeviceSelection) -> str:
|
||||
if sel.address:
|
||||
return sel.address
|
||||
if vessel.public_ip:
|
||||
return vessel.public_ip
|
||||
raise ValueError(f"Device '{sel.catalog_key}' needs an address or vessel public IP")
|
||||
|
||||
|
||||
def build_device(
|
||||
vessel: Vessel,
|
||||
sel: VesselDeviceSelection,
|
||||
entry: CatalogEntry,
|
||||
*,
|
||||
existing_secret_plain: str | None = None,
|
||||
) -> Device:
|
||||
port, username, secret = merge_selection_with_defaults(
|
||||
entry.key,
|
||||
entry,
|
||||
port=sel.port,
|
||||
username=sel.username,
|
||||
secret=sel.secret,
|
||||
existing_secret=existing_secret_plain,
|
||||
)
|
||||
secret_enc = encrypt_secret(secret) if secret else None
|
||||
return Device(
|
||||
vessel_id=vessel.id,
|
||||
catalog_key=entry.key,
|
||||
name=f"{vessel.name} — {entry.label}",
|
||||
device_type=entry.device_type,
|
||||
address=_resolve_address(vessel, sel),
|
||||
port=port,
|
||||
mcp_server=entry.mcp_server,
|
||||
username=username,
|
||||
secret_enc=secret_enc,
|
||||
notes=sel.notes,
|
||||
)
|
||||
|
||||
|
||||
def validate_selections(selections: list[VesselDeviceSelection]) -> list[tuple[VesselDeviceSelection, CatalogEntry]]:
|
||||
catalog = catalog_by_key()
|
||||
enabled = [s for s in selections if s.enabled]
|
||||
if not enabled:
|
||||
raise ValueError("Select at least one device for this vessel")
|
||||
out: list[tuple[VesselDeviceSelection, CatalogEntry]] = []
|
||||
seen: set[str] = set()
|
||||
asterisk_picked = False
|
||||
for sel in enabled:
|
||||
entry = catalog.get(sel.catalog_key)
|
||||
if not entry:
|
||||
raise ValueError(f"Unknown catalog key: {sel.catalog_key}")
|
||||
if sel.catalog_key in seen:
|
||||
raise ValueError(f"Duplicate device slot: {sel.catalog_key}")
|
||||
if sel.catalog_key in ASTERISK_CATALOG_KEYS:
|
||||
if asterisk_picked:
|
||||
raise ValueError(
|
||||
"Only one Asterisk slot per vessel — pick GeneseasX (docker voip) OR TMGeneseas/satbox (native)"
|
||||
)
|
||||
asterisk_picked = True
|
||||
seen.add(sel.catalog_key)
|
||||
out.append((sel, entry))
|
||||
return out
|
||||
Reference in New Issue
Block a user