2026-06-10 01:16:37 +03:30
|
|
|
using Microsoft.AspNetCore.Builder;
|
|
|
|
|
using Microsoft.AspNetCore.Http;
|
|
|
|
|
using Microsoft.AspNetCore.Routing;
|
|
|
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
|
using TeamUp.Modules.Assembler.Domain;
|
|
|
|
|
using TeamUp.Modules.Assembler.Persistence;
|
2026-06-10 12:07:35 +03:30
|
|
|
using TeamUp.SharedKernel.Ai;
|
2026-06-10 01:16:37 +03:30
|
|
|
using TeamUp.SharedKernel.Modularity;
|
|
|
|
|
|
|
|
|
|
namespace TeamUp.Modules.Assembler.Endpoints;
|
|
|
|
|
|
|
|
|
|
internal static class AssemblerEndpoints
|
|
|
|
|
{
|
|
|
|
|
public static void Map(IEndpointRouteBuilder endpoints)
|
|
|
|
|
{
|
|
|
|
|
var group = endpoints.MapGroup("/api/assembler").WithTags("Assembler");
|
|
|
|
|
|
|
|
|
|
group.MapGet("/ping", () => TypedResults.Ok(new ModulePing("assembler")));
|
|
|
|
|
group.MapPost("/runs", CreateRun).RequireAuthorization();
|
|
|
|
|
group.MapGet("/runs/{id:guid}", GetRun).RequireAuthorization();
|
2026-06-15 15:21:10 +03:30
|
|
|
group.MapGet("/agent-activity", GetAgentActivity).RequireAuthorization();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// The live pulse behind each agent's face: the latest run status per agent. The client passes the
|
|
|
|
|
// ids of the AI seats it is showing (it already holds them) and composes the on-screen face state —
|
|
|
|
|
// this keeps the module boundary clean (Assembler owns runs; it never reaches into seats/teams).
|
|
|
|
|
private static async Task<IResult> GetAgentActivity(
|
|
|
|
|
string? agentIds, AssemblerDbContext db, CancellationToken ct)
|
|
|
|
|
{
|
|
|
|
|
var ids = (agentIds ?? string.Empty)
|
|
|
|
|
.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
|
|
|
|
.Select(s => Guid.TryParse(s, out var g) ? g : (Guid?)null)
|
|
|
|
|
.Where(g => g.HasValue)
|
|
|
|
|
.Select(g => g!.Value)
|
|
|
|
|
.Distinct()
|
|
|
|
|
.ToList();
|
|
|
|
|
|
|
|
|
|
if (ids.Count == 0)
|
|
|
|
|
{
|
|
|
|
|
return Results.Ok(Array.Empty<AgentActivityResponse>());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Latest run per agent. Project the few columns we need, then pick the newest per agent in
|
|
|
|
|
// memory — at dogfood scale this is a small set and avoids brittle GroupBy translation.
|
|
|
|
|
var runs = await db.AgentRuns
|
|
|
|
|
.Where(r => r.AgentId != null && ids.Contains(r.AgentId!.Value))
|
|
|
|
|
.Select(r => new
|
|
|
|
|
{
|
|
|
|
|
AgentId = r.AgentId!.Value,
|
|
|
|
|
r.Status,
|
|
|
|
|
r.WorkItemId,
|
|
|
|
|
r.CreatedAtUtc,
|
|
|
|
|
r.CompletedAtUtc,
|
|
|
|
|
})
|
|
|
|
|
.ToListAsync(ct);
|
|
|
|
|
|
|
|
|
|
var activity = runs
|
|
|
|
|
.GroupBy(r => r.AgentId)
|
|
|
|
|
.Select(g => g.OrderByDescending(r => r.CreatedAtUtc).First())
|
|
|
|
|
.Select(r => new AgentActivityResponse(
|
|
|
|
|
r.AgentId,
|
|
|
|
|
r.Status.ToString(),
|
|
|
|
|
r.WorkItemId,
|
|
|
|
|
r.CompletedAtUtc ?? r.CreatedAtUtc))
|
|
|
|
|
.ToList();
|
|
|
|
|
|
|
|
|
|
return Results.Ok(activity);
|
2026-06-10 01:16:37 +03:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Dispatch a task to an AI seat: record a queued AgentRun and enqueue the job. The worker
|
2026-06-10 12:07:35 +03:30
|
|
|
// drains it off the request path. Shares AgentRunDispatcher with the board triggers.
|
2026-06-10 01:16:37 +03:30
|
|
|
private static async Task<IResult> CreateRun(
|
2026-06-10 12:07:35 +03:30
|
|
|
CreateRunRequest request, IAgentDispatcher dispatcher, AssemblerDbContext db, CancellationToken ct)
|
2026-06-10 01:16:37 +03:30
|
|
|
{
|
2026-06-10 12:07:35 +03:30
|
|
|
var runId = await dispatcher.DispatchAsync(request.SeatId, request.WorkItemId, ct);
|
|
|
|
|
var run = await db.AgentRuns.FirstAsync(r => r.Id == runId, ct);
|
2026-06-10 01:16:37 +03:30
|
|
|
return Results.Ok(ToResponse(run));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static async Task<IResult> GetRun(Guid id, AssemblerDbContext db, CancellationToken ct)
|
|
|
|
|
{
|
|
|
|
|
var run = await db.AgentRuns.FirstOrDefaultAsync(r => r.Id == id, ct);
|
|
|
|
|
return run is null ? Results.NotFound() : Results.Ok(ToResponse(run));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static RunResponse ToResponse(AgentRun run) => new(
|
|
|
|
|
run.Id, run.SeatId, run.WorkItemId, run.AgentId, run.Status.ToString(),
|
|
|
|
|
run.ActionType, run.ActionRisk, run.Prompt, run.Output, run.Error);
|
|
|
|
|
}
|