Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
- **External Integrations:** Use `src/Core.Application/Common/` and `src/Infrastructure.AgentFramework/` for connectors.
- **RBAC & Security:** Enforced in API layer, see `ConfigureServicesAuth.cs`.
- **Auth-Triggered User Provisioning:** Keep the flow UI-tied (not middleware/pipeline) because OBO token acquisition requires user context on the main UI thread/circuit.
- **Not-Found Integration Scenarios:** When adding not-found integration scenarios for commands, use non-empty IDs for missing entities so tests validate not-found behavior instead of bad-request validation for empty IDs.

## UI Design Guidelines
- **ChatPage Layout:** Implement distinct desktop and mobile layouts with a strict row/column structure on mobile.
Expand Down
4 changes: 2 additions & 2 deletions src/Core.Application/Abstractions/IActorsTool.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@

public interface IActorsTool
{
Task<IActorResponse> GetActorByIdAsync(Guid actorId, CancellationToken cancellationToken);
Task<IActorResponse?> GetActorByIdAsync(Guid actorId, CancellationToken cancellationToken);
Task<ICollection<IActorResponse>> GetActorsByNameAsync(string name, CancellationToken cancellationToken);
}
}
4 changes: 2 additions & 2 deletions src/Core.Application/Abstractions/IChatSessionsTool.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@
public interface IChatSessionsTool
{
Task<IEnumerable<string>> ListRecentSessionsAsync(DateTime? startDate, DateTime? endDate, CancellationToken cancellationToken);
Task<string> UpdateChatSessionTitleAsync(Guid sessionId, string newTitle, CancellationToken cancellationToken);
}
Task<string?> UpdateChatSessionTitleAsync(Guid sessionId, string newTitle, CancellationToken cancellationToken);
}
19 changes: 0 additions & 19 deletions src/Core.Application/Actor/ActorGuard.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
using Goodtocode.AgentFramework.Core.Domain.Actor;
using Goodtocode.AgentFramework.Core.Domain.Chat;

namespace Goodtocode.AgentFramework.Core.Application.Actor;

public static class ActorGuard
Expand All @@ -13,22 +10,6 @@ public static void GuardAgainstEmptyUserContext(IUserContext? userContext)
]);
}

public static void GuardAgainstNotFound(ActorEntity? actor)
{
if (actor == null)
{
throw new CustomNotFoundException("Actor Not Found");
}
}

public static void GuardAgainstNotFound(ChatSessionEntity? entity)
{
if (entity is null)
{
throw new CustomNotFoundException("Chat Session Not Found");
}
}

public static void GuardAgainstInvalidUserContext(IUserContext? userContext)
{
if (userContext is null)
Expand Down
17 changes: 11 additions & 6 deletions src/Core.Application/Actor/DeleteOurActorByOwnerIdCommand.cs
Original file line number Diff line number Diff line change
@@ -1,21 +1,26 @@
namespace Goodtocode.AgentFramework.Core.Application.Actor;

public class DeleteActorByOwnerIdCommand : UserScopedRequest, IRequest
public class DeleteActorByOwnerIdCommand : UserScopedRequest, IRequest<CommandResult>
{
public Guid OwnerId { get; set; }

}

public class DeleteActorByOwnerIdCommandHandler(IAgentFrameworkContext context) : IRequestHandler<DeleteActorByOwnerIdCommand>
public class DeleteActorByOwnerIdCommandHandler(IAgentFrameworkContext context) : IRequestHandler<DeleteActorByOwnerIdCommand, CommandResult>
{
private readonly IAgentFrameworkContext _context = context;

public async Task Handle(DeleteActorByOwnerIdCommand request, CancellationToken cancellationToken)
public async Task<CommandResult> Handle(DeleteActorByOwnerIdCommand request, CancellationToken cancellationToken)
{
var actor = await _context.Actors.Where(x => x.OwnerId == request.OwnerId).FirstOrDefaultAsync(cancellationToken);
ActorGuard.GuardAgainstNotFound(actor);
if (actor is null)
{
return CommandResult.NotFound();
}

_context.Actors.Remove(actor!);
_context.Actors.Remove(actor);
await _context.SaveChangesAsync(cancellationToken);

return CommandResult.Success();
}
}
}
19 changes: 12 additions & 7 deletions src/Core.Application/Actor/DeleteOurActorCommand.cs
Original file line number Diff line number Diff line change
@@ -1,21 +1,26 @@
namespace Goodtocode.AgentFramework.Core.Application.Actor;

public class DeleteOurActorCommand : UserScopedRequest, IRequest
public class DeleteOurActorCommand : UserScopedRequest, IRequest<CommandResult>
{
public Guid Id { get; set; }

}

