Skip to content

C4 Level 3 — Components

Goal: zoom into the FastAPI service container and show the major software components (the hexagonal layers), their dependencies, and the ports they implement.

Diagram

Hold "Alt" / "Option" to enable pan & zoom
flowchart TB
    subgraph COMPOSITION["composition/ (composition root)"]
        APP["api_app.create_app<br/>wires adapters to ports<br/>(composition root)"]
        FACTORY["production_factory<br/>select_embedder<br/>select_retriever<br/>select_answerer"]
        OBS["observability.init_tracing<br/>OTel SDK init"]
        CFG["settings.Settings<br/>(pydantic-settings)"]
        IM["ingestion_main<br/>(separate entrypoint,<br/>same image)"]
    end

    subgraph APPLICATION["application/ (use cases)"]
        ROUTES["api.routes<br/>POST /ask, GET /healthz,<br/>GET /metrics"]
        MID["api.middleware<br/>RequestIdMiddleware<br/>(FIRST in chain)"]
        ERR["api.error_mapper<br/>typed exceptions -><br/>HTTP status codes"]
        METRICS["api.metrics_middleware<br/>Prometheus exporter"]
        SERVICE["answering.answering_service<br/>AnsweringService"]
        GRAPH["answering.graph<br/>LangGraph StateGraph<br/>(4 nodes + 1 edge)"]
        ISERVICE["ingestion.ingestion_service<br/>IngestionService"]
        ILOCK["ingestion.ingestion_lock<br/>IngestionRunLock"]
        VAL["ingestion.pre_embed_validator<br/>(validation gate)"]
    end

    subgraph ADAPTERS["adapters/ (concrete implementations)"]
        SCRAPER["http_source<br/>RequestsPageScraper"]
        CLEANER["cleaner<br/>BoilerplatePageCleaner"]
        EXTRACT["bs4_text_extractor<br/>(pre-processor)"]
        ANALYZER["llm_page_analyzer<br/>OpenAIPageAnalyzer<br/>(WP06)"]
        HCHUNKER["hybrid_chunker<br/>HybridChunker<br/>(WP06)"]
        FCHUNKER["chunker<br/>FixedSizeChunker<br/>(fallback)"]
        OPENAI_EMB["embedding_openai<br/>OpenAIEmbedder<br/>(default)"]
        LOCAL_EMB["embedding_local<br/>SentenceTransformersEmbedder<br/>(fallback)"]
        LEX["lexical_rerank_retriever<br/>(Dutch verb-form fix)"]
        CHR_VS["vectorstore_chroma<br/>ChromaVectorStore + ChromaRetriever"]
        ANSWERER["answerer_openai<br/>OpenAIAnswerGenerator"]
        POLICY["low_confidence_policy<br/>ThresholdLowConfidencePolicy"]
        SCRUB["secret_scrubber<br/>SecretScrubber"]
        METRICS_AD["metrics<br/>MetricsRecorder"]
    end

    subgraph DOMAIN["domain/ (pure business types + ports)"]
        D_ENT_A["answering.entities<br/>AgentState, RetrievedChunk,<br/>Confidence"]
        D_PORTS_A["answering.ports<br/>Retriever, AnswerGenerator,<br/>LowConfidencePolicy"]
        D_ENT_I["ingestion.entities<br/>SourcePage, Chunk,<br/>PageStructure"]
        D_PORTS_I["ingestion.ports<br/>PageScraper, PageCleaner,<br/>Chunker, Embedder,<br/>VectorStore, IngestionRunLock"]
        D_ERR["shared.errors<br/>VectorStoreUnavailable,<br/>LLMUnavailable, ...<br/>(DomainError hierarchy)"]
    end

    ROUTES --> MID --> METRICS --> SERVICE
    ROUTES --> ERR
    SERVICE --> GRAPH
    GRAPH -.uses.-> D_PORTS_A
    GRAPH --> ANSWERER
    GRAPH --> LEX
    LEX --> CHR_VS
    GRAPH --> POLICY
    ROUTES --> METRICS_AD

    IM --> ISERVICE
    ISERVICE --> ILOCK
    ISERVICE --> SCRAPER
    ISERVICE --> CLEANER
    ISERVICE --> ANALYZER
    ISERVICE --> HCHUNKER
    ISERVICE --> FCHUNKER
    ISERVICE --> OPENAI_EMB
    ISERVICE --> LOCAL_EMB
    ISERVICE --> CHR_VS
    ISERVICE --> VAL
    ISERVICE -.uses.-> D_PORTS_I

    APP --> FACTORY
    FACTORY --> D_PORTS_A
    FACTORY --> D_PORTS_I
    APP --> OBS
    APP --> CFG

    ANALYZER --> D_ENT_I
    ANSWERER --> D_ERR
    CHR_VS --> D_ERR
    POLICY --> D_ERR

