Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ serde_json = "1.0.140"
tokio = { version = "1.45.1", features = ["full"] }
tokio-stream = { version = "0.1.17", features = ["sync"] }
url = { version = "2.5.4", features = ["serde"] }
regex = "1.11.1"

chrono = { version = "0.4.41", optional = true }
crossterm = { version = "0.28.1", features = ["event-stream"], optional = true }
Expand Down
145 changes: 131 additions & 14 deletions resources/ts/components/AgentsList.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import clsx from "clsx";
import React, { CSSProperties } from "react";
import React, { CSSProperties, useState } from "react";

import { type Agent } from "../schemas/Agent";

Expand All @@ -9,27 +9,145 @@ import {
agentUsage,
agentUsage__progress,
agentsTable,
sortIndicator,
sortIndicatorAsc,
sortIndicatorDesc,
} from "./Dashboard.module.css";

function formatTimestamp(timestamp: number): string {
return new Date(timestamp * 1000).toLocaleString();
}

type SortColumn =
| "name"
| "model"
| "issues"
| "llamacppAddr"
| "lastUpdate"
| "idleSlots"
| "processingSlots";

function getSortIndicator(
sortConfig: { key: SortColumn; direction: "ascending" | "descending" },
currentKey: SortColumn
): React.ReactNode {
if (sortConfig.key !== currentKey) {
return null;
}
const className = clsx(sortIndicator, sortConfig.direction === "ascending" ? sortIndicatorAsc : sortIndicatorDesc);
return (
<span className={className}>
{sortConfig.direction === "ascending" ? "↑" : "↓"}
</span>
);
}

