Common utilities and components for backend services developed by the Data Competence Center Basel-Stadt.
dcc-backend-common is a Python library that provides shared functionality for backend services, including:
- LLM Agent: Abstract base class for pydantic-ai agents with streaming, postprocessing, and thinking control
- FastAPI Health Probes: Kubernetes-ready health check endpoints (liveness, readiness, startup)
- Structured Logging: Integration with
structlogfor consistent logging across services - Configuration Management: Environment-based configuration with
python-dotenv
uv add dcc-backend-commonuv add "dcc-backend-common[pydantic_ai]"uv add "dcc-backend-common[fastapi]"uv add "dcc-backend-common[pydantic_ai,fastapi]"- Python 3.12 or higher
- Core dependencies:
pydantic>=2.12.5,python-dotenv,structlog>=25.5.0
pydantic_aiextras:pydantic-ai>=1.103.0,pydantic-ai-slim[openai]>=1.103.0fastapiextras:aiohttp>=3.13.3,fastapi>=0.136.0,<1.0
BaseAgent is an abstract base class for building pydantic-ai agents with built-in streaming, postprocessing, and thinking mode control. It targets OpenAI-compatible endpoints (e.g. vLLM serving Gemma).
from pydantic_ai import Agent
from pydantic_ai.models import Model
from dcc_backend_common.config.app_config import LlmConfig
from dcc_backend_common.llm_agent import BaseAgent
class MyAgent(BaseAgent[None, str]):
def create_agent(self, model: Model) -> Agent[None, str]:
return Agent(
model=model,
system_prompt="You are a helpful assistant.",
)
config = LlmConfig(
llm_model="gemma-3-27b-it",
llm_url="https://your-vllm-endpoint/v1",
llm_api_key="your-key",
)
agent = MyAgent(config)
result = await agent.run("What is the capital of Switzerland?")Pass enable_thinking=True to enable extended reasoning. The agent uses chat_template_kwargs: {enable_thinking: bool} via the OpenAI extra_body parameter — no prompt modification required.
agent_with_thinking = MyAgent(config, enable_thinking=True)
result = await agent_with_thinking.run("Solve this step by step: ...")
agent_no_thinking = MyAgent(config, enable_thinking=False) # default# Stream text deltas
async for chunk in agent.run_stream_text("Tell me a story"):
print(chunk, end="", flush=True)
# Stream full accumulated text (delta=False)
async for text in agent.run_stream_text("Tell me a story", delta=False):
print(text)
# Stream structured output chunks
async for chunk in agent.run_stream_output("List three cities"):
print(chunk)
# Stream raw pydantic-ai events
async for event in agent.run_stream_events("Hello"):
print(event)from pydantic import BaseModel
class CityList(BaseModel):
cities: list[str]
country: str
class CityAgent(BaseAgent[None, CityList]):
def create_agent(self, model: Model) -> Agent[None, CityList]:
return Agent(model=model, output_type=CityList)
agent = CityAgent(config, output_type=CityList)
result = await agent.run("List three Swiss cities")
# result.cities == ["Zurich", "Basel", "Bern"]Use stream_list to yield list items one by one as they are generated:
class ItemAgent(BaseAgent[None, str]):
def create_agent(self, model: Model) -> Agent[None, str]:
return Agent(model=model)
agent = ItemAgent(config)
async for item in agent.stream_list("Name five fruits"):
print(item) # prints each fruit as soon as it is readyAll output passes through a postprocessing pipeline automatically:
replace_eszett: Replacesßwithssin all string fields (including nested Pydantic models, dicts, and lists)trim_text: Strips leading whitespace from text output (first chunk only in streaming)
Custom postprocessors can be added by overriding _get_postprocessors():
class MyAgent(BaseAgent[None, str]):
def _get_postprocessors(self):
return super()._get_postprocessors() + [my_custom_processor]Prompt transformation (e.g. injecting context) can be done by overriding process_prompt():
class MyAgent(BaseAgent[None, str]):
def process_prompt(self, prompt, deps):
return f"[context] {prompt}"from dcc_backend_common.llm_agent.debugging import withDebbugger
class MyAgent(BaseAgent[None, str]):
@withDebbugger
async def run(self, *args, **kwargs):
return await super().run(*args, **kwargs)Or inject an event stream handler directly:
from dcc_backend_common.llm_agent.debugging import create_event_debugger
async for event in agent.run_stream_events(
"Hello",
event_stream_handler=create_event_debugger("my-agent"),
):
...Kubernetes-ready health check endpoints that follow best practices for container orchestration.
from fastapi import FastAPI
from dcc_backend_common.fastapi_health_probes import health_probe_router
app = FastAPI()
service_dependencies = [
{
"name": "database",
"health_check_url": "http://postgres:5432/health",
"api_key": None,
},
{
"name": "external-api",
"health_check_url": "https://api.example.com/health",
"api_key": "your-api-key-here",
},
]
app.include_router(health_probe_router(service_dependencies))| Endpoint | Purpose | Kubernetes action on failure |
|---|---|---|
GET /health/liveness |
Process is alive and not deadlocked | Container is restarted |
GET /health/readiness |
App is ready to handle requests | Traffic is stopped to this pod |
GET /health/startup |
App has finished initialization | Liveness/readiness probes are blocked |
Liveness — returns uptime in seconds. Keep it simple; do not check external deps here.
Readiness — checks all configured service dependencies:
{
"status": "ready",
"checks": {
"database": "healthy",
"external-api": "healthy"
}
}Startup — returns startup timestamp. Useful for apps that load large ML models on boot.
from dcc_backend_common.logger import init_logger, get_logger
init_logger() # JSON in production (IS_PROD=true), colored console otherwise
logger = get_logger(__name__)
logger.info("request_received", extra={"user_id": 42})A request_id and timestamp are added automatically to every log entry.
Load strongly-typed configuration from environment variables:
from dcc_backend_common.config.app_config import AppConfig, LlmConfig
config = AppConfig.from_env()
print(config) # secrets are redacted in __str__
llm = LlmConfig(
llm_model="gemma-3-27b-it",
llm_url="http://vllm:8000/v1",
llm_api_key="your-key",
)AppConfig.from_env() reads CLIENT_URL, HMAC_SECRET, OPENAI_API_KEY, LLM_URL, DOCLING_URL, WHISPER_URL, OCR_URL from the environment. Missing required values raise AppConfigError.
git clone https://github.com/DCC-BS/backend-common.git
cd backend-common
uv sync --group dev --all-extrasuv run pytest tests/unit/Integration tests require a real LLM endpoint:
LLM_URL=... LLM_API_KEY=... LLM_MODEL=... uv run pytest tests/integration/ -m integrationmake check # lock check + pre-commit + ty type check
uv run pytest tests/unit # unit testsThis project uses GitHub Actions for automated releases to PyPI.
- Update the
versionfield inpyproject.toml. - Commit and push to
main. - In GitHub Actions, run the Publish to PyPI workflow manually.
The workflow detects the version, creates a git tag, builds the package, and publishes via Trusted Publishing.
See CONTRIBUTING.md for details.
MIT — see LICENSE.
- Data Competence Center Basel-Stadt — dcc@bs.ch
- Tobias Bollinger — tobias.bollinger@bs.ch
- Yanick Schraner — yanick.schraner@bs.ch
- Homepage: https://DCC-BS.github.io/backend-common/
- Repository: https://github.com/DCC-BS/backend-common
- Documentation: https://DCC-BS.github.io/backend-common/