Skip to content

Commit ecde7bb

Browse files
authored
Merge pull request #3 from DIYA73/test-codewatch-review
feat: agent template marketplace — GET /templates, POST /templates/:i…
2 parents 4d36ec7 + fdd4232 commit ecde7bb

6 files changed

Lines changed: 243 additions & 0 deletions

File tree

apps/api/src/app.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { FlowsModule } from './flows/flows.module';
1313
import { TriggersModule } from './triggers/triggers.module';
1414
import { ExecutionsModule } from './executions/executions.module';
1515
import { GatewayModule } from './gateway/gateway.module';
16+
import { TemplatesModule } from './templates/templates.module';
1617

1718
@Module({
1819
imports: [
@@ -40,6 +41,7 @@ import { GatewayModule } from './gateway/gateway.module';
4041
TriggersModule,
4142
ExecutionsModule,
4243
GatewayModule,
44+
TemplatesModule,
4345
],
4446
providers: [
4547
{ provide: APP_GUARD, useClass: JwtAuthGuard },
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn } from 'typeorm';
2+
import type { FlowGraph } from '../flows/entities/flow.entity';
3+
4+
export const TemplateCategory = {
5+
AI: 'ai',
6+
DATA: 'data',
7+
AUTOMATION: 'automation',
8+
MONITORING: 'monitoring',
9+
COMMUNICATION: 'communication',
10+
} as const;
11+
export type TemplateCategory = typeof TemplateCategory[keyof typeof TemplateCategory];
12+
13+
@Entity('templates')
14+
export class Template {
15+
@PrimaryGeneratedColumn('uuid')
16+
id!: string;
17+
18+
@Column()
19+
name!: string;
20+
21+
@Column({ type: 'text' })
22+
description!: string;
23+
24+
@Column({ type: 'varchar' })
25+
category!: TemplateCategory;
26+
27+
@Column({ nullable: true, type: 'varchar' })
28+
icon!: string | null;
29+
30+
@Column({ type: 'jsonb' })
31+
graph!: FlowGraph;
32+
33+
@Column({ default: 0 })
34+
useCount!: number;
35+
36+
@Column({ default: true })
37+
isBuiltin!: boolean;
38+
39+
@CreateDateColumn()
40+
createdAt!: Date;
41+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { Controller, Get, Post, Param, Query, UseGuards } from '@nestjs/common';
2+
import { ApiTags, ApiBearerAuth } from '@nestjs/swagger';
3+
import { TemplatesService } from './templates.service';
4+
import { JwtAuthGuard } from '../common/guards/jwt-auth.guard';
5+
import { CurrentWorkspaceId } from '../common/decorators/current-user.decorator';
6+
import type { TemplateCategory } from './template.entity';
7+
8+
@ApiTags('Templates')
9+
@Controller('templates')
10+
export class TemplatesController {
11+
constructor(private readonly service: TemplatesService) {}
12+
13+
@Get()
14+
findAll(@Query('category') category?: TemplateCategory) {
15+
return this.service.findAll(category);
16+
}
17+
18+
@Get(':id')
19+
findOne(@Param('id') id: string) {
20+
return this.service.findOne(id);
21+
}
22+
23+
@Post(':id/use')
24+
@ApiBearerAuth()
25+
@UseGuards(JwtAuthGuard)
26+
useTemplate(
27+
@Param('id') id: string,
28+
@CurrentWorkspaceId() workspaceId: string,
29+
) {
30+
return this.service.useTemplate(id, workspaceId);
31+
}
32+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { Module } from '@nestjs/common';
2+
import { TypeOrmModule } from '@nestjs/typeorm';
3+
import { Template } from './template.entity';
4+
import { TemplatesService } from './templates.service';
5+
import { TemplatesController } from './templates.controller';
6+
import { FlowsModule } from '../flows/flows.module';
7+
8+
@Module({
9+
imports: [TypeOrmModule.forFeature([Template]), FlowsModule],
10+
providers: [TemplatesService],
11+
controllers: [TemplatesController],
12+
})
13+
export class TemplatesModule {}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import { TemplateCategory } from './template.entity';
2+
import type { FlowGraph } from '../flows/entities/flow.entity';
3+
4+
interface SeedTemplate {
5+
name: string;
6+
description: string;
7+
category: TemplateCategory;
8+
icon: string;
9+
graph: FlowGraph;
10+
isBuiltin: boolean;
11+
}
12+
13+
export const BUILTIN_TEMPLATES: SeedTemplate[] = [
14+
{
15+
name: 'Web Scraper → Summarize → Email',
16+
description: 'Scrape a webpage, summarize the content with an LLM, and send the summary by email.',
17+
category: TemplateCategory.AI,
18+
icon: '🌐',
19+
isBuiltin: true,
20+
graph: {
21+
nodes: [
22+
{ id: 'n1', type: 'web-scraper', position: { x: 100, y: 200 }, data: { label: 'Web Scraper', config: { url: '', selector: 'body' } } },
23+
{ id: 'n2', type: 'ai-llm', position: { x: 400, y: 200 }, data: { label: 'Summarize', config: { prompt: 'Summarize this content in 3 bullet points: {{input}}', model: 'gpt-4o-mini' } } },
24+
{ id: 'n3', type: 'email-sender', position: { x: 700, y: 200 }, data: { label: 'Send Email', config: { to: '', subject: 'Daily Summary' } } },
25+
],
26+
edges: [
27+
{ id: 'e1', source: 'n1', target: 'n2' },
28+
{ id: 'e2', source: 'n2', target: 'n3' },
29+
],
30+
},
31+
},
32+
{
33+
name: 'GitHub PR Review',
34+
description: 'Fetch open pull requests from a GitHub repo and generate an AI code review for each.',
35+
category: TemplateCategory.AI,
36+
icon: '🔍',
37+
isBuiltin: true,
38+
graph: {
39+
nodes: [
40+
{ id: 'n1', type: 'api-caller', position: { x: 100, y: 200 }, data: { label: 'Fetch PRs', config: { url: 'https://api.github.com/repos/{{owner}}/{{repo}}/pulls', method: 'GET', headers: { Authorization: 'Bearer {{github_token}}' } } } },
41+
{ id: 'n2', type: 'ai-llm', position: { x: 400, y: 200 }, data: { label: 'Review Code', config: { prompt: 'Review this pull request and identify potential issues: {{input}}', model: 'gpt-4o' } } },
42+
{ id: 'n3', type: 'webhook-sender', position: { x: 700, y: 200 }, data: { label: 'Post Comment', config: { url: '', method: 'POST' } } },
43+
],
44+
edges: [
45+
{ id: 'e1', source: 'n1', target: 'n2' },
46+
{ id: 'e2', source: 'n2', target: 'n3' },
47+
],
48+
},
49+
},
50+
{
51+
name: 'Daily News Digest',
52+
description: 'Fetch news from an RSS feed, filter relevant items, and compile a daily digest.',
53+
category: TemplateCategory.AUTOMATION,
54+
icon: '📰',
55+
isBuiltin: true,
56+
graph: {
57+
nodes: [
58+
{ id: 'n1', type: 'rss-reader', position: { x: 100, y: 200 }, data: { label: 'RSS Feed', config: { url: '', maxItems: 20 } } },
59+
{ id: 'n2', type: 'ai-llm', position: { x: 400, y: 200 }, data: { label: 'Filter & Rank', config: { prompt: 'From these news items, select the 5 most important and explain why: {{input}}', model: 'gpt-4o-mini' } } },
60+
{ id: 'n3', type: 'email-sender', position: { x: 700, y: 200 }, data: { label: 'Send Digest', config: { to: '', subject: 'Daily News Digest' } } },
61+
],
62+
edges: [
63+
{ id: 'e1', source: 'n1', target: 'n2' },
64+
{ id: 'e2', source: 'n2', target: 'n3' },
65+
],
66+
},
67+
},
68+
{
69+
name: 'API Health Monitor',
70+
description: 'Ping a list of API endpoints, check response times, and alert on failures.',
71+
category: TemplateCategory.MONITORING,
72+
icon: '🔔',
73+
isBuiltin: true,
74+
graph: {
75+
nodes: [
76+
{ id: 'n1', type: 'api-caller', position: { x: 100, y: 200 }, data: { label: 'Health Check', config: { url: '', method: 'GET', timeout: 5000 } } },
77+
{ id: 'n2', type: 'condition', position: { x: 400, y: 200 }, data: { label: 'Check Status', config: { condition: '{{status}} !== 200 || {{responseTime}} > 2000' } } },
78+
{ id: 'n3', type: 'webhook-sender', position: { x: 700, y: 100 }, data: { label: 'Alert Slack', config: { url: '', method: 'POST' } } },
79+
{ id: 'n4', type: 'logger', position: { x: 700, y: 300 }, data: { label: 'Log OK', config: { level: 'info' } } },
80+
],
81+
edges: [
82+
{ id: 'e1', source: 'n1', target: 'n2' },
83+
{ id: 'e2', source: 'n2', target: 'n3', label: 'fail' },
84+
{ id: 'e3', source: 'n2', target: 'n4', label: 'pass' },
85+
],
86+
},
87+
},
88+
{
89+
name: 'Data Extractor & CSV Export',
90+
description: 'Extract structured data from text or HTML using an LLM and format it as CSV.',
91+
category: TemplateCategory.DATA,
92+
icon: '📊',
93+
isBuiltin: true,
94+
graph: {
95+
nodes: [
96+
{ id: 'n1', type: 'web-scraper', position: { x: 100, y: 200 }, data: { label: 'Scrape Data', config: { url: '', selector: 'table' } } },
97+
{ id: 'n2', type: 'ai-llm', position: { x: 400, y: 200 }, data: { label: 'Extract Structure', config: { prompt: 'Extract all data from this content and return as JSON array with consistent keys: {{input}}', model: 'gpt-4o-mini' } } },
98+
{ id: 'n3', type: 'transformer', position: { x: 700, y: 200 }, data: { label: 'To CSV', config: { format: 'csv' } } },
99+
],
100+
edges: [
101+
{ id: 'e1', source: 'n1', target: 'n2' },
102+
{ id: 'e2', source: 'n2', target: 'n3' },
103+
],
104+
},
105+
},
106+
];
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { Injectable, NotFoundException, OnModuleInit } from '@nestjs/common';
2+
import { InjectRepository } from '@nestjs/typeorm';
3+
import { Repository } from 'typeorm';
4+
import { Template, TemplateCategory } from './template.entity';
5+
import { FlowsService } from '../flows/flows.service';
6+
import type { Flow } from '../flows/entities/flow.entity';
7+
import { BUILTIN_TEMPLATES } from './templates.seed';
8+
9+
@Injectable()
10+
export class TemplatesService implements OnModuleInit {
11+
constructor(
12+
@InjectRepository(Template)
13+
private readonly repo: Repository<Template>,
14+
private readonly flowsService: FlowsService,
15+
) {}
16+
17+
async onModuleInit(): Promise<void> {
18+
const count = await this.repo.count({ where: { isBuiltin: true } });
19+
if (count === 0) {
20+
await this.repo.save(
21+
BUILTIN_TEMPLATES.map((t) => this.repo.create(t)),
22+
);
23+
}
24+
}
25+
26+
async findAll(category?: TemplateCategory): Promise<Template[]> {
27+
const where = category ? { category } : {};
28+
return this.repo.find({ where, order: { useCount: 'DESC', createdAt: 'DESC' } });
29+
}
30+
31+
async findOne(id: string): Promise<Template> {
32+
const template = await this.repo.findOne({ where: { id } });
33+
if (!template) throw new NotFoundException(`Template ${id} not found`);
34+
return template;
35+
}
36+
37+
async useTemplate(id: string, workspaceId: string): Promise<Flow> {
38+
const template = await this.findOne(id);
39+
40+
const flow = await this.flowsService.create(workspaceId, {
41+
name: `${template.name} (from template)`,
42+
description: template.description,
43+
graph: template.graph,
44+
});
45+
46+
await this.repo.increment({ id }, 'useCount', 1);
47+
return flow;
48+
}
49+
}

0 commit comments

Comments
 (0)