public class DeleteActorCommandHandler(IAgentFrameworkContext context) : IRequestHandler<DeleteOurActorCommand>
public class DeleteActorCommandHandler(IAgentFrameworkContext context) : IRequestHandler<DeleteOurActorCommand, CommandResult>
{
private readonly IAgentFrameworkContext _context = context;

public async Task Handle(DeleteOurActorCommand request, CancellationToken cancellationToken)
public async Task<CommandResult> Handle(DeleteOurActorCommand request, CancellationToken cancellationToken)
{
var Actor = _context.Actors.Find(request.Id);
ActorGuard.GuardAgainstNotFound(Actor);
var actor = await _context.Actors.FindAsync([request.Id, cancellationToken], cancellationToken: cancellationToken);
if (actor is null)
{
return CommandResult.NotFound();
}

_context.Actors.Remove(Actor!);
_context.Actors.Remove(actor);
await _context.SaveChangesAsync(cancellationToken);

return CommandResult.Success();
}
}
}
11 changes: 5 additions & 6 deletions src/Core.Application/Actor/GetMyActorQuery.cs
Original file line number Diff line number Diff line change
@@ -1,21 +1,20 @@
namespace Goodtocode.AgentFramework.Core.Application.Actor;

public class GetMyActorQuery : UserScopedRequest, IRequest<ActorDto>
public class GetMyActorQuery : UserScopedRequest, IRequest<ActorDto?>
{
public Guid OwnerId { get; set; }

}

public class GetActorByOwnerIdQueryHandler(IAgentFrameworkContext context) : IRequestHandler<GetMyActorQuery, ActorDto>
public class GetActorByOwnerIdQueryHandler(IAgentFrameworkContext context) : IRequestHandler<GetMyActorQuery, ActorDto?>
{
private readonly IAgentFrameworkContext _context = context;

public async Task<ActorDto> Handle(GetMyActorQuery request, CancellationToken cancellationToken)
public async Task<ActorDto?> Handle(GetMyActorQuery request, CancellationToken cancellationToken)
{
var actor = await _context.Actors
.FirstOrDefaultAsync(x => x.OwnerId == request.UserContext.OwnerId && x.TenantId == request.UserContext.TenantId, cancellationToken: cancellationToken);
ActorGuard.GuardAgainstNotFound(actor);

return ActorDto.CreateFrom(actor);
return actor is null ? null : ActorDto.CreateFrom(actor);
}
}
}
11 changes: 5 additions & 6 deletions src/Core.Application/Actor/GetOurActorChatSessionQuery.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,22 @@

namespace Goodtocode.AgentFramework.Core.Application.Actor;

public class GetOurActorChatSessionQuery : UserScopedRequest, IRequest<ChatSessionDto>
public class GetOurActorChatSessionQuery : UserScopedRequest, IRequest<ChatSessionDto?>
{
public Guid ActorId { get; set; }
public Guid ChatSessionId { get; set; }

}

public class GetOurActorChatSessionQueryHandler(IAgentFrameworkContext context) : IRequestHandler<GetOurActorChatSessionQuery, ChatSessionDto>
public class GetOurActorChatSessionQueryHandler(IAgentFrameworkContext context) : IRequestHandler<GetOurActorChatSessionQuery, ChatSessionDto?>
{
private readonly IAgentFrameworkContext _context = context;

public async Task<ChatSessionDto> Handle(GetOurActorChatSessionQuery request, CancellationToken cancellationToken)
public async Task<ChatSessionDto?> Handle(GetOurActorChatSessionQuery request, CancellationToken cancellationToken)
{
var returnData = await _context.ChatSessions
.FirstOrDefaultAsync(x => x.Id == request.ChatSessionId && x.ActorId == request.ActorId && x.TenantId == request.UserContext.TenantId, cancellationToken: cancellationToken);
ActorGuard.GuardAgainstNotFound(returnData);

return ChatSessionDto.CreateFrom(returnData);
return returnData is null ? null : ChatSessionDto.CreateFrom(returnData);
}
}
}
11 changes: 5 additions & 6 deletions src/Core.Application/Actor/GetOurActorQuery.cs
Original file line number Diff line number Diff line change
@@ -1,20 +1,19 @@
namespace Goodtocode.AgentFramework.Core.Application.Actor;

public class GetOurActorQuery : UserScopedRequest, IRequest<ActorDto>
public class GetOurActorQuery : UserScopedRequest, IRequest<ActorDto?>
{
public Guid ActorId { get; set; }
}

