Files
nexus/backend/Services/AgentService.cs
T
bao eeb6174de0 Initial commit: Nexus Mission Control Platform
- ASP.NET Core 10 Backend (JWT Auth, Agent config API)
- Vue 3 Frontend (Dashboard, Team, Agents, Config Editor)
- PostgreSQL Database
- Docker Compose setup
- Mission Control Dashboard redesign
2026-06-09 16:31:56 +02:00

223 lines
7.3 KiB
C#

using System.Text.Json;
using System.Text.Json.Serialization;
using Nexus.Api.Domain;
using Nexus.Api.Integrations;
namespace Nexus.Api.Services;
public sealed record AgentConfig
{
[JsonPropertyName("id")]
public string Id { get; init; } = string.Empty;
[JsonPropertyName("name")]
public string Name { get; init; } = string.Empty;
[JsonPropertyName("workspace")]
public string? Workspace { get; init; }
[JsonPropertyName("agentDir")]
public string? AgentDir { get; init; }
[JsonPropertyName("model")]
public string? Model { get; init; }
[JsonPropertyName("identity")]
public AgentIdentityConfig? Identity { get; init; }
[JsonPropertyName("subagents")]
public SubAgentConfig? Subagents { get; init; }
}
public sealed record SubAgentConfig
{
[JsonPropertyName("allowAgents")]
public IReadOnlyList<string>? AllowAgents { get; init; }
}
public sealed record AgentIdentityConfig
{
[JsonPropertyName("name")]
public string Name { get; init; } = string.Empty;
[JsonPropertyName("theme")]
public string Theme { get; init; } = string.Empty;
}
public sealed record AgentInfo(
string Id,
string Name,
string Role,
string Model,
OperationalStatus Status,
DateTimeOffset? LastSeen,
string? Workspace,
string? Description
);
public sealed record AgentDetail(
string Id,
string Name,
string Role,
string Model,
OperationalStatus Status,
DateTimeOffset? LastSeen,
string? Workspace,
string? AgentDir,
string? Description,
IReadOnlyList<string>? SubAgents,
string? IdentityName
);
public interface IAgentService
{
Task<IReadOnlyCollection<AgentInfo>> GetAgentsAsync(CancellationToken cancellationToken);
Task<AgentDetail?> GetAgentAsync(string id, CancellationToken cancellationToken);
}
public sealed class AgentService(IConfiguration configuration, IAgentRuntime runtime) : IAgentService
{
private static readonly JsonSerializerOptions JsonOptions = new()
{
PropertyNameCaseInsensitive = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
public async Task<IReadOnlyCollection<AgentInfo>> GetAgentsAsync(CancellationToken cancellationToken)
{
var configs = await LoadAgentConfigsAsync(cancellationToken);
var runtimeStatus = await runtime.GetStatusAsync(cancellationToken);
var overallOperational = runtimeStatus.Status;
var now = DateTimeOffset.UtcNow;
var agents = new List<AgentInfo>(configs.Count);
foreach (var config in configs)
{
var model = config.Model ?? "deepseek/deepseek-v4-flash";
var role = DeriveRole(config.Id);
var description = config.Identity?.Theme ?? string.Empty;
// main agent doesn't have a separate identity; set a generic description
if (string.IsNullOrEmpty(description))
{
description = config.Id switch
{
"main" => "Primary conversational agent — routing and general-purpose chat",
_ => description
};
}
agents.Add(new AgentInfo(
Id: config.Id,
Name: config.Identity?.Name ?? config.Name ?? config.Id,
Role: role,
Model: model,
Status: overallOperational,
LastSeen: now,
Workspace: config.Workspace,
Description: description
));
}
return agents.AsReadOnly();
}
public async Task<AgentDetail?> GetAgentAsync(string id, CancellationToken cancellationToken)
{
var configs = await LoadAgentConfigsAsync(cancellationToken);
var config = configs.FirstOrDefault(a =>
a.Id.Equals(id, StringComparison.OrdinalIgnoreCase));
if (config is null) return null;
var runtimeStatus = await runtime.GetStatusAsync(cancellationToken);
var now = DateTimeOffset.UtcNow;
var role = DeriveRole(config.Id);
var description = config.Identity?.Theme ?? string.Empty;
if (string.IsNullOrEmpty(description) && config.Id == "main")
description = "Primary conversational agent — routing and general-purpose chat";
return new AgentDetail(
Id: config.Id,
Name: config.Identity?.Name ?? config.Name ?? config.Id,
Role: role,
Model: config.Model ?? "deepseek/deepseek-v4-flash",
Status: runtimeStatus.Status,
LastSeen: now,
Workspace: config.Workspace,
AgentDir: config.AgentDir,
Description: description,
SubAgents: config.Subagents?.AllowAgents,
IdentityName: config.Identity?.Name
);
}
private static string DeriveRole(string agentId) => agentId.ToLowerInvariant() switch
{
"iris" => "Orchestrator",
"programmer" => "Developer",
"reviewer" => "Reviewer",
"architekt" => "Architect",
"main" => "Assistant",
_ => "Custom"
};
private async Task<IReadOnlyList<AgentConfig>> LoadAgentConfigsAsync(CancellationToken cancellationToken)
{
var path = configuration.GetValue<string>("AgentConfigPath")
?? "/home/node/.openclaw/openclaw.json";
if (!File.Exists(path))
return Array.Empty<AgentConfig>();
var json = await File.ReadAllTextAsync(path, cancellationToken);
using var document = JsonDocument.Parse(json, new JsonDocumentOptions { AllowTrailingCommas = true });
var root = document.RootElement;
if (!root.TryGetProperty("agents", out var agentsElement))
return Array.Empty<AgentConfig>();
if (!agentsElement.TryGetProperty("list", out var listElement))
return Array.Empty<AgentConfig>();
var defaults = agentsElement.TryGetProperty("defaults", out var defaultsElement)
? JsonSerializer.Deserialize<AgentDefaults>(defaultsElement.GetRawText(), JsonOptions)
: null;
var configs = new List<AgentConfig>();
foreach (var agentElement in listElement.EnumerateArray())
{
var config = JsonSerializer.Deserialize<AgentConfig>(agentElement.GetRawText(), JsonOptions);
if (config is null || string.IsNullOrWhiteSpace(config.Id))
continue;
// Inherit defaults for missing fields
if (string.IsNullOrWhiteSpace(config.Name))
config = config with { Name = config.Id };
if (string.IsNullOrWhiteSpace(config.Model) && defaults?.Model?.Primary is not null)
config = config with { Model = defaults.Model.Primary };
if (string.IsNullOrWhiteSpace(config.Workspace) && defaults?.Workspace is not null)
config = config with { Workspace = defaults.Workspace };
configs.Add(config);
}
return configs.AsReadOnly();
}
private sealed record AgentDefaults
{
[JsonPropertyName("workspace")]
public string? Workspace { get; init; }
[JsonPropertyName("model")]
public AgentDefaultModel? Model { get; init; }
}
private sealed record AgentDefaultModel
{
[JsonPropertyName("primary")]
public string? Primary { get; init; }
}
}