Skip to content
Open
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
52 changes: 52 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,58 @@ One-click content repurposing to 8 formats with AI-powered tone preservation and
---
## [Unreleased]

### Added

**AI Pipeline Templates & Preset Library** ([Issue #36](https://github.com/byte5digital/numen/issues/36))

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.

**Features:**
- **8 built-in templates:** Blog Post Pipeline, Social Media Campaign, Product Description, Email Newsletter, Press Release, Landing Page, Technical Documentation, Video Script
- **Template library API:** Discover, rate, and install templates with metadata
- **Space-scoped templates:** Custom templates per content space with full RBAC support
- **Install wizard:** Auto-configures personas, stages, and input variables from template schema
- **Template versioning:** Track changes, publish/unpublish versions, rollback support
- **Template packs:** Plugin system for registering template collections
- **Community ratings:** Rate and provide feedback on templates
- **Metadata support:** Categories, icons, author info, and schema versioning
- **Security:** Space-scoped installs, RBAC permission gates, audit logging

**Endpoints:**
- `GET /api/v1/spaces/{space}/pipeline-templates` — List templates (paginated)
- `POST /api/v1/spaces/{space}/pipeline-templates` — Create custom template
- `GET /api/v1/spaces/{space}/pipeline-templates/{template}` — Get template details
- `PATCH /api/v1/spaces/{space}/pipeline-templates/{template}` — Update template
- `DELETE /api/v1/spaces/{space}/pipeline-templates/{template}` — Delete template
- `POST /api/v1/spaces/{space}/pipeline-templates/{template}/publish` — Publish template
- `POST /api/v1/spaces/{space}/pipeline-templates/{template}/unpublish` — Unpublish template
- `GET /api/v1/spaces/{space}/pipeline-templates/{template}/versions` — List versions
- `POST /api/v1/spaces/{space}/pipeline-templates/{template}/versions` — Create version
- `GET /api/v1/spaces/{space}/pipeline-templates/{template}/versions/{version}` — Get version
- `POST /api/v1/spaces/{space}/pipeline-templates/installs/{version}` — Install template (rate-limited 5/min)
- `PATCH /api/v1/spaces/{space}/pipeline-templates/installs/{install}` — Update install
- `DELETE /api/v1/spaces/{space}/pipeline-templates/installs/{install}` — Remove install
- `GET /api/v1/spaces/{space}/pipeline-templates/{template}/ratings` — List ratings
- `POST /api/v1/spaces/{space}/pipeline-templates/{template}/ratings` — Rate template

**Plugin Hooks:**
- `registerTemplateCategory(array $category)` — Register custom template categories
- `registerTemplatePack(array $pack)` — Register template collections from plugins

**Models:**
- `PipelineTemplate` — Template metadata (name, slug, category, icon, author)
- `PipelineTemplateVersion` — Versioned template definitions with JSON schema
- `PipelineTemplateInstall` — Track template usage per space
- `PipelineTemplateRating` — Community feedback (1-5 stars)

**New environment variables:**
- `TEMPLATE_LIBRARY_ENABLED=true`
- `TEMPLATE_INSTALL_RATE_LIMIT=5` (per minute)

See [docs/pipeline-templates.md](docs/pipeline-templates.md) for complete documentation.



### Added

- Webhooks admin UI — manage webhook endpoints, event subscriptions, delivery logs, and secret rotation directly from the admin panel (Settings → Webhooks)
Expand Down
24 changes: 22 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,28 @@ Manage webhook endpoints and event subscriptions directly from the admin panel (
- See [docs/graphql-api.md](docs/graphql-api.md) for the full guide


### Plugin & Extension System
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.
### AI Pipeline Templates & Preset Library
**New in v0.10.0.** Reusable AI pipeline templates for accelerated content creation workflows.

**Features:**
- **8 built-in templates:** Blog Post Pipeline, Social Media Campaign, Product Description, Email Newsletter, Press Release, Landing Page, Technical Documentation, Video Script
- **Template library API:** Browse, rate, and install templates from community library
- **Space-scoped templates:** Custom templates per content space with RBAC
- **One-click install wizard:** Auto-configures personas, stages, and variables from template schema
- **Template versioning:** Version management with changelog and rollback support
- **Template packs:** Plugin-registered template collections with metadata
- **Plugin hooks:** `registerTemplateCategory()` and `registerTemplatePack()` for extending the library
- **Template ratings:** Community feedback and quality metrics

**Endpoints:**
- `GET /api/v1/spaces/{space}/pipeline-templates` — List templates
- `POST /api/v1/spaces/{space}/pipeline-templates` — Create custom template
- `POST /api/v1/spaces/{space}/pipeline-templates/{template}/publish` — Publish template
- `POST /api/v1/spaces/{space}/pipeline-templates/{template}/versions` — Create new version
- `POST /api/v1/spaces/{space}/pipeline-templates/installs/{version}` — Install template
- `POST /api/v1/spaces/{space}/pipeline-templates/{template}/ratings` — Rate template

See [docs/pipeline-templates.md](docs/pipeline-templates.md) for the complete feature guide.

### Plugin & Extension System
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.
Expand Down
68 changes: 68 additions & 0 deletions app/Http/Controllers/Admin/TemplateLibraryController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<?php

namespace App\Http\Controllers\Admin;

use App\Http\Controllers\Controller;
use App\Models\Space;
use Illuminate\Http\Request;
use Inertia\Inertia;

class TemplateLibraryController extends Controller
{
public function index(Request $request)
{
/** @var \App\Models\User $user */
$user = $request->user();
$space = Space::where('owner_id', $user->id)->first();

return Inertia::render('Pipelines/Templates/Library', [
'spaceId' => ($space !== null ? $space->id : ''),
]);
}

public function create(Request $request)
{
/** @var \App\Models\User $user */
$user = $request->user();
$space = Space::where('owner_id', $user->id)->first();

return Inertia::render('Pipelines/Templates/Editor', [
'spaceId' => ($space !== null ? $space->id : ''),
]);
}

public function edit(Request $request, string $templateId)
{
/** @var \App\Models\User $user */
$user = $request->user();
$space = Space::where('owner_id', $user->id)->first();

return Inertia::render('Pipelines/Templates/Editor', [
'spaceId' => ($space !== null ? $space->id : ''),
'templateId' => $templateId,
]);
}

public function marketplace(Request $request)
{
/** @var \App\Models\User $user */
$user = $request->user();
$space = Space::where('owner_id', $user->id)->first();

return Inertia::render('Pipelines/Templates/Marketplace', [
'spaceId' => ($space !== null ? $space->id : ''),
]);
}

public function install(Request $request)
{
/** @var \App\Models\User $user */
$user = $request->user();
$space = Space::where('owner_id', $user->id)->first();

return Inertia::render('Pipelines/Templates/InstallWizard', [
'spaceId' => ($space !== null ? $space->id : ''),
'templateId' => $request->query('template'),
]);
}
}
96 changes: 96 additions & 0 deletions app/Http/Controllers/Api/Templates/PipelineTemplateController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
<?php

namespace App\Http\Controllers\Api\Templates;

use App\Http\Controllers\Controller;
use App\Http\Requests\Templates\StorePipelineTemplateRequest;
use App\Http\Requests\Templates\UpdatePipelineTemplateRequest;
use App\Http\Resources\PipelineTemplateResource;
use App\Models\PipelineTemplate;
use App\Models\Space;
use App\Services\PipelineTemplates\PipelineTemplateService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;

class PipelineTemplateController extends Controller
{
public function __construct(
private readonly PipelineTemplateService $service,
) {}

public function index(Space $space): AnonymousResourceCollection
{
$spaceTemplates = PipelineTemplate::with('latestVersion')
->where('space_id', $space->id)
->latest()
->get();

$marketplace = PipelineTemplate::with('latestVersion')
->whereNull('space_id')
->where('is_published', true)
->latest()
->get();

return PipelineTemplateResource::collection($spaceTemplates->merge($marketplace));
}

public function show(Space $space, PipelineTemplate $template): PipelineTemplateResource
{
abort_if($template->space_id !== null && $template->space_id !== $space->id, 403);

$template->load('latestVersion', 'versions');

return new PipelineTemplateResource($template);
}

public function store(Space $space, StorePipelineTemplateRequest $request): JsonResponse
{
$data = $request->validated();
$template = $this->service->create($space, $data);

if (! empty($data['definition'])) {
$this->service->createVersion($template, $data['definition'], $data['version'] ?? '1.0.0', $data['changelog'] ?? null);
}

$template->load('latestVersion');

return (new PipelineTemplateResource($template))->response()->setStatusCode(201);
}

public function update(Space $space, PipelineTemplate $template, UpdatePipelineTemplateRequest $request): PipelineTemplateResource
{
abort_if($template->space_id !== null && $template->space_id !== $space->id, 403);

$template = $this->service->update($template, $request->validated());
$template->load('latestVersion');

return new PipelineTemplateResource($template);
}

public function destroy(Space $space, PipelineTemplate $template): JsonResponse
{
abort_if($template->space_id !== null && $template->space_id !== $space->id, 403);

$this->service->delete($template);

return response()->json(null, 204);
}

public function publish(Space $space, PipelineTemplate $template): PipelineTemplateResource
{
abort_if($template->space_id !== null && $template->space_id !== $space->id, 403);

$this->service->publish($template);

return new PipelineTemplateResource($template->refresh());
}

public function unpublish(Space $space, PipelineTemplate $template): PipelineTemplateResource
{
abort_if($template->space_id !== null && $template->space_id !== $space->id, 403);

$this->service->unpublish($template);

return new PipelineTemplateResource($template->refresh());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php

namespace App\Http\Controllers\Api\Templates;

use App\Http\Controllers\Controller;
use App\Http\Requests\Templates\InstallTemplateRequest;
use App\Http\Resources\PipelineTemplateInstallResource;
use App\Models\PipelineTemplateInstall;
use App\Models\PipelineTemplateVersion;
use App\Models\Space;
use App\Services\PipelineTemplates\PipelineTemplateInstallService;
use Illuminate\Http\JsonResponse;

class PipelineTemplateInstallController extends Controller
{
public function __construct(
private readonly PipelineTemplateInstallService $installService,
) {}

public function store(PipelineTemplateVersion $version, Space $space, InstallTemplateRequest $request): JsonResponse
{
$data = $request->validated();
$install = $this->installService->install($version, $space, $data['variable_values'] ?? [], $data['config_overrides'] ?? []);
$install->load('template', 'templateVersion');

return (new PipelineTemplateInstallResource($install))->response()->setStatusCode(201);
}

public function destroy(PipelineTemplateInstall $install, Space $space): JsonResponse
{
abort_if($install->space_id !== $space->id, 403);

$this->installService->uninstall($install);

return response()->json(null, 204);
}

public function update(PipelineTemplateInstall $install, Space $space, InstallTemplateRequest $request): PipelineTemplateInstallResource
{
abort_if($install->space_id !== $space->id, 403);

$install->loadMissing('templateVersion.template');
/** @var PipelineTemplateVersion $currentVersion */
$currentVersion = $install->getRelation('templateVersion');
/** @var \App\Models\PipelineTemplate $tmpl */
$tmpl = $currentVersion->getRelation('template');
/** @var PipelineTemplateVersion $newVersion */
$newVersion = $tmpl->versions()->where('is_latest', true)->firstOrFail();
$updatedInstall = $this->installService->update($install, $newVersion);
$updatedInstall->load('template', 'templateVersion');

return new PipelineTemplateInstallResource($updatedInstall);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

namespace App\Http\Controllers\Api\Templates;

use App\Http\Controllers\Controller;
use App\Http\Requests\Templates\RateTemplateRequest;
use App\Models\PipelineTemplate;
use App\Models\PipelineTemplateRating;
use App\Models\Space;
use Illuminate\Http\JsonResponse;

class PipelineTemplateRatingController extends Controller
{
public function index(Space $space, PipelineTemplate $template): JsonResponse
{
$ratings = $template->ratings()->with('user')->latest()->get();
$average = $ratings->avg('rating');

return response()->json([
'data' => $ratings->map(fn (PipelineTemplateRating $r) => [
'id' => $r->id,
'rating' => $r->rating,
'review' => $r->review,
'user' => $r->user ? ['id' => $r->user->id, 'name' => $r->user->name] : null,
'created_at' => $r->created_at->toIso8601String(),
]),
'meta' => [
'average_rating' => $average ? round((float) $average, 2) : null,
'total' => $ratings->count(),
],
]);
}

public function store(Space $space, PipelineTemplate $template, RateTemplateRequest $request): JsonResponse
{
$rating = PipelineTemplateRating::updateOrCreate(
['template_id' => $template->id, 'user_id' => $request->user()?->id],
['rating' => $request->integer('rating'), 'review' => $request->string('review')->value() ?: null],
);

return response()->json([
'data' => ['id' => $rating->id, 'rating' => $rating->rating, 'review' => $rating->review, 'created_at' => $rating->created_at->toIso8601String()],
], 201);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

namespace App\Http\Controllers\Api\Templates;

use App\Http\Controllers\Controller;
use App\Http\Requests\Templates\CreateVersionRequest;
use App\Http\Resources\PipelineTemplateVersionResource;
use App\Models\PipelineTemplate;
use App\Models\PipelineTemplateVersion;
use App\Models\Space;
use App\Services\PipelineTemplates\PipelineTemplateService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;

class PipelineTemplateVersionController extends Controller
{
public function __construct(
private readonly PipelineTemplateService $service,
) {}

public function index(Space $space, PipelineTemplate $template): AnonymousResourceCollection
{
return PipelineTemplateVersionResource::collection($template->versions()->latest()->get());
}

public function store(Space $space, PipelineTemplate $template, CreateVersionRequest $request): JsonResponse
{
$data = $request->validated();
$version = $this->service->createVersion($template, $data['definition'], $data['version'], $data['changelog'] ?? null);

return (new PipelineTemplateVersionResource($version))->response()->setStatusCode(201);
}

public function show(Space $space, PipelineTemplate $template, PipelineTemplateVersion $version): PipelineTemplateVersionResource
{
return new PipelineTemplateVersionResource($version);
}
}
Loading
Loading