Skip to content

Commit

Permalink
Add agent, service, and API key CRUD operations
Browse files Browse the repository at this point in the history
Introduced CRUD functionalities for agent, service, and API key entities. Added endpoint handlers, filtering capabilities, and memory/EF core stores to support these operations. Updated configuration mappings as well.
  • Loading branch information
sfmskywalker committed Aug 18, 2024
1 parent 740569d commit cb637e3
Show file tree
Hide file tree
Showing 29 changed files with 476 additions and 18 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@
});

builder.Services.AddControllers();
builder.Services.Configure<AgentsOptions>(options => builder.Configuration.GetSection("SemanticKernel").Bind(options));
builder.Services.Configure<AgentsOptions>(options => builder.Configuration.GetSection("Agents").Bind(options));
builder.Services.Configure<WebhookSourcesOptions>(options => builder.Configuration.GetSection("Webhooks").Bind(options));
builder.Services.Configure<OrchardCoreOptions>(options => builder.Configuration.GetSection("OrchardCore").Bind(options));
builder.Services.Configure<OrchardCoreClientOptions>(options => builder.Configuration.GetSection("OrchardCore:Client").Bind(options));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@
"BlogPost"
]
},
"SemanticKernel": {
"Agents": {
"ApiKeys": [
{
"Name": "gpt-4o",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
using System.ComponentModel.DataAnnotations;

namespace Elsa.Agents.Api.Endpoints.ApiKeys.Create;

public class Request
{
public string Name { get; set; } = default!;
public string Value { get; set; }= default!;
[Required] public string Name { get; set; } = default!;
[Required] public string Value { get; set; }= default!;
}
31 changes: 31 additions & 0 deletions src/modules/Elsa.Agents.Api/Endpoints/ApiKeys/Delete/Endpoint.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using Elsa.Abstractions;
using Elsa.Agents.Persistence.Contracts;
using JetBrains.Annotations;

namespace Elsa.Agents.Api.Endpoints.ApiKeys.Delete;

/// Delete an API key.
[UsedImplicitly]
public class Endpoint(IApiKeyStore store) : ElsaEndpoint<Request>
{
/// <inheritdoc />
public override void Configure()
{
Delete("/ai/api-keys/{id}");
ConfigurePermissions("ai/api-keys:delete");
}

/// <inheritdoc />
public override async Task HandleAsync(Request req, CancellationToken ct)
{
var entity = await store.GetAsync(req.Id, ct);

if(entity == null)
{
await SendNotFoundAsync(ct);
return;
}

await store.DeleteAsync(entity, ct);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using System.ComponentModel.DataAnnotations;

namespace Elsa.Agents.Api.Endpoints.ApiKeys.Delete;

public class Request
{
[Required] public string Id { get; set; } = default!;
}
33 changes: 33 additions & 0 deletions src/modules/Elsa.Agents.Api/Endpoints/ApiKeys/Get/Endpoint.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using Elsa.Abstractions;
using Elsa.Agents.Persistence.Contracts;
using Elsa.Agents.Persistence.Entities;
using Elsa.Models;
using JetBrains.Annotations;

namespace Elsa.Agents.Api.Endpoints.ApiKeys.Get;

/// Lists all registered API keys.
[UsedImplicitly]
public class Endpoint(IApiKeyStore store) : ElsaEndpoint<Request, ApiKeyDefinition>
{
/// <inheritdoc />
public override void Configure()
{
Get("/ai/api-keys/{id}");
ConfigurePermissions("ai/api-keys:read");
}

/// <inheritdoc />
public override async Task<ApiKeyDefinition> ExecuteAsync(Request req, CancellationToken ct)
{
var entity = await store.GetAsync(req.Id, ct);

if(entity == null)
{
await SendNotFoundAsync(ct);
return null!;
}

return entity;
}
}
8 changes: 8 additions & 0 deletions src/modules/Elsa.Agents.Api/Endpoints/ApiKeys/Get/Request.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using System.ComponentModel.DataAnnotations;

namespace Elsa.Agents.Api.Endpoints.ApiKeys.Get;

public class Request
{
[Required] public string Id { get; set; } = default!;
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public class Endpoint(IApiKeyStore store) : ElsaEndpoint<Request, ApiKeyDefiniti
/// <inheritdoc />
public override void Configure()
{
Post("/ai/agents/{id}");
Post("/ai/api-keys/{id}");
ConfigurePermissions("ai/api-keys:write");
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
using System.ComponentModel.DataAnnotations;

namespace Elsa.Agents.Api.Endpoints.ApiKeys.Update;

public class Request
{
public string Id { get; set; } = default!;
public string Name { get; set; } = default!;
public string Value { get; set; }= default!;
[Required] public string Id { get; set; } = default!;
[Required] public string Name { get; set; } = default!;
[Required] public string Value { get; set; }= default!;
}
48 changes: 48 additions & 0 deletions src/modules/Elsa.Agents.Api/Endpoints/Services/Create/Endpoint.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
using Elsa.Abstractions;
using Elsa.Agents.Persistence.Contracts;
using Elsa.Agents.Persistence.Entities;
using Elsa.Agents.Persistence.Filters;
using Elsa.Workflows.Contracts;
using JetBrains.Annotations;

namespace Elsa.Agents.Api.Endpoints.Services.Create;

/// Lists all registered API keys.
[UsedImplicitly]
public class Endpoint(IServiceStore store, IIdentityGenerator identityGenerator) : ElsaEndpoint<Request, ServiceDefinition>
{
/// <inheritdoc />
public override void Configure()
{
Post("/ai/services");
ConfigurePermissions("ai/services:write");
}

/// <inheritdoc />
public override async Task<ServiceDefinition> ExecuteAsync(Request req, CancellationToken ct)
{
var existingEntityFilter = new ServiceDefinitionFilter
{
Name = req.Name
};
var existingEntity = await store.FindAsync(existingEntityFilter, ct);

if (existingEntity != null)
{
AddError("A Service already exists with the specified name");
await SendErrorsAsync(cancellation: ct);
return existingEntity;
}

var newEntity = new ServiceDefinition
{
Id = identityGenerator.GenerateId(),
Name = req.Name.Trim(),
Type = req.Type,
Settings = req.Settings
};

await store.AddAsync(newEntity, ct);
return newEntity;
}
}
10 changes: 10 additions & 0 deletions src/modules/Elsa.Agents.Api/Endpoints/Services/Create/Request.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using System.ComponentModel.DataAnnotations;

namespace Elsa.Agents.Api.Endpoints.Services.Create;

public class Request
{
[Required] public string Name { get; set; } = default!;
[Required] public string Type { get; set; } = default!;
public IDictionary<string, object> Settings { get; set; } = new Dictionary<string, object>();
}
31 changes: 31 additions & 0 deletions src/modules/Elsa.Agents.Api/Endpoints/Services/Delete/Endpoint.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using Elsa.Abstractions;
using Elsa.Agents.Persistence.Contracts;
using JetBrains.Annotations;

namespace Elsa.Agents.Api.Endpoints.Services.Delete;

/// Delete an API key.
[UsedImplicitly]
public class Endpoint(IServiceStore store) : ElsaEndpoint<Request>
{
/// <inheritdoc />
public override void Configure()
{
Delete("/ai/services/{id}");
ConfigurePermissions("ai/services:delete");
}

/// <inheritdoc />
public override async Task HandleAsync(Request req, CancellationToken ct)
{
var entity = await store.GetAsync(req.Id, ct);

if(entity == null)
{
await SendNotFoundAsync(ct);
return;
}

await store.DeleteAsync(entity, ct);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using System.ComponentModel.DataAnnotations;

namespace Elsa.Agents.Api.Endpoints.Services.Delete;

public class Request
{
[Required] public string Id { get; set; } = default!;
}
32 changes: 32 additions & 0 deletions src/modules/Elsa.Agents.Api/Endpoints/Services/Get/Endpoint.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using Elsa.Abstractions;
using Elsa.Agents.Persistence.Contracts;
using Elsa.Agents.Persistence.Entities;
using JetBrains.Annotations;

namespace Elsa.Agents.Api.Endpoints.Services.Get;

/// Lists all registered API keys.
[UsedImplicitly]
public class Endpoint(IServiceStore store) : ElsaEndpoint<Request, ServiceDefinition>
{
/// <inheritdoc />
public override void Configure()
{
Get("/ai/services/{id}");
ConfigurePermissions("ai/services:read");
}

/// <inheritdoc />
public override async Task<ServiceDefinition> ExecuteAsync(Request req, CancellationToken ct)
{
var entity = await store.GetAsync(req.Id, ct);

if(entity == null)
{
await SendNotFoundAsync(ct);
return null!;
}

return entity;
}
}
8 changes: 8 additions & 0 deletions src/modules/Elsa.Agents.Api/Endpoints/Services/Get/Request.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using System.ComponentModel.DataAnnotations;

namespace Elsa.Agents.Api.Endpoints.Services.Get;

public class Request
{
[Required] public string Id { get; set; } = default!;
}
26 changes: 26 additions & 0 deletions src/modules/Elsa.Agents.Api/Endpoints/Services/List/Endpoint.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using Elsa.Abstractions;
using Elsa.Agents.Persistence.Contracts;
using Elsa.Agents.Persistence.Entities;
using Elsa.Models;
using JetBrains.Annotations;

namespace Elsa.Agents.Api.Endpoints.Services.List;

/// Lists all registered API keys.
[UsedImplicitly]
public class Endpoint(IServiceStore store) : ElsaEndpointWithoutRequest<ListResponse<ServiceDefinition>>
{
/// <inheritdoc />
public override void Configure()
{
Get("/ai/services");
ConfigurePermissions("ai/services:read");
}

/// <inheritdoc />
public override async Task<ListResponse<ServiceDefinition>> ExecuteAsync(CancellationToken ct)
{
var entities = await store.ListAsync(ct);
return new ListResponse<ServiceDefinition>(entities.ToList());
}
}
59 changes: 59 additions & 0 deletions src/modules/Elsa.Agents.Api/Endpoints/Services/Update/Endpoint.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
using Elsa.Abstractions;
using Elsa.Agents.Persistence.Contracts;
using Elsa.Agents.Persistence.Entities;
using Elsa.Agents.Persistence.Filters;
using JetBrains.Annotations;

namespace Elsa.Agents.Api.Endpoints.Services.Update;

/// Lists all registered API keys.
[UsedImplicitly]
public class Endpoint(IServiceStore store) : ElsaEndpoint<Request, ServiceDefinition>
{
/// <inheritdoc />
public override void Configure()
{
Post("/ai/services/{id}");
ConfigurePermissions("ai/services:write");
}

/// <inheritdoc />
public override async Task<ServiceDefinition> ExecuteAsync(Request req, CancellationToken ct)
{
var entity = await store.GetAsync(req.Id, ct);

if(entity == null)
{
await SendNotFoundAsync(ct);
return null!;
}

var isNameDuplicate = await IsNameDuplicateAsync(req.Name, req.Id, ct);

if (isNameDuplicate)
{
AddError("Another service already exists with the specified name");
await SendErrorsAsync(cancellation: ct);
return entity;
}

entity.Name = req.Name.Trim();
entity.Type = req.Type.Trim();
entity.Settings = req.Settings;

await store.UpdateAsync(entity, ct);
return entity;
}

private async Task<bool> IsNameDuplicateAsync(string name, string id, CancellationToken cancellationToken)
{
var filter = new ServiceDefinitionFilter
{
Name = name,
NotId = id
};

var entity = await store.FindAsync(filter, cancellationToken);
return entity != null;
}
}
11 changes: 11 additions & 0 deletions src/modules/Elsa.Agents.Api/Endpoints/Services/Update/Request.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using System.ComponentModel.DataAnnotations;

namespace Elsa.Agents.Api.Endpoints.Services.Update;

public class Request
{
[Required] public string Id { get; set; } = default!;
[Required] public string Name { get; set; } = default!;
[Required] public string Type { get; set; } = default!;
public IDictionary<string, object> Settings { get; set; } = new Dictionary<string, object>();
}
Loading

0 comments on commit cb637e3

Please sign in to comment.