Skip to content

Instantly share code, notes, and snippets.

@wmeints
Created October 22, 2024 04:48
Show Gist options
  • Save wmeints/0c20a1d30e77b9b487bb075210df97c2 to your computer and use it in GitHub Desktop.
Save wmeints/0c20a1d30e77b9b487bb075210df97c2 to your computer and use it in GitHub Desktop.

Technical architecture

This section covers the technical layout of the application and how to work with various parts of the code.

Working with use-cases

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.

Validating input to the use-cases

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.

Real-time communication

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.

Handling API requests

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.

Testing the components in the application

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.

Database access

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.

Language model interactions

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment