Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions .github/workflows/test-template.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
name: Test Cookiecutter Template

on:
pull_request:
branches: [main]
types: [opened, synchronize, reopened]

permissions:
contents: read

jobs:
test-template:
name: Test Cookiecutter Template
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Check if template directory changed
id: template-changed
run: |
BASE="${{ github.event.pull_request.base.sha }}"
HEAD="${{ github.event.pull_request.head.sha }}"
CHANGED=$(git diff --name-only "$BASE" "$HEAD" -- template/)
if [ -z "$CHANGED" ]; then
echo "changed=false" >> "$GITHUB_OUTPUT"
echo "No changes in template/ — skipping template tests."
else
echo "changed=true" >> "$GITHUB_OUTPUT"
echo "Changes detected in template/:"
echo "$CHANGED"
fi

- name: Set up Python
if: steps.template-changed.outputs.changed == 'true'
uses: actions/setup-python@v5
with:
python-version: "3.10"

- name: Install uv and cookiecutter
if: steps.template-changed.outputs.changed == 'true'
run: pip install uv cookiecutter

- name: Run cookiecutter template
if: steps.template-changed.outputs.changed == 'true'
run: |
cookiecutter template/ --no-input project_name="PR Test"

- name: Install project dependencies
if: steps.template-changed.outputs.changed == 'true'
working-directory: pr-test
run: uv sync

- name: Validate QType YAML
if: steps.template-changed.outputs.changed == 'true'
working-directory: pr-test
env:
OPENAI_API_KEY: DUMMY_KEY_FOR_VALIDATION
run: uv run qtype validate pr-test.qtype.yaml

- name: Regenerate tools YAML and check for drift
if: steps.template-changed.outputs.changed == 'true'
working-directory: pr-test
run: |
uv run qtype convert module pr_test.tools \
-o pr-test.tools.qtype.yaml.new
if ! diff -q pr-test.tools.qtype.yaml pr-test.tools.qtype.yaml.new; then
echo "❌ tools.qtype.yaml is out of sync with tools.py."
echo "Run 'qtype convert module pr_test.tools -o pr-test.tools.qtype.yaml'"
echo "and commit the result."
diff pr-test.tools.qtype.yaml pr-test.tools.qtype.yaml.new
exit 1
fi
echo "✅ tools.qtype.yaml matches tools.py"

- name: Set up Docker Buildx
if: steps.template-changed.outputs.changed == 'true'
uses: docker/setup-buildx-action@v3

- name: Build Docker image
if: steps.template-changed.outputs.changed == 'true'
working-directory: pr-test
run: docker build -t pr-test .
5 changes: 5 additions & 0 deletions template/cookiecutter.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"project_name": "My Project",
"__slug": "{{ cookiecutter.project_name.lower().replace(' ', '-') }}",
"__module": "{{ cookiecutter.__slug.replace('-', '_') }}"
}
42 changes: 42 additions & 0 deletions template/{{cookiecutter.__slug}}/.vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "QType Serve (debug with reload)",
"type": "debugpy",
"request": "launch",
"module": "qtype.cli",
"args": [
"serve",
"--reload",
"{{ cookiecutter.__slug }}.qtype.yaml"
],
"console": "integratedTerminal",
"cwd": "${workspaceFolder}",
"justMyCode": false,
"envFile": "${workspaceFolder}/.env"
},
{
"name": "QType Run (CLI)",
"type": "debugpy",
"request": "launch",
"module": "qtype.cli",
"args": [
"run",
"{{ cookiecutter.__slug }}.qtype.yaml",
"${input:cliArgs}"
],
"console": "integratedTerminal",
"cwd": "${workspaceFolder}",
"justMyCode": false,
"envFile": "${workspaceFolder}/.env"
}
],
"inputs": [
{
"id": "cliArgs",
"description": "Additional CLI arguments (e.g. '--flow ask --input \\'{}\\'')",
"type": "promptString"
}
]
}
22 changes: 22 additions & 0 deletions template/{{cookiecutter.__slug}}/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
FROM python:3.12-slim

WORKDIR /app

# Install uv for fast dependency management
RUN pip install --no-cache-dir uv

# Copy project definition first and install dependencies (cached layer)
COPY pyproject.toml .
RUN uv pip install --system .
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since the dependencies are likely heavier than the source code, and the source code changes more often, I think this line should be swapped with line 10. That way the python dependencies get cached on builds

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Swapped the order in commit 7dedc43pyproject.toml is now copied and dependencies installed before the source module is added, so the heavy dep layer gets cached on subsequent builds when only source changes.


