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 injector library 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.py initializes with auto_bind=True and binds the global Settings singleton.
  • Per-request injection in private_gpt/launcher.py attaches the injector to request.state, making it available to all FastAPI route handlers.
  • Components are declared as @singleton classes with @inject constructors, automatically receiving dependencies like Settings, LLMComponent, or VectorStoreComponent.
  • API routes retrieve services via request.state.injector.get(ServiceClass), maintaining loose coupling between HTTP and business logic.
  • New components require only the @singleton and @inject decorators 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →