From 5c7198316ba56d45e07872d2b5be5b6e4983538d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leynier=20Guti=C3=A9rrez=20Gonz=C3=A1lez?= Date: Sat, 8 Aug 2026 17:47:20 -0600 Subject: [PATCH] feat: add ML training and serving stacks --- catalog/components.yml | 66 ++++---- catalog/generated/catalog.json | 146 +++++++++++++----- copier.yml | 8 +- docs/reference/components.md | 4 +- scripts/compile_catalog.py | 4 +- template/_catalog_dependencies.jinja | 6 +- template/pyproject.toml.jinja | 14 +- ...or quality_tools %}ops.py{% endif %}.jinja | 38 +++++ ...time_config %}settings.py{% endif %}.jinja | 9 +- ...ng != 'none' %}serving.py{% endif %}.jinja | 31 ++++ ..., 'hybrid'] %}training.py{% endif %}.jinja | 60 +++++++ ...ality_tools %}test_ops.py{% endif %}.jinja | 9 ++ ... 'none' %}test_serving.py{% endif %}.jinja | 8 + ...brid'] %}test_training.py{% endif %}.jinja | 9 ++ ...ime_config %}.env.example{% endif %}.jinja | 6 +- .../service/pyproject.toml.jinja | 16 ++ .../__init__.py.jinja | 5 + .../training/pyproject.toml.jinja | 16 ++ .../__init__.py.jinja | 5 + tests/test_ai_presets.py | 82 ++++++++++ 20 files changed, 465 insertions(+), 77 deletions(-) create mode 100644 template/src/{{ module_name }}/{% if mlops_tools or quality_tools %}ops.py{% endif %}.jinja create mode 100644 template/src/{{ module_name }}/{% if serving != 'none' %}serving.py{% endif %}.jinja create mode 100644 template/src/{{ module_name }}/{% if workload in ['training', 'hybrid'] %}training.py{% endif %}.jinja create mode 100644 template/tests/{% if mlops_tools or quality_tools %}test_ops.py{% endif %}.jinja create mode 100644 template/tests/{% if serving != 'none' %}test_serving.py{% endif %}.jinja create mode 100644 template/tests/{% if workload in ['training', 'hybrid'] %}test_training.py{% endif %}.jinja create mode 100644 template/{% if workload == 'hybrid' %}packages{% endif %}/service/pyproject.toml.jinja create mode 100644 template/{% if workload == 'hybrid' %}packages{% endif %}/service/src/{{ module_name }}_service/__init__.py.jinja create mode 100644 template/{% if workload == 'hybrid' %}packages{% endif %}/training/pyproject.toml.jinja create mode 100644 template/{% if workload == 'hybrid' %}packages{% endif %}/training/src/{{ module_name }}_training/__init__.py.jinja diff --git a/catalog/components.yml b/catalog/components.yml index 96d8e75..ff580a6 100644 --- a/catalog/components.yml +++ b/catalog/components.yml @@ -199,36 +199,42 @@ components: tier: stable workloads: [training, hybrid] python: ">=3.11,<3.15" + packages: ["torch>=2.13.0,<3"] - id: transformers name: Hugging Face Transformers layer: framework tier: stable workloads: [training, hybrid] python: ">=3.11,<3.15" + packages: ["transformers>=5.14.1,<6", "torch>=2.13.0,<3"] - id: jax name: JAX layer: framework tier: stable workloads: [training, hybrid] python: ">=3.11,<3.15" + packages: ["jax>=0.11.0,<0.12"] - id: keras name: Keras / TensorFlow layer: framework tier: stable workloads: [training, hybrid] - python: ">=3.11,<3.15" + python: ">=3.11,<3.14" + packages: ["keras>=3.15.1,<4", "tensorflow>=2.21.0,<3"] - id: scikit-learn name: scikit-learn layer: framework tier: stable workloads: [training, hybrid] python: ">=3.11,<3.15" + packages: ["scikit-learn>=1.9.0,<2"] - id: xgboost name: XGBoost layer: framework tier: stable workloads: [training, hybrid] python: ">=3.11,<3.15" + packages: ["xgboost>=3.4.0,<4"] # Interfaces are composable and can expose the same application through # multiple entrypoints. @@ -284,37 +290,37 @@ components: - {id: oidc, name: OAuth / OIDC, layer: auth, tier: platform, python: ">=3.11,<3.15", packages: ["authlib>=1.7.2,<2"]} - {id: supabase-auth, name: Supabase Auth, layer: auth, tier: platform, python: ">=3.11,<3.15", packages: ["supabase>=2.31.0,<3"]} - - {id: bentoml, name: BentoML, layer: serving, tier: stable, python: ">=3.11,<3.15"} - - {id: litellm, name: LiteLLM, layer: serving, tier: stable, python: ">=3.11,<3.15"} - - {id: vllm, name: vLLM, layer: serving, tier: platform, python: ">=3.11,<3.15"} - - {id: ollama-serving, name: Ollama, layer: serving, tier: stable, python: ">=3.11,<3.15"} - - {id: ray-serve, name: Ray Serve, layer: serving, tier: stable, python: ">=3.11,<3.15"} + - {id: bentoml, name: BentoML, layer: serving, tier: stable, python: ">=3.11,<3.15", packages: ["bentoml>=1.4.39,<2"]} + - {id: litellm, name: LiteLLM, layer: serving, tier: stable, python: ">=3.11,<3.15", packages: ["litellm>=1.95.0,<2"]} + - {id: vllm, name: vLLM, layer: serving, tier: platform, python: ">=3.11,<3.14", packages: ["vllm>=0.26.0,<0.27"]} + - {id: ollama-serving, name: Ollama, layer: serving, tier: stable, python: ">=3.11,<3.15", packages: ["ollama>=0.6.2,<1"]} + - {id: ray-serve, name: Ray Serve, layer: serving, tier: stable, python: ">=3.11,<3.15", packages: ["ray[serve]>=2.56.1,<3"]} - - {id: lightning, name: Lightning, layer: training_extension, tier: stable, python: ">=3.11,<3.15"} - - {id: datasets, name: Hugging Face Datasets, layer: training_extension, tier: stable, python: ">=3.11,<3.15"} - - {id: accelerate, name: Accelerate, layer: training_extension, tier: stable, python: ">=3.11,<3.15"} - - {id: peft, name: PEFT, layer: training_extension, tier: stable, python: ">=3.11,<3.15"} - - {id: trl, name: TRL, layer: training_extension, tier: stable, python: ">=3.11,<3.15"} - - {id: optuna, name: Optuna, layer: training_extension, tier: stable, python: ">=3.11,<3.15"} - - {id: prefect, name: Prefect, layer: mlops, tier: stable, python: ">=3.11,<3.15"} - - {id: dagster, name: Dagster, layer: mlops, tier: stable, python: ">=3.11,<3.15"} - - {id: temporal, name: Temporal, layer: mlops, tier: stable, python: ">=3.11,<3.15"} - - {id: celery, name: Celery, layer: mlops, tier: stable, python: ">=3.11,<3.15"} - - {id: dvc, name: DVC, layer: mlops, tier: stable, python: ">=3.11,<3.15"} - - {id: feast, name: Feast, layer: mlops, tier: stable, python: ">=3.11,<3.15"} - - {id: evidently, name: Evidently, layer: mlops, tier: stable, python: ">=3.11,<3.15"} - - {id: polars, name: Polars, layer: mlops, tier: stable, python: ">=3.11,<3.15"} - - {id: pandera, name: Pandera, layer: mlops, tier: stable, python: ">=3.11,<3.15"} + - {id: lightning, name: Lightning, layer: training_extension, tier: stable, python: ">=3.11,<3.15", packages: ["lightning>=2.6.5,<3"]} + - {id: datasets, name: Hugging Face Datasets, layer: training_extension, tier: stable, python: ">=3.11,<3.15", packages: ["datasets>=5.0.1,<6"]} + - {id: accelerate, name: Accelerate, layer: training_extension, tier: stable, python: ">=3.11,<3.15", packages: ["accelerate>=1.14.0,<2"]} + - {id: peft, name: PEFT, layer: training_extension, tier: stable, python: ">=3.11,<3.15", packages: ["peft>=0.20.0,<0.21"]} + - {id: trl, name: TRL, layer: training_extension, tier: stable, python: ">=3.11,<3.15", packages: ["trl>=1.9.2,<2"]} + - {id: optuna, name: Optuna, layer: training_extension, tier: stable, python: ">=3.11,<3.15", packages: ["optuna>=4.9.0,<5"]} + - {id: prefect, name: Prefect, layer: mlops, tier: stable, python: ">=3.11,<3.15", packages: ["prefect>=3.8.2,<4"]} + - {id: dagster, name: Dagster, layer: mlops, tier: stable, python: ">=3.11,<3.15", packages: ["dagster>=1.13.17,<2"]} + - {id: temporal, name: Temporal, layer: mlops, tier: stable, python: ">=3.11,<3.15", packages: ["temporalio>=1.31.0,<2"]} + - {id: celery, name: Celery, layer: mlops, tier: stable, python: ">=3.11,<3.15", packages: ["celery>=5.6.3,<6"]} + - {id: dvc, name: DVC, layer: mlops, tier: stable, python: ">=3.11,<3.15", packages: ["dvc>=3.67.1,<4"]} + - {id: feast, name: Feast, layer: mlops, tier: stable, python: ">=3.11,<3.15", packages: ["feast>=0.65.0,<1"]} + - {id: evidently, name: Evidently, layer: mlops, tier: stable, python: ">=3.11,<3.15", packages: ["evidently>=0.7.21,<1"]} + - {id: polars, name: Polars, layer: mlops, tier: stable, python: ">=3.11,<3.15", packages: ["polars>=1.43.2,<2"]} + - {id: pandera, name: Pandera, layer: mlops, tier: stable, python: ">=3.11,<3.15", packages: ["pandera>=0.32.1,<0.33"]} - - {id: deepeval, name: DeepEval, layer: quality, tier: stable, python: ">=3.11,<3.15"} - - {id: ragas, name: Ragas, layer: quality, tier: stable, python: ">=3.11,<3.15"} - - {id: pydantic-evals, name: Pydantic Evals, layer: quality, tier: stable, python: ">=3.11,<3.15"} - - {id: opentelemetry, name: OpenTelemetry, layer: quality, tier: stable, python: ">=3.11,<3.15"} - - {id: langfuse, name: Langfuse, layer: quality, tier: platform, python: ">=3.11,<3.15"} - - {id: phoenix, name: Phoenix, layer: quality, tier: stable, python: ">=3.11,<3.15"} - - {id: mlflow, name: MLflow, layer: quality, tier: stable, python: ">=3.11,<3.15"} - - {id: langsmith, name: LangSmith, layer: quality, tier: platform, python: ">=3.11,<3.15"} - - {id: wandb, name: Weights & Biases, layer: quality, tier: platform, python: ">=3.11,<3.15"} + - {id: deepeval, name: DeepEval, layer: quality, tier: stable, python: ">=3.11,<3.15", packages: ["deepeval>=4.1.5,<5"]} + - {id: ragas, name: Ragas, layer: quality, tier: stable, python: ">=3.11,<3.15", packages: ["ragas>=0.4.3,<0.5"]} + - {id: pydantic-evals, name: Pydantic Evals, layer: quality, tier: stable, python: ">=3.11,<3.15", packages: ["pydantic-evals>=2.27.0,<3"]} + - {id: opentelemetry, name: OpenTelemetry, layer: quality, tier: stable, python: ">=3.11,<3.15", packages: ["opentelemetry-sdk>=1.44.0,<2"]} + - {id: langfuse, name: Langfuse, layer: quality, tier: platform, python: ">=3.11,<3.15", packages: ["langfuse>=4.14.3,<5"]} + - {id: phoenix, name: Phoenix, layer: quality, tier: stable, python: ">=3.11,<3.15", packages: ["arize-phoenix>=19.19.1,<20"]} + - {id: mlflow, name: MLflow, layer: quality, tier: stable, python: ">=3.11,<3.15", packages: ["mlflow>=3.15.1,<4"]} + - {id: langsmith, name: LangSmith, layer: quality, tier: platform, python: ">=3.11,<3.15", packages: ["langsmith>=0.10.17,<0.11"]} + - {id: wandb, name: Weights & Biases, layer: quality, tier: platform, python: ">=3.11,<3.15", packages: ["wandb>=0.28.1,<0.29"]} - {id: docker, name: Docker / Compose, layer: deploy, tier: stable, python: ">=3.11,<3.15"} - {id: render, name: Render, layer: deploy, tier: platform, python: ">=3.11,<3.15"} diff --git a/catalog/generated/catalog.json b/catalog/generated/catalog.json index 664f85c..0f9a8fd 100644 --- a/catalog/generated/catalog.json +++ b/catalog/generated/catalog.json @@ -392,7 +392,9 @@ "hybrid" ], "roles": [], - "packages": [] + "packages": [ + "torch>=2.13.0,<3" + ] }, { "id": "transformers", @@ -405,7 +407,10 @@ "hybrid" ], "roles": [], - "packages": [] + "packages": [ + "transformers>=5.14.1,<6", + "torch>=2.13.0,<3" + ] }, { "id": "jax", @@ -418,20 +423,25 @@ "hybrid" ], "roles": [], - "packages": [] + "packages": [ + "jax>=0.11.0,<0.12" + ] }, { "id": "keras", "name": "Keras / TensorFlow", "layer": "framework", "tier": "stable", - "python": ">=3.11,<3.15", + "python": ">=3.11,<3.14", "workloads": [ "training", "hybrid" ], "roles": [], - "packages": [] + "packages": [ + "keras>=3.15.1,<4", + "tensorflow>=2.21.0,<3" + ] }, { "id": "scikit-learn", @@ -444,7 +454,9 @@ "hybrid" ], "roles": [], - "packages": [] + "packages": [ + "scikit-learn>=1.9.0,<2" + ] }, { "id": "xgboost", @@ -457,7 +469,9 @@ "hybrid" ], "roles": [], - "packages": [] + "packages": [ + "xgboost>=3.4.0,<4" + ] }, { "id": "streamlit", @@ -1055,7 +1069,9 @@ "python": ">=3.11,<3.15", "workloads": [], "roles": [], - "packages": [] + "packages": [ + "bentoml>=1.4.39,<2" + ] }, { "id": "litellm", @@ -1065,17 +1081,21 @@ "python": ">=3.11,<3.15", "workloads": [], "roles": [], - "packages": [] + "packages": [ + "litellm>=1.95.0,<2" + ] }, { "id": "vllm", "name": "vLLM", "layer": "serving", "tier": "platform", - "python": ">=3.11,<3.15", + "python": ">=3.11,<3.14", "workloads": [], "roles": [], - "packages": [] + "packages": [ + "vllm>=0.26.0,<0.27" + ] }, { "id": "ollama-serving", @@ -1085,7 +1105,9 @@ "python": ">=3.11,<3.15", "workloads": [], "roles": [], - "packages": [] + "packages": [ + "ollama>=0.6.2,<1" + ] }, { "id": "ray-serve", @@ -1095,7 +1117,9 @@ "python": ">=3.11,<3.15", "workloads": [], "roles": [], - "packages": [] + "packages": [ + "ray[serve]>=2.56.1,<3" + ] }, { "id": "lightning", @@ -1105,7 +1129,9 @@ "python": ">=3.11,<3.15", "workloads": [], "roles": [], - "packages": [] + "packages": [ + "lightning>=2.6.5,<3" + ] }, { "id": "datasets", @@ -1115,7 +1141,9 @@ "python": ">=3.11,<3.15", "workloads": [], "roles": [], - "packages": [] + "packages": [ + "datasets>=5.0.1,<6" + ] }, { "id": "accelerate", @@ -1125,7 +1153,9 @@ "python": ">=3.11,<3.15", "workloads": [], "roles": [], - "packages": [] + "packages": [ + "accelerate>=1.14.0,<2" + ] }, { "id": "peft", @@ -1135,7 +1165,9 @@ "python": ">=3.11,<3.15", "workloads": [], "roles": [], - "packages": [] + "packages": [ + "peft>=0.20.0,<0.21" + ] }, { "id": "trl", @@ -1145,7 +1177,9 @@ "python": ">=3.11,<3.15", "workloads": [], "roles": [], - "packages": [] + "packages": [ + "trl>=1.9.2,<2" + ] }, { "id": "optuna", @@ -1155,7 +1189,9 @@ "python": ">=3.11,<3.15", "workloads": [], "roles": [], - "packages": [] + "packages": [ + "optuna>=4.9.0,<5" + ] }, { "id": "prefect", @@ -1165,7 +1201,9 @@ "python": ">=3.11,<3.15", "workloads": [], "roles": [], - "packages": [] + "packages": [ + "prefect>=3.8.2,<4" + ] }, { "id": "dagster", @@ -1175,7 +1213,9 @@ "python": ">=3.11,<3.15", "workloads": [], "roles": [], - "packages": [] + "packages": [ + "dagster>=1.13.17,<2" + ] }, { "id": "temporal", @@ -1185,7 +1225,9 @@ "python": ">=3.11,<3.15", "workloads": [], "roles": [], - "packages": [] + "packages": [ + "temporalio>=1.31.0,<2" + ] }, { "id": "celery", @@ -1195,7 +1237,9 @@ "python": ">=3.11,<3.15", "workloads": [], "roles": [], - "packages": [] + "packages": [ + "celery>=5.6.3,<6" + ] }, { "id": "dvc", @@ -1205,7 +1249,9 @@ "python": ">=3.11,<3.15", "workloads": [], "roles": [], - "packages": [] + "packages": [ + "dvc>=3.67.1,<4" + ] }, { "id": "feast", @@ -1215,7 +1261,9 @@ "python": ">=3.11,<3.15", "workloads": [], "roles": [], - "packages": [] + "packages": [ + "feast>=0.65.0,<1" + ] }, { "id": "evidently", @@ -1225,7 +1273,9 @@ "python": ">=3.11,<3.15", "workloads": [], "roles": [], - "packages": [] + "packages": [ + "evidently>=0.7.21,<1" + ] }, { "id": "polars", @@ -1235,7 +1285,9 @@ "python": ">=3.11,<3.15", "workloads": [], "roles": [], - "packages": [] + "packages": [ + "polars>=1.43.2,<2" + ] }, { "id": "pandera", @@ -1245,7 +1297,9 @@ "python": ">=3.11,<3.15", "workloads": [], "roles": [], - "packages": [] + "packages": [ + "pandera>=0.32.1,<0.33" + ] }, { "id": "deepeval", @@ -1255,7 +1309,9 @@ "python": ">=3.11,<3.15", "workloads": [], "roles": [], - "packages": [] + "packages": [ + "deepeval>=4.1.5,<5" + ] }, { "id": "ragas", @@ -1265,7 +1321,9 @@ "python": ">=3.11,<3.15", "workloads": [], "roles": [], - "packages": [] + "packages": [ + "ragas>=0.4.3,<0.5" + ] }, { "id": "pydantic-evals", @@ -1275,7 +1333,9 @@ "python": ">=3.11,<3.15", "workloads": [], "roles": [], - "packages": [] + "packages": [ + "pydantic-evals>=2.27.0,<3" + ] }, { "id": "opentelemetry", @@ -1285,7 +1345,9 @@ "python": ">=3.11,<3.15", "workloads": [], "roles": [], - "packages": [] + "packages": [ + "opentelemetry-sdk>=1.44.0,<2" + ] }, { "id": "langfuse", @@ -1295,7 +1357,9 @@ "python": ">=3.11,<3.15", "workloads": [], "roles": [], - "packages": [] + "packages": [ + "langfuse>=4.14.3,<5" + ] }, { "id": "phoenix", @@ -1305,7 +1369,9 @@ "python": ">=3.11,<3.15", "workloads": [], "roles": [], - "packages": [] + "packages": [ + "arize-phoenix>=19.19.1,<20" + ] }, { "id": "mlflow", @@ -1315,7 +1381,9 @@ "python": ">=3.11,<3.15", "workloads": [], "roles": [], - "packages": [] + "packages": [ + "mlflow>=3.15.1,<4" + ] }, { "id": "langsmith", @@ -1325,7 +1393,9 @@ "python": ">=3.11,<3.15", "workloads": [], "roles": [], - "packages": [] + "packages": [ + "langsmith>=0.10.17,<0.11" + ] }, { "id": "wandb", @@ -1335,7 +1405,9 @@ "python": ">=3.11,<3.15", "workloads": [], "roles": [], - "packages": [] + "packages": [ + "wandb>=0.28.1,<0.29" + ] }, { "id": "docker", diff --git a/copier.yml b/copier.yml index 115e936..b6dad62 100644 --- a/copier.yml +++ b/copier.yml @@ -403,7 +403,7 @@ auth: runtime_config: type: bool default: >- - {{ ai_capabilities != 'none' or sql_store != 'none' or document_store != 'none' or vector_store != 'none' or graph_store != 'none' or cache_store != 'none' or auth != 'none' }} + {{ ai_capabilities in ['agents', 'rag', 'inference', 'training'] or sql_store != 'none' or document_store != 'none' or vector_store != 'none' or graph_store != 'none' or cache_store != 'none' or auth != 'none' }} when: false serving: @@ -421,6 +421,10 @@ serving: Ollama: ollama-serving Ray Serve: ray-serve when: "{{ workload in ['inference', 'hybrid'] }}" + validator: >- + {% if workload in ['inference', 'hybrid'] and serving == 'none' %} + Inference and hybrid workloads require a serving engine + {% endif %} training_extensions: type: str @@ -518,7 +522,7 @@ python_version: validator: >- {% if (framework in ['lingo'] or 'violetear' in interfaces or 'beaver' in [document_store, vector_store, graph_store, cache_store]) and python_version == '3.11' %} The selected stack requires Python 3.12 or newer - {% elif (framework == 'crewai' or 'chainlit' in interfaces) and python_version == '3.14' %} + {% elif (framework in ['crewai', 'keras'] or 'chainlit' in interfaces or serving == 'vllm') and python_version == '3.14' %} The selected stack currently requires Python 3.13 or older {% endif %} diff --git a/docs/reference/components.md b/docs/reference/components.md index 85d2985..e3b969a 100644 --- a/docs/reference/components.md +++ b/docs/reference/components.md @@ -37,7 +37,7 @@ This reference is generated from `catalog/components.yml`. Do not edit it by han | `framework` | PyTorch (`pytorch`) | `stable` | `>=3.11,<3.15` | | `framework` | Hugging Face Transformers (`transformers`) | `stable` | `>=3.11,<3.15` | | `framework` | JAX (`jax`) | `stable` | `>=3.11,<3.15` | -| `framework` | Keras / TensorFlow (`keras`) | `stable` | `>=3.11,<3.15` | +| `framework` | Keras / TensorFlow (`keras`) | `stable` | `>=3.11,<3.14` | | `framework` | scikit-learn (`scikit-learn`) | `stable` | `>=3.11,<3.15` | | `framework` | XGBoost (`xgboost`) | `stable` | `>=3.11,<3.15` | | `interface` | Streamlit (`streamlit`) | `stable` | `>=3.11,<3.15` | @@ -88,7 +88,7 @@ This reference is generated from `catalog/components.yml`. Do not edit it by han | `auth` | Supabase Auth (`supabase-auth`) | `platform` | `>=3.11,<3.15` | | `serving` | BentoML (`bentoml`) | `stable` | `>=3.11,<3.15` | | `serving` | LiteLLM (`litellm`) | `stable` | `>=3.11,<3.15` | -| `serving` | vLLM (`vllm`) | `platform` | `>=3.11,<3.15` | +| `serving` | vLLM (`vllm`) | `platform` | `>=3.11,<3.14` | | `serving` | Ollama (`ollama-serving`) | `stable` | `>=3.11,<3.15` | | `serving` | Ray Serve (`ray-serve`) | `stable` | `>=3.11,<3.15` | | `training_extension` | Lightning (`lightning`) | `stable` | `>=3.11,<3.15` | diff --git a/scripts/compile_catalog.py b/scripts/compile_catalog.py index ce9707f..1396202 100644 --- a/scripts/compile_catalog.py +++ b/scripts/compile_catalog.py @@ -178,8 +178,10 @@ def build_artifacts( "{%- endif -%}", "{%- endfor -%}", "{%- endfor %}", - "{%- if ai_capabilities != 'none' or sql_store != 'none' or document_store != 'none' or vector_store != 'none' or graph_store != 'none' or cache_store != 'none' or auth | default('none', true) != 'none' %}", + "{%- if ai_capabilities in ['agents', 'rag', 'inference'] or 'supabase' in [sql_store, document_store] or auth | default('none', true) in ['api-key', 'supabase-auth'] %}", ' "pydantic>=2.13.4,<3",', + "{%- endif %}", + "{%- if ai_capabilities in ['agents', 'rag', 'inference', 'training'] or sql_store != 'none' or document_store != 'none' or vector_store != 'none' or graph_store != 'none' or cache_store != 'none' or auth | default('none', true) != 'none' %}", ' "pydantic-settings>=2.15.0,<3",', "{%- endif %}", "{%- if framework == 'pydantic-ai' and pydantic_ai_harness | default([]) %}", diff --git a/template/_catalog_dependencies.jinja b/template/_catalog_dependencies.jinja index 8ed2dda..64fea12 100644 --- a/template/_catalog_dependencies.jinja +++ b/template/_catalog_dependencies.jinja @@ -1,5 +1,5 @@ {# Generated by scripts/compile_catalog.py. Do not edit. #} -{%- set component_packages = {"ag2": ["ag2>=1.0.1,<2"], "agno": ["agno>=2.8.7,<3"], "anthropic": ["anthropic>=0.121.0,<1"], "autogen": ["autogen-agentchat>=0.7.5,<1"], "azure-openai": ["openai>=2.53.0,<3"], "beaver": ["beaver-db>=2.3.0,<3"], "bedrock": ["boto3>=1.43.67,<2"], "chainlit": ["chainlit>=2.11.1,<3"], "chroma": ["chromadb>=1.5.9,<2"], "cohere": ["cohere>=7.0.8,<8"], "crewai": ["crewai>=1.15.13,<2"], "deepseek": ["openai>=2.53.0,<3"], "dspy": ["dspy>=3.3.0,<4"], "duckdb": ["duckdb>=1.5.5,<2", "duckdb-engine>=0.17.0,<1"], "fastapi": ["fastapi[standard]>=0.141.1,<1", "pydantic>=2.13.4,<3", "uvicorn>=0.52.1,<1"], "fasthtml": ["python-fasthtml>=0.14.11,<0.15"], "fastmcp": ["fastmcp>=3.4.6,<4"], "fireworks": ["fireworks-ai>=1.2.6,<2"], "flask": ["flask>=3.1.2,<4"], "gemini": ["google-genai>=2.17.0,<3"], "google-adk": ["google-adk>=2.6.3,<3"], "gradio": ["gradio>=6.22.0,<7"], "groq": ["groq>=1.6.0,<2"], "haystack": ["haystack-ai>=3.0.0,<4"], "huggingface": ["huggingface-hub>=1.27.0,<2"], "jupyterlab": ["jupyterlab>=4.6.2,<5"], "lancedb": ["lancedb>=0.36.0,<1"], "langchain": ["langchain>=1.3.14,<2"], "langgraph": ["langgraph>=1.2.10,<2"], "lingo": ["lingo-ai>=2.1.0,<3"], "llamaindex": ["llama-index>=0.14.23,<0.15"], "milvus": ["pymilvus>=3.0.1,<4"], "mistral": ["mistralai>=2.9.1,<3"], "mongodb": ["pymongo>=4.17.0,<5"], "mysql": ["pymysql>=1.2.0,<2"], "neo4j": ["neo4j>=6.2.0,<7"], "nicegui": ["nicegui>=3.15.0,<4"], "oidc": ["authlib>=1.7.2,<2"], "ollama": ["ollama>=0.6.2,<1"], "openai": ["openai>=2.53.0,<3"], "openai-agents": ["openai-agents>=0.19.4,<1"], "openai-compatible": ["openai>=2.53.0,<3"], "openrouter": ["openrouter>=1.1.37,<2"], "pgvector": ["pgvector>=0.5.0,<1"], "pinecone": ["pinecone>=9.1.0,<10"], "postgresql": ["psycopg[binary]>=3.3.4,<4"], "pydantic-ai": ["pydantic-ai>=2.27.0,<3"], "qdrant": ["qdrant-client>=1.19.0,<2"], "redis": ["redis>=8.1.0,<9"], "sentence-transformers": ["sentence-transformers>=5.7.0,<6"], "smolagents": ["smolagents>=1.26.0,<2"], "sqlalchemy": ["sqlalchemy>=2.0.51,<3", "alembic>=1.19.1,<2"], "sqlmodel": ["sqlmodel>=0.0.39,<0.1", "sqlalchemy>=2.0.51,<3", "alembic>=1.19.1,<2"], "strands-agents": ["strands-agents>=1.51.0,<2"], "streamlit": ["streamlit>=1.61.1,<2"], "supabase": ["supabase>=2.31.0,<3"], "supabase-auth": ["supabase>=2.31.0,<3"], "textual": ["textual>=8.2.8,<9"], "together": ["together>=2.30.0,<3"], "typer": ["typer>=0.27.1,<1"], "violetear": ["violetear[server]>=1.4.1,<2"], "weaviate": ["weaviate-client>=4.22.0,<5"], "xai": ["xai-sdk>=1.17.0,<2"]} -%} +{%- set component_packages = {"accelerate": ["accelerate>=1.14.0,<2"], "ag2": ["ag2>=1.0.1,<2"], "agno": ["agno>=2.8.7,<3"], "anthropic": ["anthropic>=0.121.0,<1"], "autogen": ["autogen-agentchat>=0.7.5,<1"], "azure-openai": ["openai>=2.53.0,<3"], "beaver": ["beaver-db>=2.3.0,<3"], "bedrock": ["boto3>=1.43.67,<2"], "bentoml": ["bentoml>=1.4.39,<2"], "celery": ["celery>=5.6.3,<6"], "chainlit": ["chainlit>=2.11.1,<3"], "chroma": ["chromadb>=1.5.9,<2"], "cohere": ["cohere>=7.0.8,<8"], "crewai": ["crewai>=1.15.13,<2"], "dagster": ["dagster>=1.13.17,<2"], "datasets": ["datasets>=5.0.1,<6"], "deepeval": ["deepeval>=4.1.5,<5"], "deepseek": ["openai>=2.53.0,<3"], "dspy": ["dspy>=3.3.0,<4"], "duckdb": ["duckdb>=1.5.5,<2", "duckdb-engine>=0.17.0,<1"], "dvc": ["dvc>=3.67.1,<4"], "evidently": ["evidently>=0.7.21,<1"], "fastapi": ["fastapi[standard]>=0.141.1,<1", "pydantic>=2.13.4,<3", "uvicorn>=0.52.1,<1"], "fasthtml": ["python-fasthtml>=0.14.11,<0.15"], "fastmcp": ["fastmcp>=3.4.6,<4"], "feast": ["feast>=0.65.0,<1"], "fireworks": ["fireworks-ai>=1.2.6,<2"], "flask": ["flask>=3.1.2,<4"], "gemini": ["google-genai>=2.17.0,<3"], "google-adk": ["google-adk>=2.6.3,<3"], "gradio": ["gradio>=6.22.0,<7"], "groq": ["groq>=1.6.0,<2"], "haystack": ["haystack-ai>=3.0.0,<4"], "huggingface": ["huggingface-hub>=1.27.0,<2"], "jax": ["jax>=0.11.0,<0.12"], "jupyterlab": ["jupyterlab>=4.6.2,<5"], "keras": ["keras>=3.15.1,<4", "tensorflow>=2.21.0,<3"], "lancedb": ["lancedb>=0.36.0,<1"], "langchain": ["langchain>=1.3.14,<2"], "langfuse": ["langfuse>=4.14.3,<5"], "langgraph": ["langgraph>=1.2.10,<2"], "langsmith": ["langsmith>=0.10.17,<0.11"], "lightning": ["lightning>=2.6.5,<3"], "lingo": ["lingo-ai>=2.1.0,<3"], "litellm": ["litellm>=1.95.0,<2"], "llamaindex": ["llama-index>=0.14.23,<0.15"], "milvus": ["pymilvus>=3.0.1,<4"], "mistral": ["mistralai>=2.9.1,<3"], "mlflow": ["mlflow>=3.15.1,<4"], "mongodb": ["pymongo>=4.17.0,<5"], "mysql": ["pymysql>=1.2.0,<2"], "neo4j": ["neo4j>=6.2.0,<7"], "nicegui": ["nicegui>=3.15.0,<4"], "oidc": ["authlib>=1.7.2,<2"], "ollama": ["ollama>=0.6.2,<1"], "ollama-serving": ["ollama>=0.6.2,<1"], "openai": ["openai>=2.53.0,<3"], "openai-agents": ["openai-agents>=0.19.4,<1"], "openai-compatible": ["openai>=2.53.0,<3"], "openrouter": ["openrouter>=1.1.37,<2"], "opentelemetry": ["opentelemetry-sdk>=1.44.0,<2"], "optuna": ["optuna>=4.9.0,<5"], "pandera": ["pandera>=0.32.1,<0.33"], "peft": ["peft>=0.20.0,<0.21"], "pgvector": ["pgvector>=0.5.0,<1"], "phoenix": ["arize-phoenix>=19.19.1,<20"], "pinecone": ["pinecone>=9.1.0,<10"], "polars": ["polars>=1.43.2,<2"], "postgresql": ["psycopg[binary]>=3.3.4,<4"], "prefect": ["prefect>=3.8.2,<4"], "pydantic-ai": ["pydantic-ai>=2.27.0,<3"], "pydantic-evals": ["pydantic-evals>=2.27.0,<3"], "pytorch": ["torch>=2.13.0,<3"], "qdrant": ["qdrant-client>=1.19.0,<2"], "ragas": ["ragas>=0.4.3,<0.5"], "ray-serve": ["ray[serve]>=2.56.1,<3"], "redis": ["redis>=8.1.0,<9"], "scikit-learn": ["scikit-learn>=1.9.0,<2"], "sentence-transformers": ["sentence-transformers>=5.7.0,<6"], "smolagents": ["smolagents>=1.26.0,<2"], "sqlalchemy": ["sqlalchemy>=2.0.51,<3", "alembic>=1.19.1,<2"], "sqlmodel": ["sqlmodel>=0.0.39,<0.1", "sqlalchemy>=2.0.51,<3", "alembic>=1.19.1,<2"], "strands-agents": ["strands-agents>=1.51.0,<2"], "streamlit": ["streamlit>=1.61.1,<2"], "supabase": ["supabase>=2.31.0,<3"], "supabase-auth": ["supabase>=2.31.0,<3"], "temporal": ["temporalio>=1.31.0,<2"], "textual": ["textual>=8.2.8,<9"], "together": ["together>=2.30.0,<3"], "transformers": ["transformers>=5.14.1,<6", "torch>=2.13.0,<3"], "trl": ["trl>=1.9.2,<2"], "typer": ["typer>=0.27.1,<1"], "violetear": ["violetear[server]>=1.4.1,<2"], "vllm": ["vllm>=0.26.0,<0.27"], "wandb": ["wandb>=0.28.1,<0.29"], "weaviate": ["weaviate-client>=4.22.0,<5"], "xai": ["xai-sdk>=1.17.0,<2"], "xgboost": ["xgboost>=3.4.0,<4"]} -%} {%- set selected_components = [framework] + (interfaces | default([])) + ([model_provider] if ai_capabilities in ['agents', 'rag', 'inference'] else []) @@ -20,8 +20,10 @@ {%- endif -%} {%- endfor -%} {%- endfor %} -{%- if ai_capabilities != 'none' or sql_store != 'none' or document_store != 'none' or vector_store != 'none' or graph_store != 'none' or cache_store != 'none' or auth | default('none', true) != 'none' %} +{%- if ai_capabilities in ['agents', 'rag', 'inference'] or 'supabase' in [sql_store, document_store] or auth | default('none', true) in ['api-key', 'supabase-auth'] %} "pydantic>=2.13.4,<3", +{%- endif %} +{%- if ai_capabilities in ['agents', 'rag', 'inference', 'training'] or sql_store != 'none' or document_store != 'none' or vector_store != 'none' or graph_store != 'none' or cache_store != 'none' or auth | default('none', true) != 'none' %} "pydantic-settings>=2.15.0,<3", {%- endif %} {%- if framework == 'pydantic-ai' and pydantic_ai_harness | default([]) %} diff --git a/template/pyproject.toml.jinja b/template/pyproject.toml.jinja index 84395c2..8025e09 100644 --- a/template/pyproject.toml.jinja +++ b/template/pyproject.toml.jinja @@ -33,7 +33,7 @@ Issues = "https://github.com/{{ github_username }}/{{ github_repo }}/issues" Documentation = "https://{{ github_username }}.github.io/{{ github_repo }}" {%- endif %} -{% if workload in ['cli', 'api', 'mcp', 'agent', 'rag'] or interfaces | default([]) -%} +{% if workload in ['cli', 'api', 'mcp', 'agent', 'rag', 'inference', 'training', 'hybrid'] or interfaces | default([]) -%} [project.scripts] {%- if workload == 'cli' %} {{ project_slug }} = "{{ module_name }}.cli:app" @@ -45,6 +45,13 @@ Documentation = "https://{{ github_username }}.github.io/{{ github_repo }}" {{ project_slug }} = "{{ module_name }}.agent:main" {%- elif workload == 'rag' %} {{ project_slug }} = "{{ module_name }}.rag:main" +{%- elif workload == 'inference' %} +{{ project_slug }} = "{{ module_name }}.serving:main" +{%- elif workload == 'training' %} +{{ project_slug }} = "{{ module_name }}.training:main" +{%- elif workload == 'hybrid' %} +{{ project_slug }}-train = "{{ module_name }}.training:main" +{{ project_slug }}-serve = "{{ module_name }}.serving:main" {%- endif %} {%- if 'fastapi' in interfaces | default([]) %} {{ project_slug }}-fastapi = "{{ module_name }}.interfaces.fastapi_app:main" @@ -65,6 +72,11 @@ build-backend = "uv_build" module-name = "{{ module_name }}" module-root = "src" +{%- if workload == 'hybrid' %} +[tool.uv.workspace] +members = ["packages/*"] +{%- endif %} + [dependency-groups] dev = [ "deptry>=0.25.1", diff --git a/template/src/{{ module_name }}/{% if mlops_tools or quality_tools %}ops.py{% endif %}.jinja b/template/src/{{ module_name }}/{% if mlops_tools or quality_tools %}ops.py{% endif %}.jinja new file mode 100644 index 0000000..8980a96 --- /dev/null +++ b/template/src/{{ module_name }}/{% if mlops_tools or quality_tools %}ops.py{% endif %}.jinja @@ -0,0 +1,38 @@ +{% set tool_modules = { + 'prefect': 'prefect', + 'dagster': 'dagster', + 'temporal': 'temporalio', + 'celery': 'celery', + 'dvc': 'dvc', + 'feast': 'feast', + 'evidently': 'evidently', + 'polars': 'polars', + 'pandera': 'pandera', + 'deepeval': 'deepeval', + 'ragas': 'ragas', + 'pydantic-evals': 'pydantic_evals', + 'opentelemetry': 'opentelemetry.sdk', + 'langfuse': 'langfuse', + 'phoenix': 'phoenix', + 'mlflow': 'mlflow', + 'langsmith': 'langsmith', + 'wandb': 'wandb', +} -%} +"""Selected orchestration, evaluation and observability SDKs.""" + +from typing import Any + +{% for tool in ((mlops_tools | default([])) + (quality_tools | default([]))) | sort -%} +import {{ tool_modules[tool] }} as {{ tool | replace('-', '_') }}_module +{% endfor %} +MLOPS_TOOLS = ({% for tool in mlops_tools | default([]) %}"{{ tool }}"{% if not loop.last %}, {% endif %}{% endfor %}{% if mlops_tools | default([]) | length == 1 %},{% endif %}) +QUALITY_TOOLS = ({% for tool in quality_tools | default([]) %}"{{ tool }}"{% if not loop.last %}, {% endif %}{% endfor %}{% if quality_tools | default([]) | length == 1 %},{% endif %}) + + +def installed_ops_tools() -> tuple[Any, ...]: + """Return imported SDK modules in the same order as the selected tools.""" + tools: list[Any] = [] +{% for tool in (mlops_tools | default([])) + (quality_tools | default([])) %} + tools.append({{ tool | replace('-', '_') }}_module) +{% endfor %} + return tuple(tools) diff --git a/template/src/{{ module_name }}/{% if runtime_config %}settings.py{% endif %}.jinja b/template/src/{{ module_name }}/{% if runtime_config %}settings.py{% endif %}.jinja index 59900f8..c7316ba 100644 --- a/template/src/{{ module_name }}/{% if runtime_config %}settings.py{% endif %}.jinja +++ b/template/src/{{ module_name }}/{% if runtime_config %}settings.py{% endif %}.jinja @@ -1,6 +1,8 @@ """Environment-backed application settings.""" +{% if ai_capabilities in ['agents', 'rag', 'inference'] or 'supabase' in [sql_store, document_store] or auth in ['supabase-auth', 'api-key'] -%} from pydantic import SecretStr +{% endif -%} from pydantic_settings import BaseSettings, SettingsConfigDict @@ -8,7 +10,7 @@ class Settings(BaseSettings): """Configuration loaded from environment variables or a local .env file.""" model_config = SettingsConfigDict(env_file=".env", extra="ignore") -{%- if ai_capabilities != 'none' %} +{%- if ai_capabilities in ['agents', 'rag', 'inference'] %} model_provider: str = "{{ model_provider | default('none', true) }}" model_name: str = "{% if model_provider | default('none', true) == 'openai' %}gpt-5-mini{% elif model_provider | default('none', true) == 'anthropic' %}claude-sonnet-4-5{% elif model_provider | default('none', true) == 'gemini' %}gemini-2.5-flash{% elif model_provider | default('none', true) == 'bedrock' %}amazon.nova-lite-v1:0{% elif model_provider | default('none', true) == 'ollama' %}qwen3:4b{% else %}configure-me{% endif %}" @@ -19,6 +21,11 @@ class Settings(BaseSettings): embedding_model: str = "{% if embedding_provider == 'sentence-transformers' %}sentence-transformers/all-MiniLM-L6-v2{% else %}configure-me{% endif %}" {%- endif %} {%- endif %} +{%- if ai_capabilities == 'training' %} + + dataset_name: str = "configure-me" + output_dir: str = "artifacts" +{%- endif %} {%- if sql_store != 'none' %} database_url: str = "{% if sql_store == 'sqlite' %}sqlite:///app.db{% elif sql_store == 'postgresql' %}postgresql+psycopg://postgres:postgres@localhost:5432/app{% elif sql_store == 'mysql' %}mysql+pymysql://root:password@localhost:3306/app{% elif sql_store == 'duckdb' %}duckdb:///app.duckdb{% else %}configure-me{% endif %}" diff --git a/template/src/{{ module_name }}/{% if serving != 'none' %}serving.py{% endif %}.jinja b/template/src/{{ module_name }}/{% if serving != 'none' %}serving.py{% endif %}.jinja new file mode 100644 index 0000000..d729f24 --- /dev/null +++ b/template/src/{{ module_name }}/{% if serving != 'none' %}serving.py{% endif %}.jinja @@ -0,0 +1,31 @@ +"""Adapter for the selected model-serving engine.""" + +from typing import Any + +{% if serving == 'bentoml' -%} +import bentoml as serving_module +{%- elif serving == 'litellm' -%} +import litellm as serving_module +{%- elif serving == 'vllm' -%} +import vllm as serving_module +{%- elif serving == 'ollama-serving' -%} +import ollama as serving_module +{%- elif serving == 'ray-serve' -%} +from ray import serve as serving_module +{%- endif %} + +SERVING_ENGINE = "{{ serving }}" + + +def serving_sdk() -> Any: + """Return the configured serving SDK without starting a server.""" + return serving_module + + +def main() -> None: + """Report the selected engine; add model loading at the application boundary.""" + print(f"Serving engine: {SERVING_ENGINE}") + + +if __name__ == "__main__": # pragma: no cover + main() diff --git a/template/src/{{ module_name }}/{% if workload in ['training', 'hybrid'] %}training.py{% endif %}.jinja b/template/src/{{ module_name }}/{% if workload in ['training', 'hybrid'] %}training.py{% endif %}.jinja new file mode 100644 index 0000000..68bced7 --- /dev/null +++ b/template/src/{{ module_name }}/{% if workload in ['training', 'hybrid'] %}training.py{% endif %}.jinja @@ -0,0 +1,60 @@ +{% set extension_modules = { + 'lightning': 'lightning', + 'datasets': 'datasets', + 'accelerate': 'accelerate', + 'peft': 'peft', + 'trl': 'trl', + 'optuna': 'optuna', +} -%} +"""Offline-testable {{ framework }} training scaffold.""" + +from __future__ import annotations + +from typing import Any + +{% if framework == 'pytorch' -%} +import torch as framework_module +{% elif framework == 'transformers' -%} +import transformers as framework_module +import torch as backend_module +{% elif framework == 'jax' -%} +import jax as framework_module +{% elif framework == 'keras' -%} +import keras as framework_module +import tensorflow as backend_module +{% elif framework == 'scikit-learn' -%} +import sklearn as framework_module +{% elif framework == 'xgboost' -%} +import xgboost as framework_module +{% endif %} +# isort: split +{% for extension in training_extensions | default([]) | sort -%} +import {{ extension_modules[extension] }} as {{ extension | replace('-', '_') }}_module +{% endfor %} +FRAMEWORK = "{{ framework }}" +EXTENSIONS = ({% for extension in training_extensions | default([]) %}"{{ extension }}"{% if not loop.last %}, {% endif %}{% endfor %}{% if training_extensions | default([]) | length == 1 %},{% endif %}) + + +def installed_training_tools() -> tuple[Any, ...]: + """Return installed framework and extension modules.""" + tools: list[Any] = [framework_module] +{% if framework in ['transformers', 'keras'] %} tools.append(backend_module) +{% endif -%} +{% for extension in training_extensions | default([]) %} tools.append({{ extension | replace('-', '_') }}_module) +{% endfor %} return tuple(tools) + + +def train(values: list[float]) -> dict[str, float]: + """Run a deterministic local training placeholder without downloading models.""" + installed_training_tools() + mean = sum(values) / len(values) if values else 0.0 + return {"examples": float(len(values)), "mean": mean} + + +def main() -> None: + """Run the offline training demonstration.""" + print(train([1.0, 2.0, 3.0])) + + +if __name__ == "__main__": # pragma: no cover + main() diff --git a/template/tests/{% if mlops_tools or quality_tools %}test_ops.py{% endif %}.jinja b/template/tests/{% if mlops_tools or quality_tools %}test_ops.py{% endif %}.jinja new file mode 100644 index 0000000..0bf02b7 --- /dev/null +++ b/template/tests/{% if mlops_tools or quality_tools %}test_ops.py{% endif %}.jinja @@ -0,0 +1,9 @@ +"""Tests for selected operations and quality tools.""" + +from {{ module_name }}.ops import MLOPS_TOOLS, QUALITY_TOOLS, installed_ops_tools + + +def test_selected_ops_tools_are_installed() -> None: + assert MLOPS_TOOLS == ({% for tool in mlops_tools | default([]) %}"{{ tool }}"{% if not loop.last %}, {% endif %}{% endfor %}{% if mlops_tools | default([]) | length == 1 %},{% endif %}) + assert QUALITY_TOOLS == ({% for tool in quality_tools | default([]) %}"{{ tool }}"{% if not loop.last %}, {% endif %}{% endfor %}{% if quality_tools | default([]) | length == 1 %},{% endif %}) + assert len(installed_ops_tools()) == len(MLOPS_TOOLS) + len(QUALITY_TOOLS) diff --git a/template/tests/{% if serving != 'none' %}test_serving.py{% endif %}.jinja b/template/tests/{% if serving != 'none' %}test_serving.py{% endif %}.jinja new file mode 100644 index 0000000..c6de643 --- /dev/null +++ b/template/tests/{% if serving != 'none' %}test_serving.py{% endif %}.jinja @@ -0,0 +1,8 @@ +"""Tests for the serving adapter.""" + +from {{ module_name }}.serving import SERVING_ENGINE, serving_sdk + + +def test_serving_sdk_is_installed() -> None: + assert SERVING_ENGINE == "{{ serving }}" + assert serving_sdk() is not None diff --git a/template/tests/{% if workload in ['training', 'hybrid'] %}test_training.py{% endif %}.jinja b/template/tests/{% if workload in ['training', 'hybrid'] %}test_training.py{% endif %}.jinja new file mode 100644 index 0000000..d31427d --- /dev/null +++ b/template/tests/{% if workload in ['training', 'hybrid'] %}test_training.py{% endif %}.jinja @@ -0,0 +1,9 @@ +"""Tests for the offline training scaffold.""" + +from {{ module_name }}.training import EXTENSIONS, FRAMEWORK, train + + +def test_training_stack_and_offline_metric() -> None: + assert FRAMEWORK == "{{ framework }}" + assert EXTENSIONS == ({% for extension in training_extensions | default([]) %}"{{ extension }}"{% if not loop.last %}, {% endif %}{% endfor %}{% if training_extensions | default([]) | length == 1 %},{% endif %}) + assert train([1.0, 3.0]) == {"examples": 2.0, "mean": 2.0} diff --git a/template/{% if runtime_config %}.env.example{% endif %}.jinja b/template/{% if runtime_config %}.env.example{% endif %}.jinja index abae979..20faea5 100644 --- a/template/{% if runtime_config %}.env.example{% endif %}.jinja +++ b/template/{% if runtime_config %}.env.example{% endif %}.jinja @@ -1,5 +1,5 @@ # Copy this file to .env. Never commit real credentials. -{%- if ai_capabilities != 'none' %} +{%- if ai_capabilities in ['agents', 'rag', 'inference'] %} MODEL_PROVIDER={{ model_provider | default('none', true) }} MODEL_NAME={% if model_provider | default('none', true) == 'openai' %}gpt-5-mini{% elif model_provider | default('none', true) == 'anthropic' %}claude-sonnet-4-5{% elif model_provider | default('none', true) == 'gemini' %}gemini-2.5-flash{% elif model_provider | default('none', true) == 'bedrock' %}amazon.nova-lite-v1:0{% elif model_provider | default('none', true) == 'ollama' %}qwen3:4b{% else %}configure-me{% endif %} MODEL_API_KEY= @@ -8,6 +8,10 @@ MODEL_BASE_URL={% if model_provider | default('none', true) == 'ollama' %}http:/ EMBEDDING_PROVIDER={{ embedding_provider }} EMBEDDING_MODEL={% if embedding_provider == 'sentence-transformers' %}sentence-transformers/all-MiniLM-L6-v2{% else %}configure-me{% endif %} {%- endif %} +{%- if ai_capabilities == 'training' %} +DATASET_NAME=configure-me +OUTPUT_DIR=artifacts +{%- endif %} {%- endif %} {%- if sql_store != 'none' %} DATABASE_URL={% if sql_store == 'sqlite' %}sqlite:///app.db{% elif sql_store == 'postgresql' %}postgresql+psycopg://postgres:postgres@localhost:5432/app{% elif sql_store == 'mysql' %}mysql+pymysql://root:password@localhost:3306/app{% elif sql_store == 'duckdb' %}duckdb:///app.duckdb{% endif %} diff --git a/template/{% if workload == 'hybrid' %}packages{% endif %}/service/pyproject.toml.jinja b/template/{% if workload == 'hybrid' %}packages{% endif %}/service/pyproject.toml.jinja new file mode 100644 index 0000000..3fdcbf3 --- /dev/null +++ b/template/{% if workload == 'hybrid' %}packages{% endif %}/service/pyproject.toml.jinja @@ -0,0 +1,16 @@ +[project] +name = "{{ project_slug }}-service" +version = "0.1.0" +requires-python = ">={{ python_version }}" +dependencies = ["{{ project_slug }}"] + +[tool.uv.sources] +"{{ project_slug }}" = { workspace = true } + +[build-system] +requires = ["uv_build>=0.12.1,<0.13"] +build-backend = "uv_build" + +[tool.uv.build-backend] +module-name = "{{ module_name }}_service" +module-root = "src" diff --git a/template/{% if workload == 'hybrid' %}packages{% endif %}/service/src/{{ module_name }}_service/__init__.py.jinja b/template/{% if workload == 'hybrid' %}packages{% endif %}/service/src/{{ module_name }}_service/__init__.py.jinja new file mode 100644 index 0000000..08463db --- /dev/null +++ b/template/{% if workload == 'hybrid' %}packages{% endif %}/service/src/{{ module_name }}_service/__init__.py.jinja @@ -0,0 +1,5 @@ +"""Serving workspace facade.""" + +from {{ module_name }}.serving import serving_sdk + +__all__ = ["serving_sdk"] diff --git a/template/{% if workload == 'hybrid' %}packages{% endif %}/training/pyproject.toml.jinja b/template/{% if workload == 'hybrid' %}packages{% endif %}/training/pyproject.toml.jinja new file mode 100644 index 0000000..9190c2d --- /dev/null +++ b/template/{% if workload == 'hybrid' %}packages{% endif %}/training/pyproject.toml.jinja @@ -0,0 +1,16 @@ +[project] +name = "{{ project_slug }}-training" +version = "0.1.0" +requires-python = ">={{ python_version }}" +dependencies = ["{{ project_slug }}"] + +[tool.uv.sources] +"{{ project_slug }}" = { workspace = true } + +[build-system] +requires = ["uv_build>=0.12.1,<0.13"] +build-backend = "uv_build" + +[tool.uv.build-backend] +module-name = "{{ module_name }}_training" +module-root = "src" diff --git a/template/{% if workload == 'hybrid' %}packages{% endif %}/training/src/{{ module_name }}_training/__init__.py.jinja b/template/{% if workload == 'hybrid' %}packages{% endif %}/training/src/{{ module_name }}_training/__init__.py.jinja new file mode 100644 index 0000000..cef8891 --- /dev/null +++ b/template/{% if workload == 'hybrid' %}packages{% endif %}/training/src/{{ module_name }}_training/__init__.py.jinja @@ -0,0 +1,5 @@ +"""Training workspace facade.""" + +from {{ module_name }}.training import train + +__all__ = ["train"] diff --git a/tests/test_ai_presets.py b/tests/test_ai_presets.py index beb71ba..fa023ad 100644 --- a/tests/test_ai_presets.py +++ b/tests/test_ai_presets.py @@ -1,9 +1,11 @@ """Linux integration tests for the agent, MCP, RAG and interface presets.""" from pathlib import Path +from shutil import copytree, ignore_patterns import pytest from conftest import answers, assert_ok, run +from copier import run_copy PRESETS = { "fastmcp-server": ["src/demo_project/mcp.py"], @@ -24,6 +26,10 @@ "src/demo_project/data.py", "src/demo_project/interfaces/violetear_app.py", ], + "litellm-gateway": [ + "src/demo_project/serving.py", + "src/demo_project/data.py", + ], } @@ -82,3 +88,79 @@ def test_data_and_auth_vertical_slice(copie, uv: str) -> None: ) assert_ok(run([uv, "run", "deptry", "src"], project), "deptry") assert_ok(run([uv, "run", "pytest", "-q"], project), "pytest") + + +@pytest.mark.preset +def test_ml_hybrid_vertical_slice(copie, uv: str) -> None: + """A lightweight hybrid stack validates training, serving and workspace wiring.""" + result = copie.copy( + extra_answers=answers( + preset="custom", + workload="hybrid", + ai_capabilities="training", + framework="scikit-learn", + serving="bentoml", + training_extensions=["optuna"], + mlops_tools=["polars", "pandera"], + quality_tools=["opentelemetry"], + deploy_target="none", + use_docs=False, + use_codeql=False, + use_docker=False, + ) + ) + assert result.exception is None, result.exception + project = result.project_dir + + assert (project / "packages/training/pyproject.toml").is_file() + assert (project / "packages/service/pyproject.toml").is_file() + assert_ok(run([uv, "run", "ruff", "check", "."], project), "ruff check") + assert_ok( + run([uv, "run", "ruff", "format", "--check", "."], project), + "ruff format", + ) + assert_ok(run([uv, "run", "deptry", "src"], project), "deptry") + assert_ok(run([uv, "run", "pytest", "-q"], project), "pytest") + + +def test_hf_finetuning_preset_renders_without_downloading_models( + tmp_path: Path, +) -> None: + """The heavyweight reference preset has its complete workspace and dependency set.""" + template = tmp_path / "template" + copytree( + Path.cwd(), + template, + ignore=ignore_patterns(".git", ".venv", ".pytest_cache", "__pycache__"), + ) + project = tmp_path / "hf-project" + run_copy( + str(template), + project, + data=answers( + preset="hf-finetuning", + use_docs=False, + use_codeql=False, + use_docker=False, + ), + defaults=True, + unsafe=True, + skip_tasks=True, + ) + + pyproject = (project / "pyproject.toml").read_text() + for dependency in ( + "transformers>=", + "torch>=", + "datasets>=", + "accelerate>=", + "peft>=", + "trl>=", + "bentoml>=", + "mlflow>=", + ): + assert dependency in pyproject + assert (project / "src/demo_project/training.py").is_file() + assert (project / "src/demo_project/serving.py").is_file() + assert (project / "packages/training/pyproject.toml").is_file() + assert (project / "packages/service/pyproject.toml").is_file()