Refine lab status and AI defaults
This commit is contained in:
parent
8703e6933a
commit
824a2187ea
@ -22,18 +22,18 @@ def register(app) -> None:
|
||||
|
||||
payload = request.get_json(silent=True) or {}
|
||||
user_message = (payload.get("message") or "").strip()
|
||||
profile = (payload.get("profile") or payload.get("mode") or "atlas-quick").strip().lower()
|
||||
profile = (payload.get("profile") or payload.get("mode") or "atlas-smart").strip().lower()
|
||||
conversation_id = payload.get("conversation_id") if isinstance(payload.get("conversation_id"), str) else ""
|
||||
|
||||
if not user_message:
|
||||
return jsonify({"error": "message required"}), 400
|
||||
|
||||
started = time.time()
|
||||
mode = "quick"
|
||||
if profile in {"atlas-smart", "smart"}:
|
||||
mode = "smart"
|
||||
elif profile in {"atlas-genius", "genius"}:
|
||||
mode = "smart"
|
||||
if profile in {"atlas-genius", "genius"}:
|
||||
mode = "genius"
|
||||
elif profile in {"atlas-quick", "quick"}:
|
||||
mode = "quick"
|
||||
reply = _atlasbot_answer(user_message, mode, conversation_id)
|
||||
source = f"atlas-{mode}"
|
||||
if reply:
|
||||
@ -67,7 +67,7 @@ def register(app) -> None:
|
||||
def ai_info() -> Any:
|
||||
"""Return model and placement metadata for the requested AI profile."""
|
||||
|
||||
profile = (request.args.get("profile") or "atlas-quick").strip().lower()
|
||||
profile = (request.args.get("profile") or "atlas-smart").strip().lower()
|
||||
meta = _discover_ai_meta(profile)
|
||||
return jsonify(meta)
|
||||
|
||||
|
||||
@ -45,12 +45,28 @@ class AiRouteTests(TestCase):
|
||||
self.assertEqual(data.get("source"), f"atlas-{expected_mode}")
|
||||
self.assertEqual(data.get("reply"), f"{expected_mode}:conv-{profile}")
|
||||
|
||||
resp = self.client.post(
|
||||
"/api/chat",
|
||||
data=json.dumps(
|
||||
{
|
||||
"message": "How is Titan doing?",
|
||||
"conversation_id": "conv-default",
|
||||
}
|
||||
),
|
||||
content_type="application/json",
|
||||
)
|
||||
data = resp.get_json()
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertEqual(data.get("source"), "atlas-smart")
|
||||
self.assertEqual(data.get("reply"), "smart:conv-default")
|
||||
|
||||
self.assertEqual(
|
||||
seen,
|
||||
[
|
||||
("quick", "conv-atlas-quick"),
|
||||
("smart", "conv-atlas-smart"),
|
||||
("genius", "conv-atlas-genius"),
|
||||
("smart", "conv-default"),
|
||||
],
|
||||
)
|
||||
|
||||
@ -63,6 +79,14 @@ class AiRouteTests(TestCase):
|
||||
self.assertEqual(data.get("profile"), "atlas-genius")
|
||||
self.assertEqual(data.get("model"), "genius-model")
|
||||
|
||||
with mock.patch.object(ai.settings, "AI_ATLASBOT_MODEL_SMART", "smart-model"):
|
||||
resp = self.client.get("/api/ai/info")
|
||||
|
||||
data = resp.get_json()
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertEqual(data.get("profile"), "atlas-smart")
|
||||
self.assertEqual(data.get("model"), "smart-model")
|
||||
|
||||
def test_atlasbot_answer_uses_profile_specific_timeout(self):
|
||||
captured: dict[str, object] = {}
|
||||
|
||||
|
||||
@ -27,7 +27,7 @@
|
||||
<div class="status-list">
|
||||
<div v-for="item in statusItems" :key="item.label" class="status-row">
|
||||
<span class="status-dot" :class="item.state" aria-hidden="true"></span>
|
||||
<div>
|
||||
<div class="status-row-copy">
|
||||
<a v-if="item.labelHref" class="status-label status-title-link" :href="item.labelHref">
|
||||
{{ item.label }}
|
||||
</a>
|
||||
@ -45,6 +45,13 @@
|
||||
{{ item.value }}
|
||||
</RouterLink>
|
||||
<div v-else class="status-value mono">{{ item.value }}</div>
|
||||
<div v-if="item.children?.length" class="status-children" :aria-label="`${item.label} details`">
|
||||
<div v-for="child in item.children" :key="child.label" class="status-child">
|
||||
<span class="status-child-dot" :class="child.state" aria-hidden="true"></span>
|
||||
<span class="status-child-label">{{ child.label }}</span>
|
||||
<span class="status-child-value mono">{{ child.value }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -230,10 +237,18 @@ function isExternal(href) {
|
||||
background: var(--accent-cyan);
|
||||
}
|
||||
|
||||
.status-dot.warn {
|
||||
background: #f2c94c;
|
||||
}
|
||||
|
||||
.status-dot.bad {
|
||||
background: var(--accent-rose);
|
||||
}
|
||||
|
||||
.status-row-copy {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.status-label {
|
||||
font-weight: 800;
|
||||
color: var(--text-strong);
|
||||
@ -258,6 +273,45 @@ function isExternal(href) {
|
||||
color: var(--accent-cyan);
|
||||
}
|
||||
|
||||
.status-children {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.status-child {
|
||||
display: grid;
|
||||
grid-template-columns: 9px minmax(0, 1fr) auto;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
color: var(--text-muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.status-child-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 999px;
|
||||
background: var(--text-muted);
|
||||
}
|
||||
|
||||
.status-child-dot.ok {
|
||||
background: var(--accent-cyan);
|
||||
}
|
||||
|
||||
.status-child-dot.warn {
|
||||
background: #f2c94c;
|
||||
}
|
||||
|
||||
.status-child-dot.bad {
|
||||
background: var(--accent-rose);
|
||||
}
|
||||
|
||||
.status-child-value {
|
||||
color: var(--text-strong);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.status-note {
|
||||
margin: 14px 0 0;
|
||||
}
|
||||
|
||||
@ -85,11 +85,10 @@ const apiDisplay = apiUrl.host + apiUrl.pathname;
|
||||
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
const profiles = [
|
||||
{ id: "atlas-quick", label: "Atlas Quick" },
|
||||
{ id: "atlas-smart", label: "Atlas Smart" },
|
||||
{ id: "atlas-genius", label: "Atlas Genius" },
|
||||
];
|
||||
const activeProfile = ref("atlas-quick");
|
||||
const activeProfile = ref("atlas-smart");
|
||||
const profileState = reactive(
|
||||
Object.fromEntries(
|
||||
profiles.map((profile) => [
|
||||
@ -244,7 +243,8 @@ function handleKeydown(e) {
|
||||
|
||||
async function copyCurl() {
|
||||
const target = current.value.meta.endpoint || apiUrl.toString();
|
||||
const curl = `curl -X POST ${target} -H 'content-type: application/json' -d '{\"message\":\"hi\"}'`;
|
||||
const body = JSON.stringify({ message: "hi", profile: activeProfile.value });
|
||||
const curl = `curl -X POST ${target} -H 'content-type: application/json' -d '${body}'`;
|
||||
try {
|
||||
await navigator.clipboard.writeText(curl);
|
||||
copied.value = true;
|
||||
|
||||
@ -65,18 +65,19 @@ const mailtoHref = contactMailto();
|
||||
|
||||
const statusItems = computed(() => [
|
||||
{
|
||||
label: "Atlas platform",
|
||||
label: "Atlas cluster",
|
||||
labelHref: "#platform",
|
||||
value: statusLabelFrom(contentStatus.value.atlas),
|
||||
state: contentStatus.value.atlas,
|
||||
},
|
||||
{
|
||||
label: "Dedicated hosts",
|
||||
label: "Outpost hosts",
|
||||
value: dedicatedHostsLabel.value,
|
||||
state: dedicatedHostsState.value,
|
||||
children: dedicatedHostItems.value,
|
||||
},
|
||||
{
|
||||
label: "Most active service",
|
||||
label: "Most requested service",
|
||||
value: activeServiceLabel.value,
|
||||
href: activeServiceHref.value,
|
||||
state: activeServiceState.value,
|
||||
@ -98,14 +99,27 @@ const activeServiceState = computed(() => (activeService.value?.known ? "ok" : "
|
||||
const dedicatedHosts = computed(() => props.labStatus?.dedicated_hosts || null);
|
||||
const dedicatedHostsState = computed(() => {
|
||||
if (!dedicatedHosts.value?.known) return "unknown";
|
||||
return dedicatedHosts.value.up ? "ok" : "bad";
|
||||
const total = dedicatedHostTotal(dedicatedHosts.value);
|
||||
const upCount = Number.isFinite(dedicatedHosts.value.up_count) ? dedicatedHosts.value.up_count : 0;
|
||||
if (upCount <= 0) return "bad";
|
||||
if (upCount < total) return "warn";
|
||||
return "ok";
|
||||
});
|
||||
const dedicatedHostsLabel = computed(() => {
|
||||
const hosts = dedicatedHosts.value;
|
||||
if (!hosts?.known) return "Waiting for public checks";
|
||||
const total = Number.isFinite(hosts.total) ? hosts.total : 2;
|
||||
const total = dedicatedHostTotal(hosts);
|
||||
const upCount = Number.isFinite(hosts.up_count) ? hosts.up_count : 0;
|
||||
return `${upCount} of ${total} responding`;
|
||||
return `${upCount} of ${total} online`;
|
||||
});
|
||||
const dedicatedHostItems = computed(() => {
|
||||
const hosts = dedicatedHosts.value?.hosts;
|
||||
if (!Array.isArray(hosts) || !hosts.length) return [];
|
||||
return hosts.map((host) => ({
|
||||
label: dedicatedHostLabel(host.label),
|
||||
value: host.known ? (host.up ? "Online" : "Offline") : "Unknown",
|
||||
state: statusState(host),
|
||||
}));
|
||||
});
|
||||
|
||||
const lastCheckedLabel = computed(() => {
|
||||
@ -135,6 +149,18 @@ function statusLabelFrom(state) {
|
||||
if (state === "bad") return "Needs attention";
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
function dedicatedHostTotal(hosts) {
|
||||
if (Number.isFinite(hosts?.total)) return hosts.total;
|
||||
return Array.isArray(hosts?.hosts) && hosts.hosts.length ? hosts.hosts.length : 2;
|
||||
}
|
||||
|
||||
function dedicatedHostLabel(label = "") {
|
||||
const normalized = label.toLowerCase();
|
||||
if (normalized.includes("database") || normalized.includes("db")) return "Database machine";
|
||||
if (normalized.includes("jump") || normalized.includes("theia")) return "Jumphost";
|
||||
return label || "Host";
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@ -85,7 +85,8 @@ test("shows Brad's professional homepage and opens the platform diagram overlay"
|
||||
await expect(page.getByRole("link", { name: "Register" })).toBeVisible();
|
||||
await expect(page.getByRole("heading", { name: "From 0 to 100" })).toBeVisible();
|
||||
await expect(page.getByRole("link", { name: "Nextcloud", exact: true })).toBeVisible();
|
||||
await expect(page.getByText("2 of 2 responding")).toBeVisible();
|
||||
await expect(page.getByText("2 of 2 online")).toBeVisible();
|
||||
await expect(page.getByText("Database machine")).toBeVisible();
|
||||
|
||||
await page.getByRole("link", { name: "Explore the Live Platform" }).click();
|
||||
await expect(page.getByRole("heading", { name: "Titan Lab: Live Platform" })).toBeVisible();
|
||||
|
||||
@ -55,7 +55,7 @@ describe("AI chat view", () => {
|
||||
installFetch((url) => {
|
||||
if (url.includes("/api/ai/info")) {
|
||||
return jsonResponse({
|
||||
model: url.includes("atlas-smart") ? "smart-model" : "quick-model",
|
||||
model: url.includes("atlas-genius") ? "genius-model" : "smart-model",
|
||||
gpu: "titan-24",
|
||||
node: "titan-24",
|
||||
endpoint: "https://ai.example.dev/chat",
|
||||
@ -76,16 +76,18 @@ describe("AI chat view", () => {
|
||||
const wrapper = mount(AiView);
|
||||
await flushPromises();
|
||||
|
||||
expect(wrapper.text()).toContain("quick-model");
|
||||
expect(wrapper.text()).not.toContain("Atlas Quick");
|
||||
expect(wrapper.text()).toContain("smart-model");
|
||||
expect(wrapper.text()).toContain("titan-24");
|
||||
|
||||
await wrapper.find(".endpoint-copy").trigger("click");
|
||||
expect(navigator.clipboard.writeText).toHaveBeenCalledWith(expect.stringContaining("curl -X POST https://ai.example.dev/chat"));
|
||||
expect(navigator.clipboard.writeText).toHaveBeenCalledWith(expect.stringContaining('"profile":"atlas-smart"'));
|
||||
expect(wrapper.text()).toContain("copied");
|
||||
|
||||
await wrapper.findAll(".profile-tab").find((button) => button.text() === "Atlas Smart").trigger("click");
|
||||
await wrapper.findAll(".profile-tab").find((button) => button.text() === "Atlas Genius").trigger("click");
|
||||
await flushPromises();
|
||||
expect(wrapper.text()).toContain("smart-model");
|
||||
expect(wrapper.text()).toContain("genius-model");
|
||||
|
||||
Object.defineProperty(navigator, "clipboard", {
|
||||
configurable: true,
|
||||
@ -100,7 +102,7 @@ describe("AI chat view", () => {
|
||||
it("sends JSON chat requests and reveals typed responses", async () => {
|
||||
const bodies = [];
|
||||
installFetch((url, options) => {
|
||||
if (url.includes("/api/ai/info")) return jsonResponse({ model: "quick-model" });
|
||||
if (url.includes("/api/ai/info")) return jsonResponse({ model: "smart-model" });
|
||||
bodies.push(JSON.parse(options.body));
|
||||
return jsonResponse({ reply: "typed assistant response", latency_ms: 42 });
|
||||
});
|
||||
@ -117,10 +119,10 @@ describe("AI chat view", () => {
|
||||
expect(wrapper.text()).toContain("42 ms");
|
||||
expect(bodies[0]).toMatchObject({
|
||||
message: "hello atlas",
|
||||
profile: "atlas-quick",
|
||||
conversation_id: expect.stringContaining("atlas-quick"),
|
||||
profile: "atlas-smart",
|
||||
conversation_id: expect.stringContaining("atlas-smart"),
|
||||
});
|
||||
expect(localStorage.getItem("atlas-ai-conversation:atlas-quick")).toContain("uuid-1");
|
||||
expect(localStorage.getItem("atlas-ai-conversation:atlas-smart")).toContain("uuid-1");
|
||||
|
||||
await wrapper.find("textarea").setValue("second");
|
||||
await wrapper.find("form").trigger("submit.prevent");
|
||||
|
||||
@ -96,11 +96,13 @@ describe("HomeView", () => {
|
||||
expect(wrapper.text()).not.toContain("Looking for hosted services or an existing Titan Lab account");
|
||||
expect(wrapper.find("a[href^='mailto:brad@bstein.dev']").exists()).toBe(true);
|
||||
expect(wrapper.findComponent(PlatformSection).exists()).toBe(true);
|
||||
expect(wrapper.text()).toContain("Atlas platform");
|
||||
expect(wrapper.text()).toContain("Most active service");
|
||||
expect(wrapper.text()).toContain("Atlas cluster");
|
||||
expect(wrapper.text()).toContain("Most requested service");
|
||||
expect(wrapper.text()).toContain("Nextcloud");
|
||||
expect(wrapper.text()).toContain("Dedicated hosts");
|
||||
expect(wrapper.text()).toContain("2 of 2 responding");
|
||||
expect(wrapper.text()).toContain("Outpost hosts");
|
||||
expect(wrapper.text()).toContain("2 of 2 online");
|
||||
expect(wrapper.text()).toContain("Database machine");
|
||||
expect(wrapper.text()).toContain("Jumphost");
|
||||
expect(wrapper.text()).toContain("Responding");
|
||||
});
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user