@msn-control/liftoff 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +48 -0
- package/dist/args.d.ts +5 -0
- package/dist/args.js +74 -0
- package/dist/args.js.map +1 -0
- package/dist/catalogs.d.ts +29 -0
- package/dist/catalogs.js +262 -0
- package/dist/catalogs.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +11 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands.d.ts +8 -0
- package/dist/commands.js +228 -0
- package/dist/commands.js.map +1 -0
- package/dist/file-system.d.ts +10 -0
- package/dist/file-system.js +65 -0
- package/dist/file-system.js.map +1 -0
- package/dist/interactive.d.ts +4 -0
- package/dist/interactive.js +140 -0
- package/dist/interactive.js.map +1 -0
- package/dist/planner.d.ts +14 -0
- package/dist/planner.js +118 -0
- package/dist/planner.js.map +1 -0
- package/dist/templates.d.ts +3 -0
- package/dist/templates.js +1148 -0
- package/dist/templates.js.map +1 -0
- package/dist/types.d.ts +96 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +51 -0
|
@@ -0,0 +1,1148 @@
|
|
|
1
|
+
const pyModule = (value) => value.replace(/-/g, '_');
|
|
2
|
+
const titleCase = (value) => value.replace(/(^|[-_\s])([a-z])/g, (_match, prefix, letter) => `${prefix ? ' ' : ''}${letter.toUpperCase()}`).trim();
|
|
3
|
+
export function buildArtifacts(plan) {
|
|
4
|
+
const artifacts = [];
|
|
5
|
+
const add = (logicalName, category, pathParts, content) => {
|
|
6
|
+
artifacts.push({ logicalName, category, pathParts, content: ensureTrailingNewline(content) });
|
|
7
|
+
};
|
|
8
|
+
addBaseArtifacts(add, plan);
|
|
9
|
+
addBackendArtifacts(add, plan);
|
|
10
|
+
addDatabaseArtifacts(add, plan);
|
|
11
|
+
addPatternArtifacts(add, plan);
|
|
12
|
+
addEnvironmentArtifacts(add, plan);
|
|
13
|
+
addDockerArtifacts(add, plan);
|
|
14
|
+
addInfrastructureArtifacts(add, plan);
|
|
15
|
+
addGovernanceArtifacts(add, plan);
|
|
16
|
+
if (plan.includeFrontend) {
|
|
17
|
+
addFrontendArtifacts(add, plan);
|
|
18
|
+
}
|
|
19
|
+
const manifest = buildManifest(plan, artifacts);
|
|
20
|
+
artifacts.push({
|
|
21
|
+
logicalName: 'manifest',
|
|
22
|
+
category: 'manifest',
|
|
23
|
+
pathParts: ['liftoff.manifest.json'],
|
|
24
|
+
content: `${JSON.stringify(manifest, null, 2)}\n`
|
|
25
|
+
});
|
|
26
|
+
return artifacts;
|
|
27
|
+
}
|
|
28
|
+
export function buildManifest(plan, artifacts) {
|
|
29
|
+
return {
|
|
30
|
+
artifactVersion: 1,
|
|
31
|
+
generatedBy: 'Mission Control Liftoff',
|
|
32
|
+
project: {
|
|
33
|
+
name: plan.projectName,
|
|
34
|
+
pattern: plan.pattern.id,
|
|
35
|
+
cloud: plan.provider.id,
|
|
36
|
+
region: plan.region.slug,
|
|
37
|
+
frontend: plan.includeFrontend,
|
|
38
|
+
specWorkflow: plan.specWorkflow.id,
|
|
39
|
+
environments: plan.environments.map((environment) => environment.id)
|
|
40
|
+
},
|
|
41
|
+
artifacts: artifacts.map((artifact) => ({
|
|
42
|
+
logicalName: artifact.logicalName,
|
|
43
|
+
category: artifact.category,
|
|
44
|
+
pathParts: artifact.pathParts
|
|
45
|
+
}))
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function addBaseArtifacts(add, plan) {
|
|
49
|
+
add('root-readme', 'documentation', ['README.md'], renderRootReadme(plan));
|
|
50
|
+
add('root-gitignore', 'project', ['.gitignore'], renderGeneratedGitignore());
|
|
51
|
+
add('liftoff-config', 'project', ['liftoff.config.json'], JSON.stringify({
|
|
52
|
+
projectName: plan.projectName,
|
|
53
|
+
pattern: plan.pattern.id,
|
|
54
|
+
cloud: plan.provider.id,
|
|
55
|
+
region: plan.region.slug,
|
|
56
|
+
includeFrontend: plan.includeFrontend,
|
|
57
|
+
environments: plan.environments.map((environment) => environment.id),
|
|
58
|
+
specWorkflow: plan.specWorkflow.id
|
|
59
|
+
}, null, 2));
|
|
60
|
+
add('env-example', 'configuration', ['.env.example'], renderEnvExample(plan));
|
|
61
|
+
add('backend-dockerfile', 'runtime', ['Dockerfile'], renderBackendDockerfile());
|
|
62
|
+
}
|
|
63
|
+
function addBackendArtifacts(add, plan) {
|
|
64
|
+
const routeModule = pyModule(plan.pattern.id);
|
|
65
|
+
add('backend-pyproject', 'backend', ['backend', 'pyproject.toml'], renderBackendPyproject(plan));
|
|
66
|
+
add('backend-package', 'backend', ['backend', '__init__.py'], '');
|
|
67
|
+
add('backend-api-package', 'backend', ['backend', 'apis', '__init__.py'], '');
|
|
68
|
+
add('backend-main', 'backend', ['backend', 'apis', 'main.py'], renderFastApiMain(plan, routeModule));
|
|
69
|
+
add('backend-health-routes', 'backend', ['backend', 'apis', 'routes', 'health.py'], renderHealthRoutes());
|
|
70
|
+
add('backend-pattern-routes', 'backend', ['backend', 'apis', 'routes', `${routeModule}.py`], renderPatternRoutes(plan));
|
|
71
|
+
add('backend-routes-package', 'backend', ['backend', 'apis', 'routes', '__init__.py'], '');
|
|
72
|
+
add('backend-auth-dependency', 'backend', ['backend', 'apis', 'dependencies', 'auth.py'], renderAuthDependency());
|
|
73
|
+
add('backend-config-package', 'backend', ['backend', 'config', '__init__.py'], '');
|
|
74
|
+
add('backend-settings', 'backend', ['backend', 'config', 'settings.py'], renderSettings(plan));
|
|
75
|
+
add('backend-orchestration-package', 'backend', ['backend', 'orchestration', '__init__.py'], '');
|
|
76
|
+
add('backend-model-config', 'backend', ['backend', 'orchestration', 'model_config.py'], renderModelConfig(plan));
|
|
77
|
+
add('backend-messaging-tool', 'backend', ['backend', 'orchestration', 'tools', 'messaging.py'], renderMessagingBoundary());
|
|
78
|
+
add('backend-tools-package', 'backend', ['backend', 'orchestration', 'tools', '__init__.py'], '');
|
|
79
|
+
add('backend-observability', 'backend', ['backend', 'observability', 'tracing.py'], renderTracing());
|
|
80
|
+
add('backend-observability-package', 'backend', ['backend', 'observability', '__init__.py'], '');
|
|
81
|
+
add('backend-test-health', 'backend-test', ['backend', 'tests', 'test_health.py'], renderBackendHealthTest());
|
|
82
|
+
}
|
|
83
|
+
function addDatabaseArtifacts(add, plan) {
|
|
84
|
+
add('database-alembic-ini', 'database', ['database', 'alembic.ini'], renderAlembicIni());
|
|
85
|
+
add('database-alembic-env', 'database', ['database', 'migrations', 'env.py'], renderAlembicEnv());
|
|
86
|
+
add('database-initial-migration', 'database', ['database', 'migrations', 'versions', '0001_initial.py'], renderInitialMigration(plan));
|
|
87
|
+
add('database-schema', 'database', ['database', 'models', 'schema.sql'], renderDatabaseSchema(plan));
|
|
88
|
+
}
|
|
89
|
+
function addPatternArtifacts(add, plan) {
|
|
90
|
+
const routeModule = pyModule(plan.pattern.id);
|
|
91
|
+
add('pattern-agent', 'pattern', ['backend', 'orchestration', 'agents', `${routeModule}_agent.py`], renderPatternAgent(plan));
|
|
92
|
+
add('pattern-prompt', 'pattern', ['backend', 'orchestration', 'prompts', `${plan.pattern.id}.md`], renderPromptTemplate(plan));
|
|
93
|
+
add('pattern-agent-package', 'pattern', ['backend', 'orchestration', 'agents', '__init__.py'], '');
|
|
94
|
+
add('pattern-prompt-readme', 'pattern', ['backend', 'orchestration', 'prompts', 'README.md'], renderPromptReadme());
|
|
95
|
+
if (plan.pattern.id === 'rag') {
|
|
96
|
+
add('rag-vector-store', 'pattern', ['backend', 'orchestration', 'retrieval', 'vector_store.py'], renderVectorStore());
|
|
97
|
+
add('rag-retrieval-package', 'pattern', ['backend', 'orchestration', 'retrieval', '__init__.py'], '');
|
|
98
|
+
}
|
|
99
|
+
if (plan.pattern.worker) {
|
|
100
|
+
add('pattern-worker', 'pattern', ['backend', 'workers', `${routeModule}_worker.py`], renderPatternWorker(plan));
|
|
101
|
+
add('backend-workers-package', 'pattern', ['backend', 'workers', '__init__.py'], '');
|
|
102
|
+
}
|
|
103
|
+
if (plan.pattern.id === 'fine-tuned') {
|
|
104
|
+
add('fine-tuned-eval-dataset', 'pattern', ['backend', 'evaluation', 'datasets', 'sample.jsonl'], '{"input":"Example request","expected":"Expected response placeholder"}');
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function addEnvironmentArtifacts(add, plan) {
|
|
108
|
+
for (const environment of plan.environments) {
|
|
109
|
+
add(`environment-${environment.id}-backend`, 'environment', ['environments', environment.id, 'backend.env'], renderBackendEnv(plan, environment.id));
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function addDockerArtifacts(add, plan) {
|
|
113
|
+
add('docker-compose', 'local-development', ['docker-compose.yml'], renderDockerCompose(plan));
|
|
114
|
+
}
|
|
115
|
+
function addInfrastructureArtifacts(add, plan) {
|
|
116
|
+
const base = ['infrastructure', 'opentofu', 'azure'];
|
|
117
|
+
add('opentofu-versions', 'infrastructure', [...base, 'versions.tf'], renderTofuVersions());
|
|
118
|
+
add('opentofu-providers', 'infrastructure', [...base, 'providers.tf'], renderTofuProviders());
|
|
119
|
+
add('opentofu-variables', 'infrastructure', [...base, 'variables.tf'], renderTofuVariables(plan));
|
|
120
|
+
add('opentofu-main', 'infrastructure', [...base, 'main.tf'], renderTofuMain(plan));
|
|
121
|
+
add('opentofu-outputs', 'infrastructure', [...base, 'outputs.tf'], renderTofuOutputs(plan));
|
|
122
|
+
add('opentofu-local-state', 'infrastructure', [...base, 'backend.local.tf'], renderTofuLocalState());
|
|
123
|
+
add('opentofu-remote-state-example', 'infrastructure', [...base, 'backend.remote.example.tf'], renderTofuRemoteStateExample());
|
|
124
|
+
add('opentofu-readme', 'infrastructure', [...base, 'README.md'], renderTofuReadme(plan));
|
|
125
|
+
for (const environment of plan.environments) {
|
|
126
|
+
add(`opentofu-${environment.id}-tfvars`, 'infrastructure', [...base, 'environments', `${environment.id}.tfvars`], renderTofuTfvars(plan, environment.id));
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
function addGovernanceArtifacts(add, plan) {
|
|
130
|
+
if (plan.specWorkflow.id === 'openspec') {
|
|
131
|
+
const changeName = `bootstrap-${plan.safeProjectName}`;
|
|
132
|
+
add('openspec-config', 'governance', ['openspec', 'config.yaml'], renderOpenSpecConfig(plan));
|
|
133
|
+
add('openspec-seed-change-metadata', 'governance', ['openspec', 'changes', changeName, '.openspec.yaml'], 'schema: spec-driven');
|
|
134
|
+
add('openspec-seed-proposal', 'governance', ['openspec', 'changes', changeName, 'proposal.md'], renderSeedProposal(plan));
|
|
135
|
+
add('openspec-seed-design', 'governance', ['openspec', 'changes', changeName, 'design.md'], renderSeedDesign(plan));
|
|
136
|
+
add('openspec-seed-tasks', 'governance', ['openspec', 'changes', changeName, 'tasks.md'], renderSeedTasks());
|
|
137
|
+
add('openspec-spec-placeholder', 'governance', ['openspec', 'specs', '.gitkeep'], '');
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
add('spec-kit-constitution', 'governance', ['.specify', 'memory', 'constitution.md'], renderSpecKitConstitution(plan));
|
|
141
|
+
add('spec-kit-spec-template', 'governance', ['.specify', 'templates', 'spec-template.md'], renderSpecKitSpecTemplate());
|
|
142
|
+
add('spec-kit-plan-template', 'governance', ['.specify', 'templates', 'plan-template.md'], renderSpecKitPlanTemplate());
|
|
143
|
+
add('specs-placeholder', 'governance', ['specs', '.gitkeep'], '');
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
function addFrontendArtifacts(add, plan) {
|
|
147
|
+
add('frontend-package', 'frontend', ['frontend', 'package.json'], renderFrontendPackage(plan));
|
|
148
|
+
add('frontend-index', 'frontend', ['frontend', 'index.html'], renderFrontendIndex(plan));
|
|
149
|
+
add('frontend-main', 'frontend', ['frontend', 'src', 'main.ts'], renderFrontendMain());
|
|
150
|
+
add('frontend-app', 'frontend', ['frontend', 'src', 'App.vue'], renderFrontendApp(plan));
|
|
151
|
+
add('frontend-styles', 'frontend', ['frontend', 'src', 'styles.css'], renderFrontendStyles());
|
|
152
|
+
add('frontend-vite-config', 'frontend', ['frontend', 'vite.config.ts'], renderFrontendViteConfig());
|
|
153
|
+
add('frontend-tailwind-config', 'frontend', ['frontend', 'tailwind.config.ts'], renderFrontendTailwindConfig());
|
|
154
|
+
add('frontend-dockerfile', 'frontend', ['frontend', 'Dockerfile'], renderFrontendDockerfile());
|
|
155
|
+
}
|
|
156
|
+
function renderRootReadme(plan) {
|
|
157
|
+
return `# ${plan.projectName}
|
|
158
|
+
|
|
159
|
+
Generated by Mission Control Liftoff.
|
|
160
|
+
|
|
161
|
+
## Stack
|
|
162
|
+
|
|
163
|
+
- Backend: FastAPI, PydanticAI, Pydantic settings, Scalar
|
|
164
|
+
- Pattern: ${plan.pattern.label}
|
|
165
|
+
- Cloud: ${plan.provider.label} (${plan.region.slug})
|
|
166
|
+
- Infrastructure: OpenTofu
|
|
167
|
+
- Database: PostgreSQL with Alembic migrations${plan.pattern.id === 'rag' ? ' and pgvector retrieval' : ''}
|
|
168
|
+
- Cache and local messaging: Redis
|
|
169
|
+
- Observability: Langfuse
|
|
170
|
+
- Local development: Docker Compose
|
|
171
|
+
${plan.includeFrontend ? '- Frontend: Vue 3 with Tailwind\n' : ''}
|
|
172
|
+
## Local Development
|
|
173
|
+
|
|
174
|
+
\`\`\`bash
|
|
175
|
+
docker compose up --build
|
|
176
|
+
docker compose --profile observability up --build
|
|
177
|
+
\`\`\`
|
|
178
|
+
|
|
179
|
+
The backend API is available on port 8000. Scalar is exposed at \`/scalar\`.
|
|
180
|
+
|
|
181
|
+
## Infrastructure
|
|
182
|
+
|
|
183
|
+
\`\`\`bash
|
|
184
|
+
cd infrastructure/opentofu/azure
|
|
185
|
+
tofu init
|
|
186
|
+
tofu plan -var-file=environments/dev.tfvars
|
|
187
|
+
tofu apply -var-file=environments/dev.tfvars
|
|
188
|
+
\`\`\`
|
|
189
|
+
|
|
190
|
+
## Spec-Driven Workflow
|
|
191
|
+
|
|
192
|
+
Selected workflow: ${plan.specWorkflow.label}.
|
|
193
|
+
`;
|
|
194
|
+
}
|
|
195
|
+
function renderGeneratedGitignore() {
|
|
196
|
+
return `.venv/
|
|
197
|
+
__pycache__/
|
|
198
|
+
.pytest_cache/
|
|
199
|
+
node_modules/
|
|
200
|
+
dist/
|
|
201
|
+
.env
|
|
202
|
+
*.tfstate
|
|
203
|
+
*.tfstate.*
|
|
204
|
+
.terraform/
|
|
205
|
+
`;
|
|
206
|
+
}
|
|
207
|
+
function renderEnvExample(plan) {
|
|
208
|
+
return `APP_ENV=dev
|
|
209
|
+
APP_NAME=${plan.safeProjectName}
|
|
210
|
+
GENAI_PATTERN=${plan.pattern.id}
|
|
211
|
+
CLOUD_PROVIDER=${plan.provider.id}
|
|
212
|
+
AZURE_REGION=${plan.region.slug}
|
|
213
|
+
DATABASE_URL=postgresql+asyncpg://postgres:postgres@postgres:5432/${plan.safeProjectName.replace(/-/g, '_')}
|
|
214
|
+
REDIS_URL=redis://redis:6379/0
|
|
215
|
+
MESSAGING_TRANSPORT=redis-streams
|
|
216
|
+
BLOB_ENDPOINT=http://azurite:10000/devstoreaccount1
|
|
217
|
+
LANGFUSE_HOST=http://langfuse:3000
|
|
218
|
+
`;
|
|
219
|
+
}
|
|
220
|
+
function renderBackendDockerfile() {
|
|
221
|
+
return `FROM python:3.12-slim
|
|
222
|
+
|
|
223
|
+
WORKDIR /app
|
|
224
|
+
|
|
225
|
+
ENV PYTHONDONTWRITEBYTECODE=1
|
|
226
|
+
ENV PYTHONUNBUFFERED=1
|
|
227
|
+
ENV PYTHONPATH=/app
|
|
228
|
+
|
|
229
|
+
COPY backend/pyproject.toml /app/backend/pyproject.toml
|
|
230
|
+
RUN pip install --no-cache-dir /app/backend
|
|
231
|
+
|
|
232
|
+
COPY backend /app/backend
|
|
233
|
+
COPY database /app/database
|
|
234
|
+
|
|
235
|
+
EXPOSE 8000
|
|
236
|
+
CMD ["uvicorn", "backend.apis.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
|
237
|
+
`;
|
|
238
|
+
}
|
|
239
|
+
function renderBackendPyproject(plan) {
|
|
240
|
+
return `[project]
|
|
241
|
+
name = "${plan.safeProjectName}-backend"
|
|
242
|
+
version = "0.1.0"
|
|
243
|
+
requires-python = ">=3.12"
|
|
244
|
+
dependencies = [
|
|
245
|
+
"fastapi>=0.111",
|
|
246
|
+
"uvicorn[standard]>=0.30",
|
|
247
|
+
"pydantic>=2.7",
|
|
248
|
+
"pydantic-settings>=2.3",
|
|
249
|
+
"pydantic-ai>=0.0.13",
|
|
250
|
+
"scalar-fastapi>=1.0",
|
|
251
|
+
"sqlalchemy[asyncio]>=2.0",
|
|
252
|
+
"asyncpg>=0.29",
|
|
253
|
+
"alembic>=1.13",
|
|
254
|
+
"redis>=5.0",
|
|
255
|
+
"langfuse>=2.39",
|
|
256
|
+
"azure-servicebus>=7.12",
|
|
257
|
+
"azure-storage-blob>=12.20",
|
|
258
|
+
"azure-communication-email>=1.0"
|
|
259
|
+
]
|
|
260
|
+
|
|
261
|
+
[project.optional-dependencies]
|
|
262
|
+
test = ["pytest>=8.2", "httpx>=0.27"]
|
|
263
|
+
|
|
264
|
+
[build-system]
|
|
265
|
+
requires = ["setuptools>=70"]
|
|
266
|
+
build-backend = "setuptools.build_meta"
|
|
267
|
+
|
|
268
|
+
[tool.pytest.ini_options]
|
|
269
|
+
pythonpath = [".."]
|
|
270
|
+
testpaths = ["tests"]
|
|
271
|
+
`;
|
|
272
|
+
}
|
|
273
|
+
function renderFastApiMain(plan, routeModule) {
|
|
274
|
+
return `from fastapi import FastAPI
|
|
275
|
+
|
|
276
|
+
try:
|
|
277
|
+
from scalar_fastapi import get_scalar_api_reference
|
|
278
|
+
except ImportError: # pragma: no cover - dependency is present in generated runtime
|
|
279
|
+
get_scalar_api_reference = None
|
|
280
|
+
|
|
281
|
+
from backend.apis.routes import health, ${routeModule}
|
|
282
|
+
from backend.config.settings import get_settings
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
settings = get_settings()
|
|
286
|
+
app = FastAPI(title=settings.app_name, version="0.1.0")
|
|
287
|
+
|
|
288
|
+
app.include_router(health.router)
|
|
289
|
+
app.include_router(${routeModule}.router)
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
@app.get("/scalar", include_in_schema=False)
|
|
293
|
+
def scalar_reference():
|
|
294
|
+
if get_scalar_api_reference is None:
|
|
295
|
+
return {"message": "Install scalar-fastapi to enable the Scalar developer portal."}
|
|
296
|
+
return get_scalar_api_reference(openapi_url=app.openapi_url, title=f"{app.title} API")
|
|
297
|
+
`;
|
|
298
|
+
}
|
|
299
|
+
function renderHealthRoutes() {
|
|
300
|
+
return `from fastapi import APIRouter
|
|
301
|
+
|
|
302
|
+
router = APIRouter(tags=["operations"])
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
@router.get("/health")
|
|
306
|
+
def health():
|
|
307
|
+
return {"status": "ok"}
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
@router.get("/ready")
|
|
311
|
+
def ready():
|
|
312
|
+
return {"status": "ready"}
|
|
313
|
+
`;
|
|
314
|
+
}
|
|
315
|
+
function renderPatternRoutes(plan) {
|
|
316
|
+
const moduleName = pyModule(plan.pattern.id);
|
|
317
|
+
const agentName = `${moduleName}_agent`;
|
|
318
|
+
const prefix = plan.pattern.routePrefix;
|
|
319
|
+
if (plan.pattern.id === 'streaming') {
|
|
320
|
+
return `from fastapi import APIRouter
|
|
321
|
+
from fastapi.responses import StreamingResponse
|
|
322
|
+
|
|
323
|
+
from backend.orchestration.agents.${agentName} import stream_response
|
|
324
|
+
|
|
325
|
+
router = APIRouter(prefix="${prefix}", tags=["${plan.pattern.id}"])
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
@router.get("")
|
|
329
|
+
def stream(prompt: str):
|
|
330
|
+
return StreamingResponse(stream_response(prompt), media_type="text/event-stream")
|
|
331
|
+
`;
|
|
332
|
+
}
|
|
333
|
+
if (plan.pattern.id === 'rag') {
|
|
334
|
+
return `from fastapi import APIRouter
|
|
335
|
+
from pydantic import BaseModel
|
|
336
|
+
|
|
337
|
+
from backend.orchestration.agents.${agentName} import answer_question, enqueue_ingestion
|
|
338
|
+
|
|
339
|
+
router = APIRouter(prefix="${prefix}", tags=["rag"])
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
class QueryRequest(BaseModel):
|
|
343
|
+
question: str
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
class IngestionRequest(BaseModel):
|
|
347
|
+
source_uri: str
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
@router.post("/query")
|
|
351
|
+
async def query(request: QueryRequest):
|
|
352
|
+
return await answer_question(request.question)
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
@router.post("/ingest")
|
|
356
|
+
async def ingest(request: IngestionRequest):
|
|
357
|
+
return await enqueue_ingestion(request.source_uri)
|
|
358
|
+
`;
|
|
359
|
+
}
|
|
360
|
+
const bodyClass = `${titleCase(plan.pattern.id).replace(/\s/g, '')}Request`;
|
|
361
|
+
return `from fastapi import APIRouter
|
|
362
|
+
from pydantic import BaseModel
|
|
363
|
+
|
|
364
|
+
from backend.orchestration.agents.${agentName} import run_${moduleName}
|
|
365
|
+
|
|
366
|
+
router = APIRouter(prefix="${prefix}", tags=["${plan.pattern.id}"])
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
class ${bodyClass}(BaseModel):
|
|
370
|
+
input: str
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
@router.post("/run")
|
|
374
|
+
async def run(request: ${bodyClass}):
|
|
375
|
+
return await run_${moduleName}(request.input)
|
|
376
|
+
`;
|
|
377
|
+
}
|
|
378
|
+
function renderAuthDependency() {
|
|
379
|
+
return `from dataclasses import dataclass
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
@dataclass(frozen=True)
|
|
383
|
+
class CurrentUser:
|
|
384
|
+
subject: str = "local-developer"
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
async def get_current_user() -> CurrentUser:
|
|
388
|
+
return CurrentUser()
|
|
389
|
+
`;
|
|
390
|
+
}
|
|
391
|
+
function renderSettings(plan) {
|
|
392
|
+
return `from functools import lru_cache
|
|
393
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
class Settings(BaseSettings):
|
|
397
|
+
model_config = SettingsConfigDict(env_file=".env", extra="ignore")
|
|
398
|
+
|
|
399
|
+
app_name: str = "${plan.projectName}"
|
|
400
|
+
app_env: str = "dev"
|
|
401
|
+
genai_pattern: str = "${plan.pattern.id}"
|
|
402
|
+
cloud_provider: str = "${plan.provider.id}"
|
|
403
|
+
azure_region: str = "${plan.region.slug}"
|
|
404
|
+
database_url: str
|
|
405
|
+
redis_url: str
|
|
406
|
+
messaging_transport: str = "redis-streams"
|
|
407
|
+
blob_endpoint: str | None = None
|
|
408
|
+
langfuse_host: str | None = None
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
@lru_cache
|
|
412
|
+
def get_settings() -> Settings:
|
|
413
|
+
return Settings()
|
|
414
|
+
`;
|
|
415
|
+
}
|
|
416
|
+
function renderModelConfig(plan) {
|
|
417
|
+
return `from pydantic import BaseModel, Field
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
class ModelConfig(BaseModel):
|
|
421
|
+
provider: str = Field(default="azure-openai")
|
|
422
|
+
deployment_name: str = Field(default="gpt-4.1")
|
|
423
|
+
embedding_deployment_name: str = Field(default="text-embedding-3-large")
|
|
424
|
+
pattern: str = Field(default="${plan.pattern.id}")
|
|
425
|
+
`;
|
|
426
|
+
}
|
|
427
|
+
function renderMessagingBoundary() {
|
|
428
|
+
return `from typing import Protocol
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
class MessagePublisher(Protocol):
|
|
432
|
+
async def publish(self, topic: str, payload: dict) -> None:
|
|
433
|
+
...
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
class RedisStreamPublisher:
|
|
437
|
+
async def publish(self, topic: str, payload: dict) -> None:
|
|
438
|
+
# Implement with redis.asyncio in application code.
|
|
439
|
+
return None
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
class AzureServiceBusPublisher:
|
|
443
|
+
async def publish(self, topic: str, payload: dict) -> None:
|
|
444
|
+
# Implement with azure-servicebus in cloud configuration.
|
|
445
|
+
return None
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
def build_message_publisher(transport: str) -> MessagePublisher:
|
|
449
|
+
if transport == "azure-service-bus":
|
|
450
|
+
return AzureServiceBusPublisher()
|
|
451
|
+
return RedisStreamPublisher()
|
|
452
|
+
`;
|
|
453
|
+
}
|
|
454
|
+
function renderTracing() {
|
|
455
|
+
return `from contextlib import asynccontextmanager
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
@asynccontextmanager
|
|
459
|
+
async def trace_llm_operation(name: str):
|
|
460
|
+
# Wire Langfuse callbacks here when model credentials are configured.
|
|
461
|
+
yield {"trace": name}
|
|
462
|
+
`;
|
|
463
|
+
}
|
|
464
|
+
function renderBackendHealthTest() {
|
|
465
|
+
return `from fastapi.testclient import TestClient
|
|
466
|
+
|
|
467
|
+
from backend.apis.main import app
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
def test_health():
|
|
471
|
+
client = TestClient(app)
|
|
472
|
+
response = client.get("/health")
|
|
473
|
+
assert response.status_code == 200
|
|
474
|
+
assert response.json()["status"] == "ok"
|
|
475
|
+
`;
|
|
476
|
+
}
|
|
477
|
+
function renderAlembicIni() {
|
|
478
|
+
return `[alembic]
|
|
479
|
+
script_location = migrations
|
|
480
|
+
sqlalchemy.url = driver://user:pass@localhost/dbname
|
|
481
|
+
`;
|
|
482
|
+
}
|
|
483
|
+
function renderAlembicEnv() {
|
|
484
|
+
return `from alembic import context
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
def run_migrations_online():
|
|
488
|
+
context.configure(url=context.get_x_argument(as_dictionary=True).get("database_url"))
|
|
489
|
+
with context.begin_transaction():
|
|
490
|
+
context.run_migrations()
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
run_migrations_online()
|
|
494
|
+
`;
|
|
495
|
+
}
|
|
496
|
+
function renderInitialMigration(plan) {
|
|
497
|
+
const vectorExtension = plan.pattern.id === 'rag' ? ' op.execute("CREATE EXTENSION IF NOT EXISTS vector")\n' : '';
|
|
498
|
+
return `from alembic import op
|
|
499
|
+
import sqlalchemy as sa
|
|
500
|
+
|
|
501
|
+
revision = "0001_initial"
|
|
502
|
+
down_revision = None
|
|
503
|
+
branch_labels = None
|
|
504
|
+
depends_on = None
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
def upgrade():
|
|
508
|
+
${vectorExtension} op.create_table(
|
|
509
|
+
"events",
|
|
510
|
+
sa.Column("id", sa.Integer(), primary_key=True),
|
|
511
|
+
sa.Column("event_type", sa.String(length=120), nullable=False),
|
|
512
|
+
sa.Column("payload", sa.JSON(), nullable=False),
|
|
513
|
+
sa.Column("created_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
|
|
514
|
+
)
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
def downgrade():
|
|
518
|
+
op.drop_table("events")
|
|
519
|
+
`;
|
|
520
|
+
}
|
|
521
|
+
function renderDatabaseSchema(plan) {
|
|
522
|
+
return `CREATE TABLE IF NOT EXISTS events (
|
|
523
|
+
id SERIAL PRIMARY KEY,
|
|
524
|
+
event_type VARCHAR(120) NOT NULL,
|
|
525
|
+
payload JSONB NOT NULL,
|
|
526
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
527
|
+
);
|
|
528
|
+
${plan.pattern.id === 'rag' ? '\nCREATE EXTENSION IF NOT EXISTS vector;\n' : ''}`;
|
|
529
|
+
}
|
|
530
|
+
function renderPatternAgent(plan) {
|
|
531
|
+
const moduleName = pyModule(plan.pattern.id);
|
|
532
|
+
if (plan.pattern.id === 'rag') {
|
|
533
|
+
return `from backend.orchestration.tools.messaging import build_message_publisher
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
async def answer_question(question: str) -> dict:
|
|
537
|
+
return {
|
|
538
|
+
"answer": "Replace this placeholder with PydanticAI retrieval orchestration.",
|
|
539
|
+
"question": question,
|
|
540
|
+
"citations": [],
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
async def enqueue_ingestion(source_uri: str) -> dict:
|
|
545
|
+
publisher = build_message_publisher("redis-streams")
|
|
546
|
+
await publisher.publish("rag.ingest", {"source_uri": source_uri})
|
|
547
|
+
return {"status": "queued", "source_uri": source_uri}
|
|
548
|
+
`;
|
|
549
|
+
}
|
|
550
|
+
if (plan.pattern.id === 'streaming') {
|
|
551
|
+
return `async def stream_response(prompt: str):
|
|
552
|
+
yield f"data: Starting response for {prompt}\\n\\n"
|
|
553
|
+
yield "data: Replace this placeholder with PydanticAI streaming orchestration.\\n\\n"
|
|
554
|
+
`;
|
|
555
|
+
}
|
|
556
|
+
return `async def run_${moduleName}(input_text: str) -> dict:
|
|
557
|
+
return {
|
|
558
|
+
"result": "Replace this placeholder with ${plan.pattern.label} PydanticAI orchestration.",
|
|
559
|
+
"input": input_text,
|
|
560
|
+
}
|
|
561
|
+
`;
|
|
562
|
+
}
|
|
563
|
+
function renderPromptTemplate(plan) {
|
|
564
|
+
return `# ${plan.pattern.label} Prompt
|
|
565
|
+
|
|
566
|
+
You are implementing a ${plan.pattern.label} generated by Mission Control Liftoff.
|
|
567
|
+
|
|
568
|
+
Use PydanticAI orchestration and return outputs that match the API contract.
|
|
569
|
+
`;
|
|
570
|
+
}
|
|
571
|
+
function renderPromptReadme() {
|
|
572
|
+
return `# Prompt Templates
|
|
573
|
+
|
|
574
|
+
Store prompt templates here and reference them from the PydanticAI orchestration layer.
|
|
575
|
+
`;
|
|
576
|
+
}
|
|
577
|
+
function renderVectorStore() {
|
|
578
|
+
return `from typing import Protocol
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
class VectorStore(Protocol):
|
|
582
|
+
async def search(self, query: str, limit: int = 5) -> list[dict]:
|
|
583
|
+
...
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
class PgVectorStore:
|
|
587
|
+
async def search(self, query: str, limit: int = 5) -> list[dict]:
|
|
588
|
+
return []
|
|
589
|
+
`;
|
|
590
|
+
}
|
|
591
|
+
function renderPatternWorker(plan) {
|
|
592
|
+
return `async def run_worker() -> None:
|
|
593
|
+
# Consume ${plan.pattern.label} jobs from the configured messaging boundary.
|
|
594
|
+
return None
|
|
595
|
+
`;
|
|
596
|
+
}
|
|
597
|
+
function renderBackendEnv(plan, environment) {
|
|
598
|
+
const transport = environment === 'dev' ? 'redis-streams' : 'azure-service-bus';
|
|
599
|
+
return `APP_ENV=${environment}
|
|
600
|
+
APP_NAME=${plan.safeProjectName}
|
|
601
|
+
GENAI_PATTERN=${plan.pattern.id}
|
|
602
|
+
CLOUD_PROVIDER=${plan.provider.id}
|
|
603
|
+
AZURE_REGION=${plan.region.slug}
|
|
604
|
+
DATABASE_URL=postgresql+asyncpg://postgres:postgres@postgres:5432/${plan.safeProjectName.replace(/-/g, '_')}
|
|
605
|
+
REDIS_URL=redis://redis:6379/0
|
|
606
|
+
MESSAGING_TRANSPORT=${transport}
|
|
607
|
+
BLOB_ENDPOINT=
|
|
608
|
+
LANGFUSE_HOST=
|
|
609
|
+
`;
|
|
610
|
+
}
|
|
611
|
+
function renderDockerCompose(plan) {
|
|
612
|
+
const frontendService = plan.includeFrontend ? `
|
|
613
|
+
frontend:
|
|
614
|
+
build:
|
|
615
|
+
context: ./frontend
|
|
616
|
+
ports:
|
|
617
|
+
- "5173:80"
|
|
618
|
+
depends_on:
|
|
619
|
+
- backend
|
|
620
|
+
` : '';
|
|
621
|
+
return `services:
|
|
622
|
+
backend:
|
|
623
|
+
build:
|
|
624
|
+
context: .
|
|
625
|
+
dockerfile: Dockerfile
|
|
626
|
+
env_file:
|
|
627
|
+
- ./environments/dev/backend.env
|
|
628
|
+
ports:
|
|
629
|
+
- "8000:8000"
|
|
630
|
+
depends_on:
|
|
631
|
+
- postgres
|
|
632
|
+
- redis
|
|
633
|
+
- azurite
|
|
634
|
+
- mailpit
|
|
635
|
+
${frontendService}
|
|
636
|
+
postgres:
|
|
637
|
+
image: pgvector/pgvector:pg16
|
|
638
|
+
environment:
|
|
639
|
+
POSTGRES_USER: postgres
|
|
640
|
+
POSTGRES_PASSWORD: postgres
|
|
641
|
+
POSTGRES_DB: ${plan.safeProjectName.replace(/-/g, '_')}
|
|
642
|
+
ports:
|
|
643
|
+
- "5432:5432"
|
|
644
|
+
|
|
645
|
+
redis:
|
|
646
|
+
image: redis:7-alpine
|
|
647
|
+
ports:
|
|
648
|
+
- "6379:6379"
|
|
649
|
+
|
|
650
|
+
azurite:
|
|
651
|
+
image: mcr.microsoft.com/azure-storage/azurite
|
|
652
|
+
command: azurite --blobHost 0.0.0.0
|
|
653
|
+
ports:
|
|
654
|
+
- "10000:10000"
|
|
655
|
+
|
|
656
|
+
mailpit:
|
|
657
|
+
image: axllent/mailpit:latest
|
|
658
|
+
ports:
|
|
659
|
+
- "8025:8025"
|
|
660
|
+
|
|
661
|
+
langfuse:
|
|
662
|
+
image: langfuse/langfuse:2
|
|
663
|
+
profiles:
|
|
664
|
+
- observability
|
|
665
|
+
environment:
|
|
666
|
+
DATABASE_URL: postgresql://postgres:postgres@postgres:5432/${plan.safeProjectName.replace(/-/g, '_')}
|
|
667
|
+
NEXTAUTH_SECRET: local-development-placeholder
|
|
668
|
+
SALT: local-development-placeholder
|
|
669
|
+
ports:
|
|
670
|
+
- "3000:3000"
|
|
671
|
+
`;
|
|
672
|
+
}
|
|
673
|
+
function renderTofuVersions() {
|
|
674
|
+
return `terraform {
|
|
675
|
+
required_version = ">= 1.6.0"
|
|
676
|
+
required_providers {
|
|
677
|
+
azurerm = {
|
|
678
|
+
source = "hashicorp/azurerm"
|
|
679
|
+
version = "~> 3.110"
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
`;
|
|
684
|
+
}
|
|
685
|
+
function renderTofuProviders() {
|
|
686
|
+
return `provider "azurerm" {
|
|
687
|
+
features {}
|
|
688
|
+
}
|
|
689
|
+
`;
|
|
690
|
+
}
|
|
691
|
+
function renderTofuVariables(plan) {
|
|
692
|
+
return `variable "environment" {
|
|
693
|
+
type = string
|
|
694
|
+
description = "Deployment environment name."
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
variable "location" {
|
|
698
|
+
type = string
|
|
699
|
+
description = "Azure region slug."
|
|
700
|
+
default = "${plan.region.slug}"
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
variable "resource_suffix" {
|
|
704
|
+
type = string
|
|
705
|
+
description = "Globally unique suffix for Azure resource names."
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
variable "postgres_admin_password" {
|
|
709
|
+
type = string
|
|
710
|
+
sensitive = true
|
|
711
|
+
description = "PostgreSQL administrator password supplied at apply time."
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
variable "enable_private_networking" {
|
|
715
|
+
type = bool
|
|
716
|
+
default = false
|
|
717
|
+
description = "Enable production-oriented private-networking-ready settings."
|
|
718
|
+
}
|
|
719
|
+
`;
|
|
720
|
+
}
|
|
721
|
+
function renderTofuMain(plan) {
|
|
722
|
+
const frontendContainer = plan.includeFrontend ? `
|
|
723
|
+
resource "azurerm_container_app" "frontend" {
|
|
724
|
+
name = "ca-${plan.safeProjectName}-frontend-\${var.environment}"
|
|
725
|
+
container_app_environment_id = azurerm_container_app_environment.main.id
|
|
726
|
+
resource_group_name = azurerm_resource_group.main.name
|
|
727
|
+
revision_mode = "Single"
|
|
728
|
+
|
|
729
|
+
template {
|
|
730
|
+
container {
|
|
731
|
+
name = "frontend"
|
|
732
|
+
image = "\${azurerm_container_registry.main.login_server}/${plan.safeProjectName}-frontend:latest"
|
|
733
|
+
cpu = 0.25
|
|
734
|
+
memory = "0.5Gi"
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
ingress {
|
|
739
|
+
external_enabled = true
|
|
740
|
+
target_port = 80
|
|
741
|
+
traffic_weight {
|
|
742
|
+
percentage = 100
|
|
743
|
+
latest_revision = true
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
` : '';
|
|
748
|
+
return `locals {
|
|
749
|
+
name_prefix = "${plan.safeProjectName}-\${var.environment}"
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
resource "azurerm_resource_group" "main" {
|
|
753
|
+
name = "rg-\${local.name_prefix}"
|
|
754
|
+
location = var.location
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
resource "azurerm_container_registry" "main" {
|
|
758
|
+
name = "acr\${replace(var.resource_suffix, "-", "")}"
|
|
759
|
+
resource_group_name = azurerm_resource_group.main.name
|
|
760
|
+
location = azurerm_resource_group.main.location
|
|
761
|
+
sku = "Basic"
|
|
762
|
+
admin_enabled = false
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
resource "azurerm_user_assigned_identity" "app" {
|
|
766
|
+
name = "id-\${local.name_prefix}"
|
|
767
|
+
resource_group_name = azurerm_resource_group.main.name
|
|
768
|
+
location = azurerm_resource_group.main.location
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
resource "azurerm_container_app_environment" "main" {
|
|
772
|
+
name = "cae-\${local.name_prefix}"
|
|
773
|
+
resource_group_name = azurerm_resource_group.main.name
|
|
774
|
+
location = azurerm_resource_group.main.location
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
resource "azurerm_container_app" "backend" {
|
|
778
|
+
name = "ca-${plan.safeProjectName}-backend-\${var.environment}"
|
|
779
|
+
container_app_environment_id = azurerm_container_app_environment.main.id
|
|
780
|
+
resource_group_name = azurerm_resource_group.main.name
|
|
781
|
+
revision_mode = "Single"
|
|
782
|
+
|
|
783
|
+
identity {
|
|
784
|
+
type = "UserAssigned"
|
|
785
|
+
identity_ids = [azurerm_user_assigned_identity.app.id]
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
template {
|
|
789
|
+
container {
|
|
790
|
+
name = "backend"
|
|
791
|
+
image = "\${azurerm_container_registry.main.login_server}/${plan.safeProjectName}-backend:latest"
|
|
792
|
+
cpu = 0.5
|
|
793
|
+
memory = "1Gi"
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
ingress {
|
|
798
|
+
external_enabled = true
|
|
799
|
+
target_port = 8000
|
|
800
|
+
traffic_weight {
|
|
801
|
+
percentage = 100
|
|
802
|
+
latest_revision = true
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
${frontendContainer}
|
|
807
|
+
resource "azurerm_postgresql_flexible_server" "main" {
|
|
808
|
+
name = "psql-\${local.name_prefix}-\${var.resource_suffix}"
|
|
809
|
+
resource_group_name = azurerm_resource_group.main.name
|
|
810
|
+
location = azurerm_resource_group.main.location
|
|
811
|
+
version = "16"
|
|
812
|
+
administrator_login = "liftoffadmin"
|
|
813
|
+
administrator_password = var.postgres_admin_password
|
|
814
|
+
storage_mb = 32768
|
|
815
|
+
sku_name = "B_Standard_B1ms"
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
resource "azurerm_redis_cache" "main" {
|
|
819
|
+
name = "redis-\${local.name_prefix}-\${var.resource_suffix}"
|
|
820
|
+
location = azurerm_resource_group.main.location
|
|
821
|
+
resource_group_name = azurerm_resource_group.main.name
|
|
822
|
+
capacity = 0
|
|
823
|
+
family = "C"
|
|
824
|
+
sku_name = "Basic"
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
resource "azurerm_storage_account" "main" {
|
|
828
|
+
name = "st\${replace(var.resource_suffix, "-", "")}"
|
|
829
|
+
resource_group_name = azurerm_resource_group.main.name
|
|
830
|
+
location = azurerm_resource_group.main.location
|
|
831
|
+
account_tier = "Standard"
|
|
832
|
+
account_replication_type = "LRS"
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
resource "azurerm_storage_container" "documents" {
|
|
836
|
+
name = "documents"
|
|
837
|
+
storage_account_name = azurerm_storage_account.main.name
|
|
838
|
+
container_access_type = "private"
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
resource "azurerm_servicebus_namespace" "main" {
|
|
842
|
+
name = "sb-\${local.name_prefix}-\${var.resource_suffix}"
|
|
843
|
+
location = azurerm_resource_group.main.location
|
|
844
|
+
resource_group_name = azurerm_resource_group.main.name
|
|
845
|
+
sku = "Standard"
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
resource "azurerm_servicebus_queue" "events" {
|
|
849
|
+
name = "events"
|
|
850
|
+
namespace_id = azurerm_servicebus_namespace.main.id
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
resource "azurerm_communication_service" "main" {
|
|
854
|
+
name = "acs-\${local.name_prefix}"
|
|
855
|
+
resource_group_name = azurerm_resource_group.main.name
|
|
856
|
+
data_location = "United States"
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
resource "azurerm_key_vault" "main" {
|
|
860
|
+
name = "kv-\${local.name_prefix}-\${var.resource_suffix}"
|
|
861
|
+
location = azurerm_resource_group.main.location
|
|
862
|
+
resource_group_name = azurerm_resource_group.main.name
|
|
863
|
+
tenant_id = data.azurerm_client_config.current.tenant_id
|
|
864
|
+
sku_name = "standard"
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
data "azurerm_client_config" "current" {}
|
|
868
|
+
`;
|
|
869
|
+
}
|
|
870
|
+
function renderTofuOutputs(plan) {
|
|
871
|
+
return `output "backend_url" {
|
|
872
|
+
value = azurerm_container_app.backend.ingress[0].fqdn
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
${plan.includeFrontend ? `output "frontend_url" {
|
|
876
|
+
value = azurerm_container_app.frontend.ingress[0].fqdn
|
|
877
|
+
}
|
|
878
|
+
` : ''}output "container_registry" {
|
|
879
|
+
value = azurerm_container_registry.main.login_server
|
|
880
|
+
}
|
|
881
|
+
`;
|
|
882
|
+
}
|
|
883
|
+
function renderTofuLocalState() {
|
|
884
|
+
return `# Local state is the V1 default for first-use simplicity.
|
|
885
|
+
# Teams can replace this file with backend.remote.example.tf when adopting shared state.
|
|
886
|
+
`;
|
|
887
|
+
}
|
|
888
|
+
function renderTofuRemoteStateExample() {
|
|
889
|
+
return `# Rename to backend.tf and configure values for shared state.
|
|
890
|
+
# terraform {
|
|
891
|
+
# backend "azurerm" {
|
|
892
|
+
# resource_group_name = "rg-opentofu-state"
|
|
893
|
+
# storage_account_name = "stliftoffstate"
|
|
894
|
+
# container_name = "tfstate"
|
|
895
|
+
# key = "mission-control/liftoff.tfstate"
|
|
896
|
+
# }
|
|
897
|
+
# }
|
|
898
|
+
`;
|
|
899
|
+
}
|
|
900
|
+
function renderTofuReadme(plan) {
|
|
901
|
+
const env = plan.environments[0]?.id ?? 'dev';
|
|
902
|
+
return `# Azure OpenTofu
|
|
903
|
+
|
|
904
|
+
Azure is the complete V1 provider for this Liftoff project.
|
|
905
|
+
|
|
906
|
+
\`\`\`bash
|
|
907
|
+
tofu init
|
|
908
|
+
tofu plan -var-file=environments/${env}.tfvars
|
|
909
|
+
tofu apply -var-file=environments/${env}.tfvars
|
|
910
|
+
tofu output
|
|
911
|
+
\`\`\`
|
|
912
|
+
|
|
913
|
+
Local OpenTofu state is generated by default. Use \`backend.remote.example.tf\` as the starting point for team remote state.
|
|
914
|
+
`;
|
|
915
|
+
}
|
|
916
|
+
function renderTofuTfvars(plan, environment) {
|
|
917
|
+
return `environment = "${environment}"
|
|
918
|
+
location = "${plan.region.slug}"
|
|
919
|
+
resource_suffix = "${plan.safeProjectName.replace(/-/g, '')}${environment}"
|
|
920
|
+
enable_private_networking = ${environment === 'prod' ? 'true' : 'false'}
|
|
921
|
+
`;
|
|
922
|
+
}
|
|
923
|
+
function renderOpenSpecConfig(plan) {
|
|
924
|
+
const frontendRule = plan.includeFrontend ? '\n - Keep frontend code under frontend.' : '';
|
|
925
|
+
return `schema: spec-driven
|
|
926
|
+
|
|
927
|
+
context: |
|
|
928
|
+
Project generated by Mission Control Liftoff.
|
|
929
|
+
GenAI pattern: ${plan.pattern.label}
|
|
930
|
+
Application framework: FastAPI + PydanticAI.
|
|
931
|
+
API developer portal: Scalar.
|
|
932
|
+
Infrastructure: OpenTofu.
|
|
933
|
+
Primary cloud: Azure (${plan.region.slug}).
|
|
934
|
+
Local development: Docker Compose.
|
|
935
|
+
Database: PostgreSQL with Alembic migrations.
|
|
936
|
+
Cache and local messaging: Redis.
|
|
937
|
+
Observability: Langfuse.
|
|
938
|
+
Environments: ${plan.environments.map((environment) => environment.id).join(', ')}.
|
|
939
|
+
|
|
940
|
+
rules:
|
|
941
|
+
specs:
|
|
942
|
+
- Requirements must describe observable product behavior.
|
|
943
|
+
- Cloud behavior must identify environment differences for generated environments.
|
|
944
|
+
design:
|
|
945
|
+
- Use PydanticAI for orchestration logic.
|
|
946
|
+
- Use Pydantic settings models for runtime configuration.
|
|
947
|
+
- Use OpenTofu for infrastructure changes.${frontendRule}
|
|
948
|
+
- Keep backend API code under backend/apis.
|
|
949
|
+
- Keep database artifacts under database.
|
|
950
|
+
tasks:
|
|
951
|
+
- Include local Docker Compose verification.
|
|
952
|
+
- Include OpenTofu validation for generated infrastructure.
|
|
953
|
+
`;
|
|
954
|
+
}
|
|
955
|
+
function renderSeedProposal(plan) {
|
|
956
|
+
return `## Why
|
|
957
|
+
|
|
958
|
+
Bootstrap the generated ${plan.pattern.label} application baseline created by Mission Control Liftoff.
|
|
959
|
+
|
|
960
|
+
## What Changes
|
|
961
|
+
|
|
962
|
+
- Establish the approved backend, infrastructure, local development, and governance baseline.
|
|
963
|
+
- Capture follow-up product requirements through spec-driven changes.
|
|
964
|
+
|
|
965
|
+
## Capabilities
|
|
966
|
+
|
|
967
|
+
### New Capabilities
|
|
968
|
+
|
|
969
|
+
- \`${plan.pattern.id}-application-baseline\`: Generated application baseline for this Liftoff project.
|
|
970
|
+
|
|
971
|
+
### Modified Capabilities
|
|
972
|
+
|
|
973
|
+
- None.
|
|
974
|
+
|
|
975
|
+
## Impact
|
|
976
|
+
|
|
977
|
+
- Generated FastAPI/PydanticAI backend, OpenTofu infrastructure, Docker Compose local development, and governance files.
|
|
978
|
+
`;
|
|
979
|
+
}
|
|
980
|
+
function renderSeedDesign(plan) {
|
|
981
|
+
return `## Context
|
|
982
|
+
|
|
983
|
+
This project was generated with Liftoff using ${plan.pattern.label}, Azure, OpenTofu, and ${plan.specWorkflow.label}.
|
|
984
|
+
|
|
985
|
+
## Goals / Non-Goals
|
|
986
|
+
|
|
987
|
+
**Goals:**
|
|
988
|
+
|
|
989
|
+
- Keep the generated baseline aligned to the approved Mission Control stack.
|
|
990
|
+
|
|
991
|
+
**Non-Goals:**
|
|
992
|
+
|
|
993
|
+
- Define domain-specific product behavior in the bootstrap change.
|
|
994
|
+
|
|
995
|
+
## Decisions
|
|
996
|
+
|
|
997
|
+
- Use FastAPI and PydanticAI for backend APIs and orchestration.
|
|
998
|
+
- Use OpenTofu for Azure infrastructure.
|
|
999
|
+
- Use Docker Compose for local development.
|
|
1000
|
+
|
|
1001
|
+
## Risks / Trade-offs
|
|
1002
|
+
|
|
1003
|
+
- The baseline contains placeholders that product-specific changes should replace.
|
|
1004
|
+
`;
|
|
1005
|
+
}
|
|
1006
|
+
function renderSeedTasks() {
|
|
1007
|
+
return `## 1. Bootstrap Review
|
|
1008
|
+
|
|
1009
|
+
- [ ] 1.1 Review generated baseline and replace placeholders with domain-specific requirements.
|
|
1010
|
+
- [ ] 1.2 Validate local Docker Compose startup.
|
|
1011
|
+
- [ ] 1.3 Validate OpenTofu plan for the first target environment.
|
|
1012
|
+
`;
|
|
1013
|
+
}
|
|
1014
|
+
function renderSpecKitConstitution(plan) {
|
|
1015
|
+
return `# Mission Control Liftoff Constitution
|
|
1016
|
+
|
|
1017
|
+
## Principle 1: Approved Application Stack
|
|
1018
|
+
Generated backend services MUST use FastAPI, PydanticAI, Pydantic configuration models, and Scalar for API documentation.
|
|
1019
|
+
|
|
1020
|
+
## Principle 2: Standard Project Layout
|
|
1021
|
+
Backend APIs live under backend/apis. Database artifacts live under database.${plan.includeFrontend ? ' Frontend code lives under frontend.' : ''}
|
|
1022
|
+
|
|
1023
|
+
## Principle 3: Infrastructure As Code
|
|
1024
|
+
Cloud infrastructure MUST be defined with OpenTofu. Azure is the supported V1 provider.
|
|
1025
|
+
|
|
1026
|
+
## Principle 4: Local Development Parity
|
|
1027
|
+
Projects MUST include Docker Compose for local development with PostgreSQL, Redis, local blob storage, and local messaging behavior.
|
|
1028
|
+
|
|
1029
|
+
## Principle 5: Observability And Operations
|
|
1030
|
+
LLM workflows MUST include Langfuse tracing hooks and environment-specific configuration for ${plan.environments.map((environment) => environment.id).join(', ')}.
|
|
1031
|
+
`;
|
|
1032
|
+
}
|
|
1033
|
+
function renderSpecKitSpecTemplate() {
|
|
1034
|
+
return `# Feature Specification
|
|
1035
|
+
|
|
1036
|
+
## User Scenarios
|
|
1037
|
+
|
|
1038
|
+
## Requirements
|
|
1039
|
+
|
|
1040
|
+
## Success Criteria
|
|
1041
|
+
`;
|
|
1042
|
+
}
|
|
1043
|
+
function renderSpecKitPlanTemplate() {
|
|
1044
|
+
return `# Implementation Plan
|
|
1045
|
+
|
|
1046
|
+
## Technical Context
|
|
1047
|
+
|
|
1048
|
+
## Constitution Check
|
|
1049
|
+
|
|
1050
|
+
## Tasks
|
|
1051
|
+
`;
|
|
1052
|
+
}
|
|
1053
|
+
function renderFrontendPackage(plan) {
|
|
1054
|
+
return JSON.stringify({
|
|
1055
|
+
name: `${plan.safeProjectName}-frontend`,
|
|
1056
|
+
version: '0.1.0',
|
|
1057
|
+
private: true,
|
|
1058
|
+
type: 'module',
|
|
1059
|
+
scripts: {
|
|
1060
|
+
dev: 'vite',
|
|
1061
|
+
build: 'vite build',
|
|
1062
|
+
preview: 'vite preview'
|
|
1063
|
+
},
|
|
1064
|
+
dependencies: {
|
|
1065
|
+
'@vitejs/plugin-vue': '^5.0.5',
|
|
1066
|
+
vite: '^5.3.1',
|
|
1067
|
+
vue: '^3.4.29',
|
|
1068
|
+
tailwindcss: '^3.4.4',
|
|
1069
|
+
autoprefixer: '^10.4.19',
|
|
1070
|
+
postcss: '^8.4.38'
|
|
1071
|
+
}
|
|
1072
|
+
}, null, 2);
|
|
1073
|
+
}
|
|
1074
|
+
function renderFrontendIndex(plan) {
|
|
1075
|
+
return `<div id="app"></div><script type="module" src="/src/main.ts"></script><title>${plan.projectName}</title>`;
|
|
1076
|
+
}
|
|
1077
|
+
function renderFrontendMain() {
|
|
1078
|
+
return `import { createApp } from 'vue';
|
|
1079
|
+
import App from './App.vue';
|
|
1080
|
+
import './styles.css';
|
|
1081
|
+
|
|
1082
|
+
createApp(App).mount('#app');
|
|
1083
|
+
`;
|
|
1084
|
+
}
|
|
1085
|
+
function renderFrontendApp(plan) {
|
|
1086
|
+
return `<script setup lang="ts">
|
|
1087
|
+
const title = '${plan.projectName}';
|
|
1088
|
+
const starter = '${plan.frontendStarter}';
|
|
1089
|
+
const pattern = '${plan.pattern.label}';
|
|
1090
|
+
</script>
|
|
1091
|
+
|
|
1092
|
+
<template>
|
|
1093
|
+
<main class="min-h-screen bg-slate-50 text-slate-950">
|
|
1094
|
+
<section class="mx-auto flex min-h-screen w-full max-w-5xl flex-col gap-6 px-6 py-10">
|
|
1095
|
+
<header>
|
|
1096
|
+
<p class="text-sm font-semibold uppercase tracking-wide text-emerald-700">Mission Control Liftoff</p>
|
|
1097
|
+
<h1 class="mt-2 text-3xl font-bold">{{ title }}</h1>
|
|
1098
|
+
<p class="mt-2 text-slate-600">{{ pattern }} starter</p>
|
|
1099
|
+
</header>
|
|
1100
|
+
<section class="rounded-lg border border-slate-200 bg-white p-6 shadow-sm">
|
|
1101
|
+
<h2 class="text-xl font-semibold">{{ starter }}</h2>
|
|
1102
|
+
<textarea class="mt-4 min-h-40 w-full rounded-md border border-slate-300 p-3" placeholder="Start with your application-specific input." />
|
|
1103
|
+
<button class="mt-4 rounded-md bg-emerald-700 px-4 py-2 font-semibold text-white">Run</button>
|
|
1104
|
+
</section>
|
|
1105
|
+
</section>
|
|
1106
|
+
</main>
|
|
1107
|
+
</template>
|
|
1108
|
+
`;
|
|
1109
|
+
}
|
|
1110
|
+
function renderFrontendStyles() {
|
|
1111
|
+
return `@tailwind base;
|
|
1112
|
+
@tailwind components;
|
|
1113
|
+
@tailwind utilities;
|
|
1114
|
+
`;
|
|
1115
|
+
}
|
|
1116
|
+
function renderFrontendViteConfig() {
|
|
1117
|
+
return `import { defineConfig } from 'vite';
|
|
1118
|
+
import vue from '@vitejs/plugin-vue';
|
|
1119
|
+
|
|
1120
|
+
export default defineConfig({ plugins: [vue()] });
|
|
1121
|
+
`;
|
|
1122
|
+
}
|
|
1123
|
+
function renderFrontendTailwindConfig() {
|
|
1124
|
+
return `import type { Config } from 'tailwindcss';
|
|
1125
|
+
|
|
1126
|
+
export default {
|
|
1127
|
+
content: ['./index.html', './src/**/*.{vue,ts}'],
|
|
1128
|
+
theme: { extend: {} },
|
|
1129
|
+
plugins: []
|
|
1130
|
+
} satisfies Config;
|
|
1131
|
+
`;
|
|
1132
|
+
}
|
|
1133
|
+
function renderFrontendDockerfile() {
|
|
1134
|
+
return `FROM node:20-alpine AS build
|
|
1135
|
+
WORKDIR /app
|
|
1136
|
+
COPY package.json package-lock.json* ./
|
|
1137
|
+
RUN npm install
|
|
1138
|
+
COPY . .
|
|
1139
|
+
RUN npm run build
|
|
1140
|
+
|
|
1141
|
+
FROM nginx:1.27-alpine
|
|
1142
|
+
COPY --from=build /app/dist /usr/share/nginx/html
|
|
1143
|
+
`;
|
|
1144
|
+
}
|
|
1145
|
+
function ensureTrailingNewline(content) {
|
|
1146
|
+
return content.endsWith('\n') ? content : `${content}\n`;
|
|
1147
|
+
}
|
|
1148
|
+
//# sourceMappingURL=templates.js.map
|