# Copy the Python module source (changes more often than dependencies)
COPY {{ cookiecutter.__module }}/ ./{{ cookiecutter.__module }}/

# Copy QType application files
COPY *.qtype.yaml .

# Expose the default QType server port
EXPOSE 8000

# Start the QType server
CMD ["qtype", "serve", "{{ cookiecutter.__slug }}.qtype.yaml"]
85 changes: 85 additions & 0 deletions template/{{cookiecutter.__slug}}/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# {{ cookiecutter.project_name }}

A QType AI application. This project was generated from the
[QType cookiecutter template](https://github.com/bazaarvoice/qtype/tree/main/template).

## Prerequisites

- Python 3.10+
- [uv](https://docs.astral.sh/uv/) (recommended) or pip
- An OpenAI API key (set `OPENAI_API_KEY` in a `.env` file)

## Setup

```bash
# Install dependencies
uv sync

# Create a .env file with your API key
echo "OPENAI_API_KEY=sk-..." > .env
```

## Validate the application

Check your QType YAML for syntax errors, reference issues, and semantic problems:

```bash
qtype validate {{ cookiecutter.__slug }}.qtype.yaml
```

## Run the application

Start the QType server with auto-reload enabled for development:

```bash
qtype serve --reload {{ cookiecutter.__slug }}.qtype.yaml
```

The server will start at <http://localhost:8000>. Open your browser to see the
interactive UI.

## Run from the command line

Invoke a flow directly without starting the server:

```bash
qtype run {{ cookiecutter.__slug }}.qtype.yaml \
--flow ask \
--input '{"user_name": "Alice", "user_question": "What is machine learning?"}'
```

## Generate tool definitions from Python

Regenerate `{{ cookiecutter.__slug }}.tools.qtype.yaml` from
`{{ cookiecutter.__module }}/tools.py` whenever you add or modify tool functions:

```bash
qtype convert module {{ cookiecutter.__module }}.tools \
-o {{ cookiecutter.__slug }}.tools.qtype.yaml
```

## Project structure

```
{{ cookiecutter.__slug }}/
├── {{ cookiecutter.__module }}/ # Python tools package
│ ├── __init__.py
│ └── tools.py # Custom tool functions
├── {{ cookiecutter.__slug }}.qtype.yaml # Main QType application
├── {{ cookiecutter.__slug }}.tools.qtype.yaml # Generated tool definitions
├── pyproject.toml # Project metadata and dependencies
├── Dockerfile # Container image definition
└── .vscode/
└── launch.json # VS Code debug configurations
```

## Docker

Build and run the application in a container:

```bash
docker build -t {{ cookiecutter.__slug }} .

# Pass API keys at runtime (never bake secrets into the image)
docker run -p 8000:8000 --env-file .env {{ cookiecutter.__slug }}
```
18 changes: 18 additions & 0 deletions template/{{cookiecutter.__slug}}/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "{{ cookiecutter.__slug }}"
version = "0.0.1"
description = "{{ cookiecutter.project_name }} - A QType AI Application"
requires-python = ">=3.10"
dependencies = [
"qtype[interpreter,mcp]",
]

[tool.hatch.build.targets.wheel]
packages = ["{{ cookiecutter.__module }}"]

[tool.uv]
package = true
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""{{ cookiecutter.project_name }} tools package."""
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
"""Tools module for {{ cookiecutter.project_name }}.

This module provides custom tools that can be used in QType flows.
Run the following to regenerate the QType tool definitions:

qtype convert module {{ cookiecutter.__module }}.tools \\
-o {{ cookiecutter.__slug }}.tools.qtype.yaml
"""

from pydantic import BaseModel


class TextAnalysisResult(BaseModel):
"""Result of analyzing a block of text."""

word_count: int
char_count: int
sentence_count: int
avg_word_length: float


def greet(name: str) -> str:
"""Greet a person by name.

Args:
name: The person's name to greet.

Returns:
A personalized greeting message.
"""
return f"Hello, {name}! Welcome to {{ cookiecutter.project_name }}."


def analyze_text(text: str) -> TextAnalysisResult:
"""Analyze text and return statistics.

Args:
text: The text to analyze.

Returns:
TextAnalysisResult containing word count, character count,
sentence count, and average word length.
"""
words = text.split()
sentences = [
s.strip()
for s in text.replace("!", ".").replace("?", ".").split(".")
if s.strip()
]
avg_word_length = (
sum(len(w) for w in words) / len(words) if words else 0.0
)
return TextAnalysisResult(
word_count=len(words),
char_count=len(text),
sentence_count=len(sentences),
avg_word_length=round(avg_word_length, 2),
)
Loading