Skip to content

Commit e1723ea

Browse files
author
numen-bot
committed
feat: Pipeline Templates (#52)
- Template management CRUD - Template versioning & rollback - Dynamic stage configuration - Pipeline execution with templates - REST API endpoints - Template validation & testing
1 parent d95e76c commit e1723ea

57 files changed

Lines changed: 6243 additions & 4 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,58 @@ One-click content repurposing to 8 formats with AI-powered tone preservation and
7676
---
7777
## [Unreleased]
7878

79+
### Added
80+
81+
**AI Pipeline Templates & Preset Library** ([Issue #36](https://github.com/byte5digital/numen/issues/36))
82+
83+
Reusable AI pipeline templates for accelerated content creation workflows, featuring 8 built-in templates, community library, space-scoped templates, one-click install wizard, template versioning, and plugin registration hooks.
84+
85+
**Features:**
86+
- **8 built-in templates:** Blog Post Pipeline, Social Media Campaign, Product Description, Email Newsletter, Press Release, Landing Page, Technical Documentation, Video Script
87+
- **Template library API:** Discover, rate, and install templates with metadata
88+
- **Space-scoped templates:** Custom templates per content space with full RBAC support
89+
- **Install wizard:** Auto-configures personas, stages, and input variables from template schema
90+
- **Template versioning:** Track changes, publish/unpublish versions, rollback support
91+
- **Template packs:** Plugin system for registering template collections
92+
- **Community ratings:** Rate and provide feedback on templates
93+
- **Metadata support:** Categories, icons, author info, and schema versioning
94+
- **Security:** Space-scoped installs, RBAC permission gates, audit logging
95+
96+
**Endpoints:**
97+
- `GET /api/v1/spaces/{space}/pipeline-templates` — List templates (paginated)
98+
- `POST /api/v1/spaces/{space}/pipeline-templates` — Create custom template
99+
- `GET /api/v1/spaces/{space}/pipeline-templates/{template}` — Get template details
100+
- `PATCH /api/v1/spaces/{space}/pipeline-templates/{template}` — Update template
101+
- `DELETE /api/v1/spaces/{space}/pipeline-templates/{template}` — Delete template
102+
- `POST /api/v1/spaces/{space}/pipeline-templates/{template}/publish` — Publish template
103+
- `POST /api/v1/spaces/{space}/pipeline-templates/{template}/unpublish` — Unpublish template
104+
- `GET /api/v1/spaces/{space}/pipeline-templates/{template}/versions` — List versions
105+
- `POST /api/v1/spaces/{space}/pipeline-templates/{template}/versions` — Create version
106+
- `GET /api/v1/spaces/{space}/pipeline-templates/{template}/versions/{version}` — Get version
107+
- `POST /api/v1/spaces/{space}/pipeline-templates/installs/{version}` — Install template (rate-limited 5/min)
108+
- `PATCH /api/v1/spaces/{space}/pipeline-templates/installs/{install}` — Update install
109+
- `DELETE /api/v1/spaces/{space}/pipeline-templates/installs/{install}` — Remove install
110+
- `GET /api/v1/spaces/{space}/pipeline-templates/{template}/ratings` — List ratings
111+
- `POST /api/v1/spaces/{space}/pipeline-templates/{template}/ratings` — Rate template
112+
113+
**Plugin Hooks:**
114+
- `registerTemplateCategory(array $category)` — Register custom template categories
115+
- `registerTemplatePack(array $pack)` — Register template collections from plugins
116+
117+
**Models:**
118+
- `PipelineTemplate` — Template metadata (name, slug, category, icon, author)
119+
- `PipelineTemplateVersion` — Versioned template definitions with JSON schema
120+
- `PipelineTemplateInstall` — Track template usage per space
121+
- `PipelineTemplateRating` — Community feedback (1-5 stars)
122+
123+
**New environment variables:**
124+
- `TEMPLATE_LIBRARY_ENABLED=true`
125+
- `TEMPLATE_INSTALL_RATE_LIMIT=5` (per minute)
126+
127+
See [docs/pipeline-templates.md](docs/pipeline-templates.md) for complete documentation.
128+
129+
130+
79131
### Added
80132

81133
- Webhooks admin UI — manage webhook endpoints, event subscriptions, delivery logs, and secret rotation directly from the admin panel (Settings → Webhooks)

README.md

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,8 +96,28 @@ Manage webhook endpoints and event subscriptions directly from the admin panel (
9696
- See [docs/graphql-api.md](docs/graphql-api.md) for the full guide
9797

9898

99-
### Plugin & Extension System
100-
First-class plugin architecture. Extend pipelines, register custom LLM providers, add admin UI, and react to content events — all from a self-contained plugin package.
99+
### AI Pipeline Templates & Preset Library
100+
**New in v0.10.0.** Reusable AI pipeline templates for accelerated content creation workflows.
101+
102+
**Features:**
103+
- **8 built-in templates:** Blog Post Pipeline, Social Media Campaign, Product Description, Email Newsletter, Press Release, Landing Page, Technical Documentation, Video Script
104+
- **Template library API:** Browse, rate, and install templates from community library
105+
- **Space-scoped templates:** Custom templates per content space with RBAC
106+
- **One-click install wizard:** Auto-configures personas, stages, and variables from template schema
107+
- **Template versioning:** Version management with changelog and rollback support
108+
- **Template packs:** Plugin-registered template collections with metadata
109+
- **Plugin hooks:** `registerTemplateCategory()` and `registerTemplatePack()` for extending the library
110+
- **Template ratings:** Community feedback and quality metrics
111+
112+
**Endpoints:**
113+
- `GET /api/v1/spaces/{space}/pipeline-templates` — List templates
114+
- `POST /api/v1/spaces/{space}/pipeline-templates` — Create custom template
115+
- `POST /api/v1/spaces/{space}/pipeline-templates/{template}/publish` — Publish template
116+
- `POST /api/v1/spaces/{space}/pipeline-templates/{template}/versions` — Create new version
117+
- `POST /api/v1/spaces/{space}/pipeline-templates/installs/{version}` — Install template
118+
- `POST /api/v1/spaces/{space}/pipeline-templates/{template}/ratings` — Rate template
119+
120+
See [docs/pipeline-templates.md](docs/pipeline-templates.md) for the complete feature guide.
101121

102122
### Plugin & Extension System
103123
First-class plugin architecture. Extend pipelines, register custom LLM providers, add admin UI, and react to content events — all from a self-contained plugin package.
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
<?php
2+
3+
namespace App\Http\Controllers\Admin;
4+
5+
use App\Http\Controllers\Controller;
6+
use App\Models\Space;
7+
use Illuminate\Http\Request;
8+
use Inertia\Inertia;
9+
10+
class TemplateLibraryController extends Controller
11+
{
12+
public function index(Request $request)
13+
{
14+
/** @var \App\Models\User $user */
15+
$user = $request->user();
16+
$space = Space::where('owner_id', $user->id)->first();
17+
18+
return Inertia::render('Pipelines/Templates/Library', [
19+
'spaceId' => ($space !== null ? $space->id : ''),
20+
]);
21+
}
22+
23+
public function create(Request $request)
24+
{
25+
/** @var \App\Models\User $user */
26+
$user = $request->user();
27+
$space = Space::where('owner_id', $user->id)->first();
28+
29+
return Inertia::render('Pipelines/Templates/Editor', [
30+
'spaceId' => ($space !== null ? $space->id : ''),
31+
]);
32+
}
33+
34+
public function edit(Request $request, string $templateId)
35+
{
36+
/** @var \App\Models\User $user */
37+
$user = $request->user();
38+
$space = Space::where('owner_id', $user->id)->first();
39+
40+
return Inertia::render('Pipelines/Templates/Editor', [
41+
'spaceId' => ($space !== null ? $space->id : ''),
42+
'templateId' => $templateId,
43+
]);
44+
}
45+
46+
public function marketplace(Request $request)
47+
{
48+
/** @var \App\Models\User $user */
49+
$user = $request->user();
50+
$space = Space::where('owner_id', $user->id)->first();
51+
52+
return Inertia::render('Pipelines/Templates/Marketplace', [
53+
'spaceId' => ($space !== null ? $space->id : ''),
54+
]);
55+
}
56+
57+
public function install(Request $request)
58+
{
59+
/** @var \App\Models\User $user */
60+
$user = $request->user();
61+
$space = Space::where('owner_id', $user->id)->first();
62+
63+
return Inertia::render('Pipelines/Templates/InstallWizard', [
64+
'spaceId' => ($space !== null ? $space->id : ''),
65+
'templateId' => $request->query('template'),
66+
]);
67+
}
68+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
<?php
2+
3+
namespace App\Http\Controllers\Api\Templates;
4+
5+
use App\Http\Controllers\Controller;
6+
use App\Http\Requests\Templates\StorePipelineTemplateRequest;
7+
use App\Http\Requests\Templates\UpdatePipelineTemplateRequest;
8+
use App\Http\Resources\PipelineTemplateResource;
9+
use App\Models\PipelineTemplate;
10+
use App\Models\Space;
11+
use App\Services\PipelineTemplates\PipelineTemplateService;
12+
use Illuminate\Http\JsonResponse;
13+
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
14+
15+
class PipelineTemplateController extends Controller
16+
{
17+
public function __construct(
18+
private readonly PipelineTemplateService $service,
19+
) {}
20+
21+
public function index(Space $space): AnonymousResourceCollection
22+
{
23+
$spaceTemplates = PipelineTemplate::with('latestVersion')
24+
->where('space_id', $space->id)
25+
->latest()
26+
->get();
27+
28+
$marketplace = PipelineTemplate::with('latestVersion')
29+
->whereNull('space_id')
30+
->where('is_published', true)
31+
->latest()
32+
->get();
33+
34+
return PipelineTemplateResource::collection($spaceTemplates->merge($marketplace));
35+
}
36+
37+
public function show(Space $space, PipelineTemplate $template): PipelineTemplateResource
38+
{
39+
abort_if($template->space_id !== null && $template->space_id !== $space->id, 403);
40+
41+
$template->load('latestVersion', 'versions');
42+
43+
return new PipelineTemplateResource($template);
44+
}
45+
46+
public function store(Space $space, StorePipelineTemplateRequest $request): JsonResponse
47+
{
48+
$data = $request->validated();
49+
$template = $this->service->create($space, $data);
50+
51+
if (! empty($data['definition'])) {
52+
$this->service->createVersion($template, $data['definition'], $data['version'] ?? '1.0.0', $data['changelog'] ?? null);
53+
}
54+
55+
$template->load('latestVersion');
56+
57+
return (new PipelineTemplateResource($template))->response()->setStatusCode(201);
58+
}
59+
60+
public function update(Space $space, PipelineTemplate $template, UpdatePipelineTemplateRequest $request): PipelineTemplateResource
61+
{
62+
abort_if($template->space_id !== null && $template->space_id !== $space->id, 403);
63+
64+
$template = $this->service->update($template, $request->validated());
65+
$template->load('latestVersion');
66+
67+
return new PipelineTemplateResource($template);
68+
}
69+
70+
public function destroy(Space $space, PipelineTemplate $template): JsonResponse
71+
{
72+
abort_if($template->space_id !== null && $template->space_id !== $space->id, 403);
73+
74+
$this->service->delete($template);
75+
76+
return response()->json(null, 204);
77+
}
78+
79+
public function publish(Space $space, PipelineTemplate $template): PipelineTemplateResource
80+
{
81+
abort_if($template->space_id !== null && $template->space_id !== $space->id, 403);
82+
83+
$this->service->publish($template);
84+
85+
return new PipelineTemplateResource($template->refresh());
86+
}
87+
88+
public function unpublish(Space $space, PipelineTemplate $template): PipelineTemplateResource
89+
{
90+
abort_if($template->space_id !== null && $template->space_id !== $space->id, 403);
91+
92+
$this->service->unpublish($template);
93+
94+
return new PipelineTemplateResource($template->refresh());
95+
}
96+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<?php
2+
3+
namespace App\Http\Controllers\Api\Templates;
4+
5+
use App\Http\Controllers\Controller;
6+
use App\Http\Requests\Templates\InstallTemplateRequest;
7+
use App\Http\Resources\PipelineTemplateInstallResource;
8+
use App\Models\PipelineTemplateInstall;
9+
use App\Models\PipelineTemplateVersion;
10+
use App\Models\Space;
11+
use App\Services\PipelineTemplates\PipelineTemplateInstallService;
12+
use Illuminate\Http\JsonResponse;
13+
14+
class PipelineTemplateInstallController extends Controller
15+
{
16+
public function __construct(
17+
private readonly PipelineTemplateInstallService $installService,
18+
) {}
19+
20+
public function store(PipelineTemplateVersion $version, Space $space, InstallTemplateRequest $request): JsonResponse
21+
{
22+
$data = $request->validated();
23+
$install = $this->installService->install($version, $space, $data['variable_values'] ?? [], $data['config_overrides'] ?? []);
24+
$install->load('template', 'templateVersion');
25+
26+
return (new PipelineTemplateInstallResource($install))->response()->setStatusCode(201);
27+
}
28+
29+
public function destroy(PipelineTemplateInstall $install, Space $space): JsonResponse
30+
{
31+
abort_if($install->space_id !== $space->id, 403);
32+
33+
$this->installService->uninstall($install);
34+
35+
return response()->json(null, 204);
36+
}
37+
38+
public function update(PipelineTemplateInstall $install, Space $space, InstallTemplateRequest $request): PipelineTemplateInstallResource
39+
{
40+
abort_if($install->space_id !== $space->id, 403);
41+
42+
$install->loadMissing('templateVersion.template');
43+
/** @var PipelineTemplateVersion $currentVersion */
44+
$currentVersion = $install->getRelation('templateVersion');
45+
/** @var \App\Models\PipelineTemplate $tmpl */
46+
$tmpl = $currentVersion->getRelation('template');
47+
/** @var PipelineTemplateVersion $newVersion */
48+
$newVersion = $tmpl->versions()->where('is_latest', true)->firstOrFail();
49+
$updatedInstall = $this->installService->update($install, $newVersion);
50+
$updatedInstall->load('template', 'templateVersion');
51+
52+
return new PipelineTemplateInstallResource($updatedInstall);
53+
}
54+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php
2+
3+
namespace App\Http\Controllers\Api\Templates;
4+
5+
use App\Http\Controllers\Controller;
6+
use App\Http\Requests\Templates\RateTemplateRequest;
7+
use App\Models\PipelineTemplate;
8+
use App\Models\PipelineTemplateRating;
9+
use App\Models\Space;
10+
use Illuminate\Http\JsonResponse;
11+
12+
class PipelineTemplateRatingController extends Controller
13+
{
14+
public function index(Space $space, PipelineTemplate $template): JsonResponse
15+
{
16+
$ratings = $template->ratings()->with('user')->latest()->get();
17+
$average = $ratings->avg('rating');
18+
19+
return response()->json([
20+
'data' => $ratings->map(fn (PipelineTemplateRating $r) => [
21+
'id' => $r->id,
22+
'rating' => $r->rating,
23+
'review' => $r->review,
24+
'user' => $r->user ? ['id' => $r->user->id, 'name' => $r->user->name] : null,
25+
'created_at' => $r->created_at->toIso8601String(),
26+
]),
27+
'meta' => [
28+
'average_rating' => $average ? round((float) $average, 2) : null,
29+
'total' => $ratings->count(),
30+
],
31+
]);
32+
}
33+
34+
public function store(Space $space, PipelineTemplate $template, RateTemplateRequest $request): JsonResponse
35+
{
36+
$rating = PipelineTemplateRating::updateOrCreate(
37+
['template_id' => $template->id, 'user_id' => $request->user()?->id],
38+
['rating' => $request->integer('rating'), 'review' => $request->string('review')->value() ?: null],
39+
);
40+
41+
return response()->json([
42+
'data' => ['id' => $rating->id, 'rating' => $rating->rating, 'review' => $rating->review, 'created_at' => $rating->created_at->toIso8601String()],
43+
], 201);
44+
}
45+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php
2+
3+
namespace App\Http\Controllers\Api\Templates;
4+
5+
use App\Http\Controllers\Controller;
6+
use App\Http\Requests\Templates\CreateVersionRequest;
7+
use App\Http\Resources\PipelineTemplateVersionResource;
8+
use App\Models\PipelineTemplate;
9+
use App\Models\PipelineTemplateVersion;
10+
use App\Models\Space;
11+
use App\Services\PipelineTemplates\PipelineTemplateService;
12+
use Illuminate\Http\JsonResponse;
13+
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
14+
15+
class PipelineTemplateVersionController extends Controller
16+
{
17+
public function __construct(
18+
private readonly PipelineTemplateService $service,
19+
) {}
20+
21+
public function index(Space $space, PipelineTemplate $template): AnonymousResourceCollection
22+
{
23+
return PipelineTemplateVersionResource::collection($template->versions()->latest()->get());
24+
}
25+
26+
public function store(Space $space, PipelineTemplate $template, CreateVersionRequest $request): JsonResponse
27+
{
28+
$data = $request->validated();
29+
$version = $this->service->createVersion($template, $data['definition'], $data['version'], $data['changelog'] ?? null);
30+
31+
return (new PipelineTemplateVersionResource($version))->response()->setStatusCode(201);
32+
}
33+
34+
public function show(Space $space, PipelineTemplate $template, PipelineTemplateVersion $version): PipelineTemplateVersionResource
35+
{
36+
return new PipelineTemplateVersionResource($version);
37+
}
38+
}

0 commit comments

Comments
 (0)