public class GetActorQueryHandler(IAgentFrameworkContext context) : IRequestHandler<GetOurActorQuery, ActorDto>
public class GetActorQueryHandler(IAgentFrameworkContext context) : IRequestHandler<GetOurActorQuery, ActorDto?>
{
private readonly IAgentFrameworkContext _context = context;

public async Task<ActorDto> Handle(GetOurActorQuery request, CancellationToken cancellationToken)
public async Task<ActorDto?> Handle(GetOurActorQuery request, CancellationToken cancellationToken)
{
var actor = await _context.Actors
.FirstOrDefaultAsync(x => x.Id == request.ActorId && x.TenantId == request.UserContext.TenantId, cancellationToken: cancellationToken);
ActorGuard.GuardAgainstNotFound(actor);

return ActorDto.CreateFrom(actor);
return actor is null ? null : ActorDto.CreateFrom(actor);
}
}
}
12 changes: 0 additions & 12 deletions src/Core.Application/Chat/ChatGuard.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,18 +49,6 @@ public static void GuardAgainstNullAgentResponse(object? response)
]);
}

public static void GuardAgainstNotFound(ChatSessionEntity? chatSession)
{
if (chatSession == null)
throw new CustomNotFoundException("Chat Session not found.");
}

public static void GuardAgainstNotFound(ChatMessageEntity? chatMessage)
{
if (chatMessage == null)
throw new CustomNotFoundException("Chat Message Not Found");
}

