Architecture
groundworkers is organised so that configuration, domain logic, and transport
concerns stay separate. The same service layer is reused across MCP, REST, and
direct Python integrations.
Composition
The startup path is:
- shared OMOP stack config is loaded
bootstrap.pyresolves it into one runtimeAppConfigbuild_application(...)constructs a reusableGroundworkersApp- transports reuse that application container
Transport entry points
Different callers enter the same runtime in different ways:
Most transport-facing workflows call services. MCP also exposes a small number of intentionally adapter-shaped capabilities, such as embedding and system status surfaces, where introducing a service layer would add indirection without adding domain value.
Dependency flow
Services coordinate reusable domain logic. Adapters isolate concrete dependencies.
What each layer owns
Shared stack configuration
The source of truth is the shared OMOP stack configuration loaded through
oa-configurator.
omop-alchemyowns the shared CDM resourceomop-graphowns graph-specific package settingsomop-embowns embedding-store and embedding-model settingsgroundworkersowns transport defaults, LLM-backed worker behavior, source-planning settings, and knowledge-pack settings
groundworkers does not maintain a second YAML-era runtime model.
bootstrap.py
bootstrap.py resolves the active stack config into the runtime AppConfig.
That includes:
- selecting the active stack file and profile
- resolving the shared CDM resource and engine
- loading sibling package config (
omop_graph,omop_emb) - loading
groundworkerspackage-owned settings - resolving optional knowledge-pack roots
If you need to change how configuration is resolved, this is the layer to edit.
app.py
build_application(config) is the composition root. It constructs:
- adapters from already-resolved concrete handles
- services from those adapters
- a
GroundworkersAppcontainer that transports can reuse
This keeps the rest of the codebase free of config-file and profile-selection knowledge.
adapters/
Adapters are dependency-facing wrappers. Each adapter should wrap one external system cleanly:
CDMAdapterwraps the SQLAlchemy engine/session factoryOmopGraphAdapterwraps the omop-graph backend runtimeOmopEmbAdapterwraps omop-emb index and query behaviorLLMAdapterwraps the configured model backend
Adapters are intentionally config-agnostic. They should accept already-built handles or explicit constructor values, not TOML sections or loader logic.
services/
Services contain reusable domain logic that should work the same regardless of transport:
VocabServicefor lexical retrieval and OMOP navigationGraphServicefor deterministic graph-backed lookup, traversal, paths, and neighborhood explorationConceptGroundingServicefor caller-facing grounding policy over the graph serviceMappingServicefor multi-channel candidate and context workflowsTextServicefor LLM-backed text preprocessingDomainServicefor LLM-backed structured-field domain hintsSourcePlanningServicefor stateless source-planning pipelines
If the logic is something a Python caller would reasonably want without going through MCP, it probably belongs in a service.
Some services also depend on other services or optional adapters as part of the assembled runtime:
ConceptGroundingServicedepends onGraphServiceMappingServicedepends onVocabServiceand can also use graph, embedding, and grounding capabilities when availableSourcePlanningServiceis always present and can be LLM-assisted when that adapter is configured
Transport layers
groundworkers exposes two transport styles over the same runtime:
- MCP via the tool modules in
tools/ - REST via
transports/rest/
The transport layers should stay thin:
- validate or clamp request inputs
- call a service
- call an adapter directly only for intentionally adapter-shaped primitives that do not have a service abstraction
- translate exceptions into transport-appropriate error responses
Business logic should not exist only in MCP wrappers or only in REST routes.
Which layer should a caller use?
| Need | Call |
|---|---|
| Tool discovery, agent interoperability, remote service | MCP tools |
| Fixed HTTP workflow endpoints | REST API |
| Domain workflows from Python | app.services.* |
| Backend-shaped primitives for a specific dependency | app.adapters.* |
Typical request flow
For adapter-backed MCP primitives, the T->>S and S->>A steps collapse into a
direct transport-to-adapter call by design.
Design rules for contributors
- Put resource and profile resolution in
bootstrap.py, not in adapters. - Keep adapters dependency-shaped and reusable.
- Keep services transport-agnostic.
- Add MCP tools only when the capability should participate in tool discovery.
- Add REST endpoints only for curated workflow operations, not every internal method.
The extension guide in Extending groundworkers spells out the expected shape for new adapters, services, MCP tools, and REST routes.