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>
79 lines
2.7 KiB
Python
79 lines
2.7 KiB
Python
"""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
|