Layer rules (enforced by import-linter)

composition   →   adapters   →   application   →   domain
  (top)                                                    (bottom, pure)
Layer May import Must NOT import
domain/ stdlib only application/, adapters/, composition/, langchain, langgraph, chromadb, fastapi, requests, beautifulsoup4, httpx, pydantic-settings, structlog, opentelemetry
application/ domain/ adapters/, composition/, the SDKs listed above
adapters/ domain/, application/, SDKs composition/
composition/ all layers

pydantic-settings is banned from domain/ (it's a framework import). pydantic v2 is fine (BaseModel, field validators).

The contract is enforced by tests/architecture/test_imports.py (CI gate). A signature change in a port without an updated adapter fails the build.

Component inventory

composition/ — composition root (3 files)

File Purpose
composition/api_app.py Builds the FastAPI app; wires every adapter to its port via the factory.
composition/ingestion_main.py CLI entrypoint for the ingestion Job. Same image, different CMD.
composition/production_factory.py Adapter-selection functions: select_embedder, select_query_embedder, select_retriever, select_answerer.
composition/observability.py OTel SDK init, auto-instrumentation, span lifecycle.
composition/settings.py BaseSettings (pydantic-settings); the single source of operator-tunable values.
composition/__main__.py python -m support_bot.composition dispatcher.

application/ — use cases (8 files)

File Purpose
application/api/routes.py POST /ask, GET /healthz, GET /metrics route handlers.
application/api/middleware.py RequestIdMiddleware (first in chain).
application/api/error_mapper.py Maps DomainError subclasses to HTTP status codes.
application/api/metrics_middleware.py Prometheus exporter middleware (after RequestIdMiddleware so it sees the request id).
application/answering/answering_service.py AnsweringService.answer(question, *, request_id).
application/answering/graph.py The LangGraph topology (4 nodes + 1 conditional edge).
application/ingestion/ingestion_service.py IngestionService.run(source_url, *, request_id).
application/ingestion/ingestion_lock.py IngestionRunLock (TTL-keyed file lock).
application/ingestion/pre_embed_validator.py Validation gate between clean and embed steps.

adapters/ — concrete implementations (16 files)

File Purpose
http_source.py RequestsPageScraper (requests-based HTTP fetcher).
cleaner.py BoilerplatePageCleaner (strips nav/footer/cookie/etc.).
bs4_text_extractor.py Bs4TextExtractor (HTML → plain text pre-processor).
llm_page_analyzer.py OpenAIPageAnalyzer (WP06: chat-completions with strict JSON-schema response_format).
hybrid_chunker.py HybridChunker (WP06: per-region chunking + sentence-boundary split).
chunker.py FixedSizeChunker (WP03 fallback).
embedding_openai.py OpenAIEmbedder (default; supports Matryoshka dimensions).
embedding_local.py SentenceTransformersEmbedder (fallback; supports E5 prefix).
lexical_rerank_retriever.py LexicalRerankRetriever (4-char stem-prefix re-ranker).
vectorstore_chroma.py ChromaVectorStore + ChromaRetriever.
answerer_openai.py OpenAIAnswerGenerator (ChatOpenAI with "context-only" system prompt).
low_confidence_policy.py ThresholdLowConfidencePolicy (default threshold 0.5).
secret_scrubber.py SecretScrubber (regex: sk-…, sk-ant-…, configurable substrings).
metrics.py MetricsRecorder (Prometheus registry builder).
structured_logger.py configure_logging (structlog JSON renderer).

domain/ — pure business types + ports (5 files)

File Purpose
domain/answering/entities.py AgentState, RetrievedChunk, Confidence literal.
domain/answering/ports.py Retriever, AnswerGenerator, LowConfidencePolicy (Protocols).
domain/ingestion/entities.py SourcePage, CleanedPage, Chunk, PageStructure, SemanticChunk, ContentKind.
domain/ingestion/ports.py PageScraper, PageCleaner, PageAnalyzer, Chunker, Embedder, VectorStore, IngestionRunLock, PreEmbedValidator (Protocols).
domain/shared/errors.py DomainError hierarchy: VectorStoreUnavailable, LLMUnavailable, EmbedderUnavailable, SourcePageUnreachable, SourcePageGarbage, ConfigurationError.
domain/shared/retrieval.py RetrievedChunk (with from_chunk classmethod + _clamp_similarity validator).

Where to look next