PrivateGPT Dependency Injection Architecture: How Components Are Wired Together
PrivateGPT uses the Python injector library to manage runtime objects through a three-layer architecture: a root injector for global configuration, per-request injector binding for FastAPI routes, and a component graph of singleton services that auto-wire via constructor injection.
The dependency injection architecture in PrivateGPT eliminates tight coupling between the FastAPI server, LLM backends, vector stores, and embedding models. By leveraging the injector library, the codebase centralizes configuration management in Settings while allowing components to declare their dependencies through decorators. This design enables seamless swapping of AI providers (OpenAI, Ollama, LlamaCPP) without modifying business logic.
The Three Layers of PrivateGPT's Dependency Injection Architecture
Root Injector and Global Configuration
The foundation of the system resides in private_gpt/di.py. The create_application_injector() function instantiates a single Injector with auto_bind=True, which allows the container to automatically resolve concrete types without explicit binding rules.
# private_gpt/di.py
from injector import Injector
from private_gpt.settings.settings import Settings, unsafe_typed_settings
def create_application_injector() -> Injector:
_injector = Injector(auto_bind=True)
_injector.binder.bind(Settings, to=unsafe_typed_settings)
return _injector
global_injector: Injector = create_application_injector()
The Settings object—parsed from settings.yaml—is bound as a singleton, ensuring all components share the same configuration instance. This global injector lives for the entire process lifetime.
Per-Request Injector Binding
To make the dependency injection architecture available within FastAPI routes, PrivateGPT binds the root injector to each request's state. This occurs in private_gpt/launcher.py through the bind_injector_to_request dependency.
# private_gpt/launcher.py
from fastapi import FastAPI, Request, Depends
from injector import Injector
def create_app(root_injector: Injector) -> FastAPI:
async def bind_injector_to_request(request: Request) -> None:
request.state.injector = root_injector
app = FastAPI(dependencies=[Depends(bind_injector_to_request)])
return app
This pattern allows route handlers to resolve services via request.state.injector.get(ServiceClass), maintaining loose coupling between HTTP layer and business logic.
Component Graph and Service Singletons
The final layer consists of concrete service classes decorated with @singleton and @inject. These classes declare their dependencies through constructor parameters, which the injector resolves automatically from the component graph.
For example, LLMComponent in private_gpt/components/llm/llm_component.py receives the global Settings instance:
# private_gpt/components/llm/llm_component.py
from injector import singleton, inject
from private_gpt.settings.settings import Settings
@singleton
class LLMComponent:
@inject
def __init__(self, settings: Settings) -> None:
self.llm = self._build_llm(settings.llm)
Similarly, VectorStoreComponent in private_gpt/components/vector_store/vector_store_component.py selects the appropriate backend (Postgres, Chroma, Qdrant) based on injected settings.
How Components Are Declared and Wired
PrivateGPT's dependency injection architecture relies on two key decorators from the injector library:
@singleton: Ensures only one instance exists per injector (process lifetime)@inject: Marks the constructor for automatic dependency resolution
When a class like ChatService requires multiple dependencies, it simply lists them in __init__:
# private_gpt/server/chat/chat_service.py
from injector import singleton, inject
@singleton
class ChatService:
@inject
def __init__(
self,
llm_component: LLMComponent,
vector_store_component: VectorStoreComponent,
embedding_component: EmbeddingComponent,
node_store_component: NodeStoreComponent,
) -> None:
self.llm_component = llm_component
self.vector_store_component = vector_store_component
self.embedding_component = embedding_component
self.node_store_component = node_store_component
The injector automatically resolves the four component arguments by looking up existing singletons or creating them if necessary. This eliminates manual factory code and ensures consistent initialization order.
Accessing Services in FastAPI Routes
Route handlers in PrivateGPT never instantiate services directly. Instead, they retrieve them from the request-bound injector:
# private_gpt/server/chat/chat_router.py
from fastapi import APIRouter, Request
from private_gpt.server.chat.chat_service import ChatService
chat_router = APIRouter()
@chat_router.post("/chat/completions")
def chat_completion(request: Request, body: ChatBody):
service = request.state.injector.get(ChatService)
# Use service...
This pattern maintains strict separation between the HTTP transport layer and the domain logic. Testing becomes straightforward because ChatService can be mocked and injected without modifying route code.
Extending the Architecture with Custom Components
Adding a new service to PrivateGPT's dependency injection architecture requires no changes to di.py or launcher.py thanks to auto_bind=True.
Step 1: Create the class with decorators:
# my_component.py
from injector import singleton, inject
from private_gpt.settings.settings import Settings
@singleton
class MyComponent:
@inject
def __init__(self, settings: Settings) -> None:
self.config_value = settings.my_section.value
Step 2: Reference it in existing services:
class ExistingService:
@inject
def __init__(self, my_component: MyComponent) -> None:
self.my_component = my_component
The injector automatically discovers and wires MyComponent when resolving ExistingService.
Summary
- PrivateGPT uses the
injectorlibrary to implement a three-layer dependency injection architecture consisting of a root injector, per-request bindings, and a component graph. - The root injector in
private_gpt/di.pyinitializes withauto_bind=Trueand binds the globalSettingssingleton. - Per-request injection in
private_gpt/launcher.pyattaches the injector torequest.state, making it available to all FastAPI route handlers. - Components are declared as
@singletonclasses with@injectconstructors, automatically receiving dependencies likeSettings,LLMComponent, orVectorStoreComponent. - API routes retrieve services via
request.state.injector.get(ServiceClass), maintaining loose coupling between HTTP and business logic. - New components require only the
@singletonand@injectdecorators to integrate into the existing graph, thanks to automatic binding.
Frequently Asked Questions
What dependency injection library does PrivateGPT use?
PrivateGPT uses the injector library, a Python implementation of the Guice dependency injection framework. This library provides the @inject and @singleton decorators used throughout the codebase to manage object lifecycles and wiring.
How does PrivateGPT handle configuration injection?
Configuration is injected via a singleton Settings object bound in private_gpt/di.py. The create_application_injector() function binds the parsed YAML configuration (unsafe_typed_settings) to the Settings type. All components receive this configuration through constructor injection (settings: Settings), ensuring consistent access to application settings without global imports.
Can I use the global injector outside of FastAPI requests?
Yes. The global_injector instance exported from private_gpt/di.py can be used in scripts, background workers, or the Gradio UI. For example, private_gpt/ui/ui.py retrieves the PrivateGptUi class via global_injector.get(PrivateGptUi). This allows non-HTTP entry points to participate in the same dependency injection architecture as the API server.
How do I add a new service to PrivateGPT's dependency injection architecture?
To add a new service, create a class decorated with @singleton and @inject, then declare its dependencies in the constructor. Thanks to auto_bind=True in the root injector, no registration in di.py is required. Simply reference the new service as a parameter in existing components, and the injector will automatically resolve and wire it into the component graph.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →