Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 068b0d31b8 | |||
| 97b8588dc3 |
@@ -52,36 +52,30 @@ public class DashboardController(
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the latest assistant messages (operations/feed) from the Iris session.
|
||||
/// Filtered to role == "assistant" — those are the work feed entries.
|
||||
/// Returns the latest assistant messages aggregated from ALL agent sessions.
|
||||
/// Events are sorted by timestamp descending (newest first).
|
||||
/// Supports optional agent filter via ?agent= query parameter.
|
||||
/// Falls back to Iris-only feed if multi-agent feed fails.
|
||||
/// </summary>
|
||||
[HttpGet("operations")]
|
||||
public async Task<List<FeedEntry>> GetOperations([FromQuery] int limit = 20)
|
||||
public async Task<List<FeedEntry>> GetOperations(
|
||||
[FromQuery] int limit = 20,
|
||||
[FromQuery] string? agent = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var messages = await gateway.GetSessionHistoryAsync("iris", Math.Clamp(limit, 1, 100));
|
||||
var feed = new List<FeedEntry>();
|
||||
var entries = await gateway.GetAllAgentOperationsAsync(Math.Clamp(limit, 1, 100));
|
||||
|
||||
foreach (var msg in messages)
|
||||
// Optional agent filter
|
||||
if (!string.IsNullOrWhiteSpace(agent))
|
||||
{
|
||||
if (!string.Equals(msg.Role, "assistant", StringComparison.OrdinalIgnoreCase))
|
||||
continue;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(msg.Content))
|
||||
continue;
|
||||
|
||||
// Parse timestamp for display-friendly "time ago"
|
||||
var ts = ParseTimestamp(msg.Timestamp);
|
||||
var timeAgo = FormatTimeAgo(ts);
|
||||
|
||||
// Extract a short agent indicator and action from content
|
||||
var (agent, action) = ExtractAgentAction(msg.Content);
|
||||
|
||||
feed.Add(new FeedEntry(agent, action, msg.Timestamp, timeAgo));
|
||||
entries = entries
|
||||
.Where(e => string.Equals(e.AgentId, agent, StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(e.Agent, agent, StringComparison.OrdinalIgnoreCase))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
return feed;
|
||||
return entries;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -367,55 +361,5 @@ public class DashboardController(
|
||||
t.UpdatedAt
|
||||
);
|
||||
|
||||
private static DateTimeOffset ParseTimestamp(string timestamp)
|
||||
{
|
||||
if (DateTimeOffset.TryParse(timestamp, null, System.Globalization.DateTimeStyles.None, out var dt))
|
||||
return dt;
|
||||
return DateTimeOffset.UtcNow;
|
||||
}
|
||||
|
||||
private static string FormatTimeAgo(DateTimeOffset ts)
|
||||
{
|
||||
var diff = DateTimeOffset.UtcNow - ts;
|
||||
if (diff.TotalMinutes < 1) return "just now";
|
||||
if (diff.TotalMinutes < 60) return $"{(int)diff.TotalMinutes}m ago";
|
||||
if (diff.TotalHours < 24) return $"{(int)diff.TotalHours}h ago";
|
||||
if (diff.TotalDays < 7) return $"{(int)diff.TotalDays}d ago";
|
||||
return ts.ToString("MMM dd");
|
||||
}
|
||||
|
||||
private static (string Agent, string Action) ExtractAgentAction(string content)
|
||||
{
|
||||
// Take first line or first ~80 chars as the action summary
|
||||
var firstLine = content.Split('\n', 2)[0].Trim();
|
||||
var summary = firstLine.Length > 80 ? firstLine[..80] + "…" : firstLine;
|
||||
|
||||
// Try to identify which agent this came from
|
||||
var agent = "Iris";
|
||||
foreach (var marker in new[] { "**Agent:**", "**Agent:** ", "*Agent:* ", "Agent:" })
|
||||
{
|
||||
var idx = content.IndexOf(marker, StringComparison.OrdinalIgnoreCase);
|
||||
if (idx >= 0)
|
||||
{
|
||||
var after = content[(idx + marker.Length)..].TrimStart();
|
||||
var end = after.IndexOfAny(['\n', '\r', ',', '.']);
|
||||
var found = end > 0 ? after[..end].Trim() : after.Split('\n', 2)[0].Trim();
|
||||
if (!string.IsNullOrWhiteSpace(found) && found.Length < 30)
|
||||
{
|
||||
agent = found;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try to find agent name at the start in brackets like [Agent: Iris]
|
||||
if (agent == "Iris")
|
||||
{
|
||||
var bracketMatch = System.Text.RegularExpressions.Regex.Match(content, @"\[Agent:\s*([^\]]+)\]");
|
||||
if (bracketMatch.Success)
|
||||
agent = bracketMatch.Groups[1].Value.Trim();
|
||||
}
|
||||
|
||||
return (agent, summary);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,9 @@ public sealed record FeedEntry(
|
||||
string Agent,
|
||||
string Action,
|
||||
string Timestamp,
|
||||
string Time
|
||||
string Time,
|
||||
string? AgentId = null,
|
||||
string? Type = null
|
||||
);
|
||||
|
||||
public sealed record DashboardStatus(
|
||||
|
||||
@@ -459,6 +459,138 @@ public sealed class OpenClawGatewayClient(HttpClient httpClient, IConfiguration
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Collects assistant messages from ALL agent sessions (multi-agent operations feed).
|
||||
/// Merges, sorts by timestamp descending, and limits the result.
|
||||
/// Falls back to an empty list if any agent session is unreachable.
|
||||
/// </summary>
|
||||
public async Task<List<FeedEntry>> GetAllAgentOperationsAsync(int limit = 30)
|
||||
{
|
||||
var allEntries = new List<FeedEntry>();
|
||||
var agentIds = LoadAgentIdsFromConfig();
|
||||
|
||||
foreach (var agentId in agentIds)
|
||||
{
|
||||
try
|
||||
{
|
||||
var sessionKey = $"agent:{agentId}:main";
|
||||
var messages = await GetSessionHistoryAsync(sessionKey, Math.Min(limit * 2, 50));
|
||||
foreach (var msg in messages)
|
||||
{
|
||||
if (!string.Equals(msg.Role, "assistant", StringComparison.OrdinalIgnoreCase))
|
||||
continue;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(msg.Content))
|
||||
continue;
|
||||
|
||||
// Parse timestamp
|
||||
var ts = ParseTimestamp(msg.Timestamp);
|
||||
var timeAgo = FormatTimeAgo(ts);
|
||||
|
||||
// Extract a short agent indicator and action from content
|
||||
var (agent, action) = ExtractAgentAction(msg.Content);
|
||||
|
||||
// Determine event type based on content heuristics
|
||||
var eventType = DetectEventType(msg.Content);
|
||||
|
||||
allEntries.Add(new FeedEntry(
|
||||
agent,
|
||||
action,
|
||||
msg.Timestamp,
|
||||
timeAgo,
|
||||
AgentId: agentId,
|
||||
Type: eventType
|
||||
));
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Agent session unreachable — skip; we still have data from other agents
|
||||
}
|
||||
}
|
||||
|
||||
// Sort descending by timestamp, then limit
|
||||
return allEntries
|
||||
.OrderByDescending(e => ParseTimestamp(e.Timestamp))
|
||||
.Take(Math.Clamp(limit, 1, 100))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static DateTimeOffset ParseTimestamp(string timestamp)
|
||||
{
|
||||
if (DateTimeOffset.TryParse(timestamp, null, System.Globalization.DateTimeStyles.None, out var dt))
|
||||
return dt;
|
||||
return DateTimeOffset.UtcNow;
|
||||
}
|
||||
|
||||
private static string FormatTimeAgo(DateTimeOffset ts)
|
||||
{
|
||||
var diff = DateTimeOffset.UtcNow - ts;
|
||||
if (diff.TotalMinutes < 1) return "just now";
|
||||
if (diff.TotalMinutes < 60) return $"{(int)diff.TotalMinutes}m ago";
|
||||
if (diff.TotalHours < 24) return $"{(int)diff.TotalHours}h ago";
|
||||
if (diff.TotalDays < 7) return $"{(int)diff.TotalDays}d ago";
|
||||
return ts.ToString("MMM dd");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines a FeedEntry event type from message content heuristics.
|
||||
/// </summary>
|
||||
private static string DetectEventType(string content)
|
||||
{
|
||||
if (content.Contains("Subagent Task") || content.Contains("subagent"))
|
||||
{
|
||||
if (content.Contains("complete") || content.Contains("done") || content.Contains("finished"))
|
||||
return "task_complete";
|
||||
return "task_start";
|
||||
}
|
||||
if (content.Contains("Deploy") || content.Contains("deploy") || content.Contains("publish"))
|
||||
return "deploy";
|
||||
if (content.Contains("System") || content.Contains("system") || content.Contains("health"))
|
||||
return "system";
|
||||
if (content.Contains("Gestartet") || content.Contains("started") || content.Contains("Session"))
|
||||
return "session_start";
|
||||
return "chat";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extracts a human-readable agent name and action summary from message content.
|
||||
/// </summary>
|
||||
private static (string Agent, string Action) ExtractAgentAction(string content)
|
||||
{
|
||||
// Take first line or first ~80 chars as the action summary
|
||||
var firstLine = content.Split('\n', 2)[0].Trim();
|
||||
var summary = firstLine.Length > 80 ? firstLine[..80] + "\u2026" : firstLine;
|
||||
|
||||
// Try to identify which agent this came from
|
||||
var agent = "Iris";
|
||||
foreach (var marker in new[] { "**Agent:**", "**Agent:** ", "*Agent:* ", "Agent:" })
|
||||
{
|
||||
var idx = content.IndexOf(marker, StringComparison.OrdinalIgnoreCase);
|
||||
if (idx >= 0)
|
||||
{
|
||||
var after = content[(idx + marker.Length)..].TrimStart();
|
||||
var end = after.IndexOfAny(['\n', '\r', ',', '.']);
|
||||
var found = end > 0 ? after[..end].Trim() : after.Split('\n', 2)[0].Trim();
|
||||
if (!string.IsNullOrWhiteSpace(found) && found.Length < 30)
|
||||
{
|
||||
agent = found;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try to find agent name at the start in brackets like [Agent: Iris]
|
||||
if (agent == "Iris")
|
||||
{
|
||||
var bracketMatch = System.Text.RegularExpressions.Regex.Match(content, @"\[Agent:\s*([^\]]+)\]");
|
||||
if (bracketMatch.Success)
|
||||
agent = bracketMatch.Groups[1].Value.Trim();
|
||||
}
|
||||
|
||||
return (agent, summary);
|
||||
}
|
||||
|
||||
public async Task<ChatResponse> SendChatMessageAsync(string agentId, string message)
|
||||
{
|
||||
try
|
||||
|
||||
Reference in New Issue
Block a user