This section covers the technical layout of the application and how to work with various parts of the code.
This project follows vertical slice architecture. Each use-case gets its own class under the feature folder. The associated request and response objects are contained as subclass within the use-case class. The use-case class has the name of the use case, for example:
- Creating a new conversation is implemented in Conversations/CreateConversation.cs
- Sending a message is implemented in Conversations/SendMessage.cs
- Uploading attachments is implemented in Attachments/UploadAttachment.cs
Use cases are implemented as a MediatR IRequestHandler
. The associated request object
must implement IRequest<T>
when sending a response or IRequest
when no response is needed.
Each use-case request must be validated. We use FluentValidation. You are only allowed the use the information in the request object to validate the contents. If you need to validate something that involves a database then this is part of the logic in the use case.
We use real-time communication when handling prompts coming from the user. This makes the application more responsive. To enable communicating the response as a stream we use SignalR.
All other communication around attachments, content, starting conversations, or removing conversations from the history should be done through minimal API endpoints in ASP.NET Core.
The frontend is implemented as a single page application that talks to either the SignalR hub
or the minimal API endpoints. The minimal API endpoints must send requests through the IMediator
object.
The whole request lifecycle happens inside the request handlers. This makes debugging and testing the code easier.
We require unit-tests written in XUnit in the InfoSupport.Agents.Ricardo.Tests
project.
The directory structure for the test project mirrors the structure of the agent project. This makes it easier
for us to find the test cases later.
We need tests that validate individual components and testss that combine every piece of the request lifecycle in the request handlers of the application.
You can run tests with dotnet test
from the root of the repository.
We use FakeItEasy
for mocking dependencies in the application. You should use mocks for unit-tests to isolate
the unit under test. For integration tests you should only use mocks for dependencies that fall outside the application scope.
We use Entity Framework Core to store information in a Postgresql database. We use the PGVector extension to allow for finding information using cosine distance measures between embedding vectors.
The database context is stored in Data/ApplicationDbContext.cs
. You should model the entities for the
various features in the corresponding folder. For example:
- Conversation is stored in the
Conversations/Models
folder. - Attachment is stored in the
Attachments/Models
folder.
We use Semantic Kernel to interact with GPT-4o on Azure OpenAI service. We don't have a dedicated class to place all the logic used to interact with GPT-4o. Instead, we create a new kernel instance with the necessary prompts and plugins in the use case request handlers.
By moving the kernel interactions into the use case handlers we are fully transparent about where the language model is used. It allows us to remain flexible in what the language model is allowed to use.