public static void GuardAgainstUnauthorized(ChatMessageEntity chatMessage, IUserContext userInfo)
{
if (chatMessage.ChatSession?.OwnerId != userInfo.OwnerId)
Expand Down
18 changes: 11 additions & 7 deletions src/Core.Application/Chat/CreateMyChatMessageCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,30 +4,34 @@

namespace Goodtocode.AgentFramework.Core.Application.Chat;

public class CreateMyChatMessageCommand : UserScopedRequest, IRequest<ChatMessageDto>
public class CreateMyChatMessageCommand : UserScopedRequest, IRequest<CommandResult<ChatMessageDto>>
{
public Guid ChatSessionId { get; set; }
public string? Message { get; set; }

}

public class CreateChatMessageCommandHandler(AIAgent agent, IAgentFrameworkContext context) : IRequestHandler<CreateMyChatMessageCommand, ChatMessageDto>
public class CreateChatMessageCommandHandler(AIAgent agent, IAgentFrameworkContext context) : IRequestHandler<CreateMyChatMessageCommand, CommandResult<ChatMessageDto>>
{
private readonly AIAgent _agent = agent;
private readonly IAgentFrameworkContext _context = context;

public async Task<ChatMessageDto> Handle(CreateMyChatMessageCommand request, CancellationToken cancellationToken)
public async Task<CommandResult<ChatMessageDto>> Handle(CreateMyChatMessageCommand request, CancellationToken cancellationToken)
{
ChatGuard.GuardAgainstEmptyMessage(request?.Message);
ChatGuard.GuardAgainstEmptyUser(request?.UserContext);

var chatSession = await _context.ChatSessions
.FirstOrDefaultAsync(x => x.Id == request!.ChatSessionId && x.OwnerId == request.UserContext.OwnerId && x.TenantId == request.UserContext.TenantId, cancellationToken);
ChatGuard.GuardAgainstNotFound(chatSession);
ChatGuard.GuardAgainstUnauthorized(chatSession!, request!.UserContext!);
if (chatSession is null)
{
return CommandResult<ChatMessageDto>.NotFound();
}

ChatGuard.GuardAgainstUnauthorized(chatSession, request!.UserContext!);

var chatHistory = new List<ChatMessage>();
foreach (ChatMessageEntity message in chatSession!.Messages)
foreach (ChatMessageEntity message in chatSession.Messages)
{
chatHistory.Add(new ChatMessage(
role: message.Role == ChatMessageRole.user ? ChatRole.User : ChatRole.Assistant,
Expand Down Expand Up @@ -64,6 +68,6 @@ public async Task<ChatMessageDto> Handle(CreateMyChatMessageCommand request, Can

await _context.SaveChangesAsync(cancellationToken);

return ChatMessageDto.CreateFrom(chatMessage);
return CommandResult<ChatMessageDto>.Success(ChatMessageDto.CreateFrom(chatMessage));
}
}
22 changes: 14 additions & 8 deletions src/Core.Application/Chat/DeleteMyChatSessionCommand.cs
Original file line number Diff line number Diff line change
@@ -1,24 +1,30 @@
namespace Goodtocode.AgentFramework.Core.Application.Chat;

public class DeleteMyChatSessionCommand : UserScopedRequest, IRequest
public class DeleteMyChatSessionCommand : UserScopedRequest, IRequest<CommandResult>
{
public Guid Id { get; set; }

}

public class DeleteMyChatSessionCommandHandler(IAgentFrameworkContext context) : IRequestHandler<DeleteMyChatSessionCommand>
public class DeleteMyChatSessionCommandHandler(IAgentFrameworkContext context) : IRequestHandler<DeleteMyChatSessionCommand, CommandResult>
{
private readonly IAgentFrameworkContext _context = context;

public async Task Handle(DeleteMyChatSessionCommand request, CancellationToken cancellationToken)
public async Task<CommandResult> Handle(DeleteMyChatSessionCommand request, CancellationToken cancellationToken)
{
ChatGuard.GuardAgainstEmptyUser(request?.UserContext);

var chatSession = _context.ChatSessions.Find(request!.Id);
ChatGuard.GuardAgainstNotFound(chatSession);
ChatGuard.GuardAgainstUnauthorized(chatSession!, request.UserContext!);
var chatSession = await _context.ChatSessions.FindAsync([request!.Id, cancellationToken], cancellationToken: cancellationToken);
if (chatSession is null)
{
return CommandResult.NotFound();
}

_context.ChatSessions.Remove(chatSession!);
ChatGuard.GuardAgainstUnauthorized(chatSession, request.UserContext!);

_context.ChatSessions.Remove(chatSession);
await _context.SaveChangesAsync(cancellationToken);

return CommandResult.Success();
}
}
}
16 changes: 10 additions & 6 deletions src/Core.Application/Chat/GetMyChatMessageQuery.cs
Original file line number Diff line number Diff line change
@@ -1,24 +1,28 @@
namespace Goodtocode.AgentFramework.Core.Application.Chat;

public class GetMyChatMessageQuery : UserScopedRequest, IRequest<ChatMessageDto>
public class GetMyChatMessageQuery : UserScopedRequest, IRequest<ChatMessageDto?>
{
public Guid Id { get; set; }

}

public class GetMyChatMessageQueryHandler(IAgentFrameworkContext context) : IRequestHandler<GetMyChatMessageQuery, ChatMessageDto>
public class GetMyChatMessageQueryHandler(IAgentFrameworkContext context) : IRequestHandler<GetMyChatMessageQuery, ChatMessageDto?>
{
private readonly IAgentFrameworkContext _context = context;

public async Task<ChatMessageDto> Handle(GetMyChatMessageQuery request,
public async Task<ChatMessageDto?> Handle(GetMyChatMessageQuery request,
CancellationToken cancellationToken)
{
ChatGuard.GuardAgainstEmptyUser(request?.UserContext);

var chatMessage = await _context.ChatMessages.FindAsync([request!.Id, cancellationToken], cancellationToken: cancellationToken);
ChatGuard.GuardAgainstNotFound(chatMessage);
ChatGuard.GuardAgainstUnauthorized(chatMessage!, request.UserContext!);
if (chatMessage is null)
{
return null;
}

ChatGuard.GuardAgainstUnauthorized(chatMessage, request.UserContext!);

return ChatMessageDto.CreateFrom(chatMessage);
}
}
}
17 changes: 11 additions & 6 deletions src/Core.Application/Chat/GetMyChatSessionQuery.cs
Original file line number Diff line number Diff line change
@@ -1,25 +1,30 @@
namespace Goodtocode.AgentFramework.Core.Application.Chat;

public class GetMyChatSessionQuery : UserScopedRequest, IRequest<ChatSessionDto>
public class GetMyChatSessionQuery : UserScopedRequest, IRequest<ChatSessionDto?>
{
public Guid Id { get; set; }

}

public class GetMyChatSessionQueryHandler(IAgentFrameworkContext context) : IRequestHandler<GetMyChatSessionQuery, ChatSessionDto>
public class GetMyChatSessionQueryHandler(IAgentFrameworkContext context) : IRequestHandler<GetMyChatSessionQuery, ChatSessionDto?>
{
private readonly IAgentFrameworkContext _context = context;

public async Task<ChatSessionDto> Handle(GetMyChatSessionQuery request, CancellationToken cancellationToken)
public async Task<ChatSessionDto?> Handle(GetMyChatSessionQuery request, CancellationToken cancellationToken)
{
ChatGuard.GuardAgainstEmptyUserForQuery(request?.UserContext);
ChatGuard.GuardAgainstEmptyId(request?.Id);

var chatSession = await _context.ChatSessions
.FirstOrDefaultAsync(x => x.Id == request!.Id && x.OwnerId == request.UserContext.OwnerId && x.TenantId == request.UserContext.TenantId, cancellationToken: cancellationToken);
ChatGuard.GuardAgainstNotFound(chatSession);
ChatGuard.GuardAgainstUnauthorized(chatSession!, request!.UserContext!);

if (chatSession is null)
{
return null;
}

ChatGuard.GuardAgainstUnauthorized(chatSession, request!.UserContext!);

return ChatSessionDto.CreateFrom(chatSession);
}
}
}
Loading
Loading