export function AgentsList({ agents }: { agents: Array<Agent> }) {
const [sortConfig, setSortConfig] = useState<{
key: SortColumn;
direction: "ascending" | "descending";
}>({ key: "name", direction: "ascending" });

function sortAgents(agents: Array<Agent>): Array<Agent> {
const sortableAgents = [...agents];
sortableAgents.sort(function (a, b) {
const { key, direction } = sortConfig;

// Helper function to get comparison value based on column type
function getValue(agent: Agent, key: SortColumn): string | number {
switch (key) {
case "name":
return agent.status.agent_name || "";
case "model":
return agent.status.model || "";
case "llamacppAddr":
return agent.status.external_llamacpp_addr;
case "lastUpdate":
return agent.last_update.secs_since_epoch;
case "idleSlots":
return agent.status.slots_idle;
case "processingSlots":
return agent.status.slots_processing;
default:
return "";
}
}

// Special handling for issues column
if (key === "issues") {
const hasIssuesA = a.status.error !== null;
const hasIssuesB = b.status.error !== null;
if (hasIssuesA !== hasIssuesB) {
return direction === "ascending" ? (hasIssuesA ? 1 : -1) : (hasIssuesA ? -1 : 1);
}
const errorA = a.status.error || "";
const errorB = b.status.error || "";
if (errorA < errorB) return direction === "ascending" ? -1 : 1;
if (errorA > errorB) return direction === "ascending" ? 1 : -1;
return 0;
}

const valueA = getValue(a, key);
const valueB = getValue(b, key);

// Handle string comparison
if (typeof valueA === "string" && typeof valueB === "string") {
if (valueA < valueB) return direction === "ascending" ? -1 : 1;
if (valueA > valueB) return direction === "ascending" ? 1 : -1;
return 0;
}

// Handle numeric comparison
if (typeof valueA === "number" && typeof valueB === "number") {
if (valueA < valueB) return direction === "ascending" ? -1 : 1;
if (valueA > valueB) return direction === "ascending" ? 1 : -1;
return 0;
}

return 0;
});
return sortableAgents;
}

function requestSort(key: SortColumn) {
let direction: "ascending" | "descending" = "ascending";
if (sortConfig.key === key && sortConfig.direction === "ascending") {
direction = "descending";
}
setSortConfig({ key, direction });
}

const sortedAgents = sortAgents(agents);

return (
<table className={agentsTable}>
<thead>
<tr>
<th>Name</th>
<th>Issues</th>
<th>Llama.cpp address</th>
<th>Last update</th>
<th>Idle slots</th>
<th>Processing slots</th>
<th onClick={function () { requestSort("name"); }}>
Name{getSortIndicator(sortConfig, "name")}
</th>
<th onClick={function () { requestSort("model"); }}>
Model{getSortIndicator(sortConfig, "model")}
</th>
<th onClick={function () { requestSort("issues"); }}>
Issues{getSortIndicator(sortConfig, "issues")}
</th>
<th onClick={function () { requestSort("llamacppAddr"); }}>
Llama.cpp address{getSortIndicator(sortConfig, "llamacppAddr")}
</th>
<th onClick={function () { requestSort("lastUpdate"); }}>
Last update{getSortIndicator(sortConfig, "lastUpdate")}
</th>
<th onClick={function () { requestSort("idleSlots"); }}>
Idle slots{getSortIndicator(sortConfig, "idleSlots")}
</th>
<th onClick={function () { requestSort("processingSlots"); }}>
Processing slots{getSortIndicator(sortConfig, "processingSlots")}
</th>
</tr>
</thead>
<tbody>
{agents.map(function ({
{sortedAgents.map(function ({
agent_id,
last_update,
quarantined_until,
Expand All @@ -47,13 +165,12 @@ export function AgentsList({ agents }: { agents: Array<Agent> }) {
quarantined_until;

return (
<tr
className={clsx(agentRow, {
[agentRowError]: hasIssues,
})}
key={agent_id}
>
<tr
className={clsx(agentRow, hasIssues ? agentRowError : undefined)}
key={agent_id}
>
<td>{status.agent_name}</td>
<td>{status.model}</td>
<td>
{status.error && (
<>
Expand Down
18 changes: 18 additions & 0 deletions resources/ts/components/Dashboard.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,29 @@
th {
border: 1px solid var(--color-border);
padding: var(--spacing-base);
cursor: pointer;

p + p {
margin-top: var(--spacing-half);
}
}

th:hover {
background-color: var(--color-hover);
}
}

.sortIndicator {
margin-left: var(--spacing-half);
font-size: 0.8em;
}

.sortIndicatorAsc {
color: var(--color-success);
}

.sortIndicatorDesc {
color: var(--color-error);
}

.agentRow.agentRowError {
Expand Down
1 change: 1 addition & 0 deletions resources/ts/schemas/Agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { StatusUpdateSchema } from "./StatusUpdate";
export const AgentSchema = z
.object({
agent_id: z.string(),
model: z.string().nullable(),
last_update: z.object({
nanos_since_epoch: z.number(),
secs_since_epoch: z.number(),
Expand Down
1 change: 1 addition & 0 deletions resources/ts/schemas/StatusUpdate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export const StatusUpdateSchema = z
is_unexpected_response_status: z.boolean().nullable(),
slots_idle: z.number(),
slots_processing: z.number(),
model: z.string().nullable(),
})
.strict();

Expand Down
15 changes: 14 additions & 1 deletion src/agent/monitoring_service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ pub struct MonitoringService {
monitoring_interval: Duration,
name: Option<String>,
status_update_tx: Sender<Bytes>,
check_model: bool, // Store the check_model flag
}

impl MonitoringService {
Expand All @@ -32,13 +33,15 @@ impl MonitoringService {
monitoring_interval: Duration,
name: Option<String>,
status_update_tx: Sender<Bytes>,
check_model: bool, // Include the check_model flag
) -> Result<Self> {
Ok(MonitoringService {
external_llamacpp_addr,
llamacpp_client,
monitoring_interval,
name,
status_update_tx,
check_model,
})
}

Expand All @@ -50,6 +53,15 @@ impl MonitoringService {
.filter(|slot| slot.is_processing)
.count();

let model: Option<String> = if self.check_model {
match self.llamacpp_client.get_model().await {
Ok(model) => model,
Err(_) => None,
}
} else {
Some("".to_string())
};

StatusUpdate {
agent_name: self.name.to_owned(),
error: slots_response.error,
Expand All @@ -63,6 +75,7 @@ impl MonitoringService {
is_unexpected_response_status: slots_response.is_unexpected_response_status,
slots_idle: slots_response.slots.len() - slots_processing,
slots_processing,
model,
}
}

Expand Down Expand Up @@ -109,4 +122,4 @@ impl Service for MonitoringService {
fn threads(&self) -> Option<usize> {
Some(1)
}
}
}
Loading