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>
|
||||
/// Returns the list of available models that can be assigned to agents.
|
||||
/// Reads from OpenClaw config dynamically, falls back to hardcoded list.
|
||||
/// </summary>
|
||||
[HttpGet("models")]
|
||||
public ActionResult<List<ModelOption>> GetAvailableModels()
|
||||
{
|
||||
var models = new List<ModelOption>
|
||||
{
|
||||
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")
|
||||
};
|
||||
var models = gateway.GetAvailableModels();
|
||||
return Ok(models);
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,10 @@ public sealed record DashboardAgentInfo(
|
||||
bool IsActive,
|
||||
string? CurrentTask,
|
||||
string? Description,
|
||||
string[] Tags
|
||||
string[] Tags,
|
||||
int Progress = 0,
|
||||
int Workload = 0,
|
||||
string? Goal = null
|
||||
);
|
||||
|
||||
public sealed record MessageEntry(
|
||||
|
||||
@@ -119,6 +119,11 @@ public sealed class OpenClawGatewayClient(HttpClient httpClient, IConfiguration
|
||||
var agentIds = LoadAgentIdsFromConfig();
|
||||
|
||||
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)
|
||||
{
|
||||
// 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(
|
||||
Id: id,
|
||||
Name: string.IsNullOrWhiteSpace(name) ? DeriveRole(id) : name,
|
||||
@@ -185,7 +199,10 @@ public sealed class OpenClawGatewayClient(HttpClient httpClient, IConfiguration
|
||||
IsActive: isActive,
|
||||
CurrentTask: currentTask,
|
||||
Description: description,
|
||||
Tags: tags
|
||||
Tags: tags,
|
||||
Progress: progress,
|
||||
Workload: workload,
|
||||
Goal: goal
|
||||
));
|
||||
}
|
||||
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
|
||||
{
|
||||
"iris" => "Chief of Staff",
|
||||
|
||||
@@ -77,6 +77,9 @@ interface DashboardAgentInfo {
|
||||
currentTask: string
|
||||
description?: string
|
||||
tags?: string[]
|
||||
progress?: number
|
||||
workload?: number
|
||||
goal?: string | null
|
||||
}
|
||||
|
||||
interface DashboardOperationEntry {
|
||||
@@ -217,9 +220,10 @@ function enrichAgent(api: DashboardAgentInfo): AgentNodeData {
|
||||
color: catalog.color ?? '#6b7385',
|
||||
icon: catalog.icon ?? 'bot',
|
||||
hero: catalog.hero ?? false,
|
||||
goal: catalog.goal ?? 'No goal set',
|
||||
progress: catalog.progress ?? 0,
|
||||
workload: catalog.workload ?? 0,
|
||||
// Use API-provided values with catalog fallback for metrics
|
||||
goal: api.goal ?? catalog.goal ?? 'No goal set',
|
||||
progress: api.progress ?? catalog.progress ?? 0,
|
||||
workload: api.workload ?? catalog.workload ?? 0,
|
||||
runtimeSeconds: 0,
|
||||
workingFeed: catalog.workingFeed ?? [],
|
||||
thinkingStream: catalog.thinkingStream ?? [],
|
||||
|
||||
Reference in New Issue
Block a user