feat(dashboard): dynamic agent metrics + model list from config, no more hardcoded data
This commit is contained in:
@@ -345,16 +345,12 @@ public class DashboardController(
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns the list of available models that can be assigned to agents.
|
/// Returns the list of available models that can be assigned to agents.
|
||||||
|
/// Reads from OpenClaw config dynamically, falls back to hardcoded list.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[HttpGet("models")]
|
[HttpGet("models")]
|
||||||
public ActionResult<List<ModelOption>> GetAvailableModels()
|
public ActionResult<List<ModelOption>> GetAvailableModels()
|
||||||
{
|
{
|
||||||
var models = new List<ModelOption>
|
var models = gateway.GetAvailableModels();
|
||||||
{
|
|
||||||
new ModelOption("openai/gpt-5.4", "GPT-5.4", "openai"),
|
|
||||||
new ModelOption("deepseek/deepseek-v4-flash", "DeepSeek V4 Flash", "deepseek"),
|
|
||||||
new ModelOption("deepseek/deepseek-v4-pro", "DeepSeek V4 Pro", "deepseek")
|
|
||||||
};
|
|
||||||
return Ok(models);
|
return Ok(models);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,10 @@ public sealed record DashboardAgentInfo(
|
|||||||
bool IsActive,
|
bool IsActive,
|
||||||
string? CurrentTask,
|
string? CurrentTask,
|
||||||
string? Description,
|
string? Description,
|
||||||
string[] Tags
|
string[] Tags,
|
||||||
|
int Progress = 0,
|
||||||
|
int Workload = 0,
|
||||||
|
string? Goal = null
|
||||||
);
|
);
|
||||||
|
|
||||||
public sealed record MessageEntry(
|
public sealed record MessageEntry(
|
||||||
|
|||||||
@@ -119,6 +119,11 @@ public sealed class OpenClawGatewayClient(HttpClient httpClient, IConfiguration
|
|||||||
var agentIds = LoadAgentIdsFromConfig();
|
var agentIds = LoadAgentIdsFromConfig();
|
||||||
|
|
||||||
var agents = new List<DashboardAgentInfo>();
|
var agents = new List<DashboardAgentInfo>();
|
||||||
|
|
||||||
|
// Read queue once for workload calculation
|
||||||
|
var queueItems = new List<QueueItem>();
|
||||||
|
try { queueItems = await GetQueueAsync(); } catch { }
|
||||||
|
|
||||||
foreach (var id in agentIds)
|
foreach (var id in agentIds)
|
||||||
{
|
{
|
||||||
// Skip the "main" agent (it's the default assistant, not a sub-agent)
|
// Skip the "main" agent (it's the default assistant, not a sub-agent)
|
||||||
@@ -177,6 +182,15 @@ public sealed class OpenClawGatewayClient(HttpClient httpClient, IConfiguration
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 6. Read goal from workspace files
|
||||||
|
var goal = await ReadAgentGoalAsync(id);
|
||||||
|
|
||||||
|
// 7. Calculate progress dynamically
|
||||||
|
var progress = CalculateAgentProgress(id, isActive, status);
|
||||||
|
|
||||||
|
// 8. Calculate workload from queue items
|
||||||
|
var workload = CalculateAgentWorkload(id, queueItems);
|
||||||
|
|
||||||
agents.Add(new DashboardAgentInfo(
|
agents.Add(new DashboardAgentInfo(
|
||||||
Id: id,
|
Id: id,
|
||||||
Name: string.IsNullOrWhiteSpace(name) ? DeriveRole(id) : name,
|
Name: string.IsNullOrWhiteSpace(name) ? DeriveRole(id) : name,
|
||||||
@@ -185,7 +199,10 @@ public sealed class OpenClawGatewayClient(HttpClient httpClient, IConfiguration
|
|||||||
IsActive: isActive,
|
IsActive: isActive,
|
||||||
CurrentTask: currentTask,
|
CurrentTask: currentTask,
|
||||||
Description: description,
|
Description: description,
|
||||||
Tags: tags
|
Tags: tags,
|
||||||
|
Progress: progress,
|
||||||
|
Workload: workload,
|
||||||
|
Goal: goal
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
return agents;
|
return agents;
|
||||||
@@ -770,6 +787,241 @@ public sealed class OpenClawGatewayClient(HttpClient httpClient, IConfiguration
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reads the agent's goal from workspace files (goals.md preferred, then AGENTS.md, SOUL.md).
|
||||||
|
/// Returns the first meaningful line or null.
|
||||||
|
/// </summary>
|
||||||
|
private async Task<string?> ReadAgentGoalAsync(string agentId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var workspacePath = "/home/node/.openclaw/workspace-" + agentId;
|
||||||
|
|
||||||
|
// 1. Try goals.md first
|
||||||
|
var goalsPath = Path.Combine(workspacePath, "goals.md");
|
||||||
|
if (System.IO.File.Exists(goalsPath))
|
||||||
|
{
|
||||||
|
var content = await System.IO.File.ReadAllTextAsync(goalsPath);
|
||||||
|
foreach (var line in content.Split('\n'))
|
||||||
|
{
|
||||||
|
var trimmed = line.Trim();
|
||||||
|
if (!string.IsNullOrWhiteSpace(trimmed) && !trimmed.StartsWith('#') && !trimmed.StartsWith('-'))
|
||||||
|
return trimmed.Length > 120 ? trimmed[..117] + "..." : trimmed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Try SOUL.md "## Rolle" section
|
||||||
|
var soulPath = Path.Combine(workspacePath, "SOUL.md");
|
||||||
|
if (System.IO.File.Exists(soulPath))
|
||||||
|
{
|
||||||
|
var soul = await System.IO.File.ReadAllTextAsync(soulPath);
|
||||||
|
using var reader = new StringReader(soul);
|
||||||
|
string? line;
|
||||||
|
var inRoleSection = false;
|
||||||
|
while ((line = reader.ReadLine()) is not null)
|
||||||
|
{
|
||||||
|
var trimmed = line.Trim();
|
||||||
|
if (trimmed.StartsWith("## ", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
if (inRoleSection) break;
|
||||||
|
if (trimmed.IndexOf("Rolle", StringComparison.OrdinalIgnoreCase) >= 0
|
||||||
|
|| trimmed.IndexOf("Role", StringComparison.OrdinalIgnoreCase) >= 0
|
||||||
|
|| trimmed.IndexOf("Oberstes", StringComparison.OrdinalIgnoreCase) >= 0)
|
||||||
|
{
|
||||||
|
inRoleSection = true;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (inRoleSection && !string.IsNullOrWhiteSpace(trimmed) && !trimmed.StartsWith('-'))
|
||||||
|
{
|
||||||
|
return trimmed.Length > 120 ? trimmed[..117] + "..." : trimmed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Look for "## Oberstes Prinzip" as second choice
|
||||||
|
inRoleSection = false;
|
||||||
|
reader = new StringReader(soul);
|
||||||
|
while ((line = reader.ReadLine()) is not null)
|
||||||
|
{
|
||||||
|
var trimmed = line.Trim();
|
||||||
|
if (trimmed.StartsWith("## ") && trimmed.IndexOf("Prinzip", StringComparison.OrdinalIgnoreCase) >= 0)
|
||||||
|
{
|
||||||
|
inRoleSection = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (inRoleSection && !string.IsNullOrWhiteSpace(trimmed) && !trimmed.StartsWith('-') && !trimmed.StartsWith('#'))
|
||||||
|
{
|
||||||
|
return trimmed.Length > 120 ? trimmed[..117] + "..." : trimmed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Try AGENTS.md first heading
|
||||||
|
var agentsPath = Path.Combine(workspacePath, "AGENTS.md");
|
||||||
|
if (System.IO.File.Exists(agentsPath))
|
||||||
|
{
|
||||||
|
var agentsContent = await System.IO.File.ReadAllTextAsync(agentsPath);
|
||||||
|
foreach (var line in agentsContent.Split('\n'))
|
||||||
|
{
|
||||||
|
var trimmed = line.Trim();
|
||||||
|
if (trimmed.StartsWith("# "))
|
||||||
|
return trimmed[2..].Trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Fallback to hardcoded
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: known goals
|
||||||
|
return agentId.ToLowerInvariant() switch
|
||||||
|
{
|
||||||
|
"iris" => "Mission Control — maximale Autonomie bei kontrolliertem Risiko",
|
||||||
|
"programmer" => "Nexus Dashboard & Dungeon System",
|
||||||
|
"reviewer" => "Zero critical findings before merge",
|
||||||
|
"architekt" => "Stabile Zero-Downtime-Deployments",
|
||||||
|
"executor" => "Sichere Host-Execution im Allowlist-Rahmen",
|
||||||
|
"researcher" => "Verifizierte, strukturierte Recherche-Ergebnisse",
|
||||||
|
_ => null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Calculates agent progress (0-100) based on session activity.
|
||||||
|
/// Active agents with recent activity get higher scores.
|
||||||
|
/// Idle agents or inactive sessions get lower scores.
|
||||||
|
/// </summary>
|
||||||
|
private static int CalculateAgentProgress(string agentId, bool isActive, JsonNode? status)
|
||||||
|
{
|
||||||
|
if (!isActive)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
// Base progress from active status
|
||||||
|
var progress = 50;
|
||||||
|
|
||||||
|
// Boost for agents that have a current task
|
||||||
|
var hasTask = status?["currentTask"]?.GetValue<string>() is not null
|
||||||
|
|| status?["task"]?.GetValue<string>() is not null;
|
||||||
|
if (hasTask)
|
||||||
|
progress += 20;
|
||||||
|
|
||||||
|
// Check for last activity timestamp — more recent = higher progress
|
||||||
|
var lastActivity = status?["lastActivity"]?.GetValue<string>()
|
||||||
|
?? status?["lastMessage"]?.GetValue<string>();
|
||||||
|
if (lastActivity is not null && DateTimeOffset.TryParse(lastActivity, out var lastTs))
|
||||||
|
{
|
||||||
|
var minutesSinceActivity = (DateTimeOffset.UtcNow - lastTs).TotalMinutes;
|
||||||
|
if (minutesSinceActivity < 1)
|
||||||
|
progress += 25; // actively working
|
||||||
|
else if (minutesSinceActivity < 5)
|
||||||
|
progress += 15;
|
||||||
|
else if (minutesSinceActivity < 15)
|
||||||
|
progress += 10;
|
||||||
|
else
|
||||||
|
progress -= 10; // stale
|
||||||
|
}
|
||||||
|
else if (hasTask)
|
||||||
|
{
|
||||||
|
// Has task but no timestamp — assume actively working
|
||||||
|
progress += 15;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.Clamp(progress, 0, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Calculates agent workload (0-100) based on queue items and active status.
|
||||||
|
/// More queued tasks or active sessions = higher workload.
|
||||||
|
/// </summary>
|
||||||
|
private static int CalculateAgentWorkload(string agentId, List<QueueItem> queueItems)
|
||||||
|
{
|
||||||
|
if (queueItems.Count == 0)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
// Calculate workload based on queue density per agent
|
||||||
|
var agentQueued = queueItems.Count(q =>
|
||||||
|
q.Name.Contains(agentId, StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| q.Id.Contains(agentId, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
|
// Base workload from total queue pressure
|
||||||
|
var totalQueuePressure = Math.Min(queueItems.Count * 10, 60);
|
||||||
|
|
||||||
|
// Agent-specific queue items add extra weight
|
||||||
|
var agentPressure = agentQueued * 25;
|
||||||
|
|
||||||
|
return Math.Clamp(totalQueuePressure + agentPressure, 0, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the list of available models by reading from the OpenClaw config,
|
||||||
|
/// with fallback to hardcoded list.
|
||||||
|
/// </summary>
|
||||||
|
public List<ModelOption> GetAvailableModels()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var configPath = configuration.GetValue<string>("AgentConfigPath")
|
||||||
|
?? "/home/node/.openclaw/openclaw.json";
|
||||||
|
|
||||||
|
if (!System.IO.File.Exists(configPath))
|
||||||
|
return GetDefaultModels();
|
||||||
|
|
||||||
|
var json = System.IO.File.ReadAllText(configPath);
|
||||||
|
using var doc = JsonDocument.Parse(json);
|
||||||
|
var root = doc.RootElement;
|
||||||
|
|
||||||
|
// Read models from agents.defaults.models
|
||||||
|
if (!root.TryGetProperty("agents", out var agentsEl))
|
||||||
|
return GetDefaultModels();
|
||||||
|
if (!agentsEl.TryGetProperty("defaults", out var defaultsEl))
|
||||||
|
return GetDefaultModels();
|
||||||
|
if (!defaultsEl.TryGetProperty("models", out var modelsEl))
|
||||||
|
return GetDefaultModels();
|
||||||
|
|
||||||
|
var models = new List<ModelOption>();
|
||||||
|
foreach (var modelProp in modelsEl.EnumerateObject())
|
||||||
|
{
|
||||||
|
var modelId = modelProp.Name;
|
||||||
|
var modelObj = modelProp.Value;
|
||||||
|
|
||||||
|
var name = modelId; // fallback: use model ID as name
|
||||||
|
var provider = ExtractProvider(modelId);
|
||||||
|
|
||||||
|
// Check for alias in the model object
|
||||||
|
if (modelObj.TryGetProperty("alias", out var aliasEl))
|
||||||
|
{
|
||||||
|
var alias = aliasEl.GetString();
|
||||||
|
if (!string.IsNullOrWhiteSpace(alias))
|
||||||
|
name = alias;
|
||||||
|
}
|
||||||
|
|
||||||
|
models.Add(new ModelOption(modelId, name, provider));
|
||||||
|
}
|
||||||
|
|
||||||
|
return models.Count > 0 ? models : GetDefaultModels();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return GetDefaultModels();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<ModelOption> GetDefaultModels()
|
||||||
|
=> new()
|
||||||
|
{
|
||||||
|
new ModelOption("openai/gpt-5.4", "GPT-5.4", "openai"),
|
||||||
|
new ModelOption("deepseek/deepseek-v4-flash", "DeepSeek V4 Flash", "deepseek"),
|
||||||
|
new ModelOption("deepseek/deepseek-v4-pro", "DeepSeek V4 Pro", "deepseek"),
|
||||||
|
new ModelOption("openai/gpt-5.5", "GPT-5.5", "openai")
|
||||||
|
};
|
||||||
|
|
||||||
|
private static string ExtractProvider(string modelId)
|
||||||
|
{
|
||||||
|
var slash = modelId.IndexOf('/');
|
||||||
|
return slash > 0 ? modelId[..slash] : "unknown";
|
||||||
|
}
|
||||||
|
|
||||||
private static string DeriveRole(string agentId) => agentId.ToLowerInvariant() switch
|
private static string DeriveRole(string agentId) => agentId.ToLowerInvariant() switch
|
||||||
{
|
{
|
||||||
"iris" => "Chief of Staff",
|
"iris" => "Chief of Staff",
|
||||||
|
|||||||
@@ -77,6 +77,9 @@ interface DashboardAgentInfo {
|
|||||||
currentTask: string
|
currentTask: string
|
||||||
description?: string
|
description?: string
|
||||||
tags?: string[]
|
tags?: string[]
|
||||||
|
progress?: number
|
||||||
|
workload?: number
|
||||||
|
goal?: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DashboardOperationEntry {
|
interface DashboardOperationEntry {
|
||||||
@@ -217,9 +220,10 @@ function enrichAgent(api: DashboardAgentInfo): AgentNodeData {
|
|||||||
color: catalog.color ?? '#6b7385',
|
color: catalog.color ?? '#6b7385',
|
||||||
icon: catalog.icon ?? 'bot',
|
icon: catalog.icon ?? 'bot',
|
||||||
hero: catalog.hero ?? false,
|
hero: catalog.hero ?? false,
|
||||||
goal: catalog.goal ?? 'No goal set',
|
// Use API-provided values with catalog fallback for metrics
|
||||||
progress: catalog.progress ?? 0,
|
goal: api.goal ?? catalog.goal ?? 'No goal set',
|
||||||
workload: catalog.workload ?? 0,
|
progress: api.progress ?? catalog.progress ?? 0,
|
||||||
|
workload: api.workload ?? catalog.workload ?? 0,
|
||||||
runtimeSeconds: 0,
|
runtimeSeconds: 0,
|
||||||
workingFeed: catalog.workingFeed ?? [],
|
workingFeed: catalog.workingFeed ?? [],
|
||||||
thinkingStream: catalog.thinkingStream ?? [],
|
thinkingStream: catalog.thinkingStream ?? [],
|
||||||
|
|||||||
Reference in New Issue
Block a user