Skip to content

Commit cdac342

Browse files
authored
Merge pull request #7 from MujahidAbbas/feature/phase-1-templates-feedback
feat: Upgrade Flowforge to v3 and add Kanban test suite
2 parents 3c80a20 + cf4ccd1 commit cdac342

File tree

60 files changed

+5676
-48
lines changed

Some content is hidden

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

60 files changed

+5676
-48
lines changed

app/Enums/DocumentType.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,12 @@ enum DocumentType: string
66
{
77
case Prd = 'prd';
88
case Tech = 'tech';
9+
10+
public function label(): string
11+
{
12+
return match ($this) {
13+
self::Prd => 'PRD',
14+
self::Tech => 'Technical Specification',
15+
};
16+
}
917
}

app/Enums/FeedbackType.php

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php
2+
3+
namespace App\Enums;
4+
5+
enum FeedbackType: string
6+
{
7+
case Completeness = 'completeness';
8+
case Clarity = 'clarity';
9+
case Technical = 'technical';
10+
case Stakeholder = 'stakeholder';
11+
case Overall = 'overall';
12+
13+
public function label(): string
14+
{
15+
return match ($this) {
16+
self::Completeness => 'Completeness',
17+
self::Clarity => 'Clarity',
18+
self::Technical => 'Technical Review',
19+
self::Stakeholder => 'Stakeholder Ready',
20+
self::Overall => 'Overall Assessment',
21+
};
22+
}
23+
24+
public function description(): string
25+
{
26+
return match ($this) {
27+
self::Completeness => 'What\'s missing from this document?',
28+
self::Clarity => 'What parts are unclear or ambiguous?',
29+
self::Technical => 'What technical gaps exist?',
30+
self::Stakeholder => 'What questions will stakeholders ask?',
31+
self::Overall => 'Rate and suggest improvements',
32+
};
33+
}
34+
35+
public function icon(): string
36+
{
37+
return match ($this) {
38+
self::Completeness => 'clipboard-document-check',
39+
self::Clarity => 'eye',
40+
self::Technical => 'cpu-chip',
41+
self::Stakeholder => 'user-group',
42+
self::Overall => 'star',
43+
};
44+
}
45+
}

app/Enums/TemplateCategory.php

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php
2+
3+
namespace App\Enums;
4+
5+
enum TemplateCategory: string
6+
{
7+
case Core = 'core';
8+
case Technical = 'technical';
9+
case Strategy = 'strategy';
10+
case Research = 'research';
11+
case Analysis = 'analysis';
12+
case Community = 'community';
13+
14+
public function label(): string
15+
{
16+
return match ($this) {
17+
self::Core => 'Core Templates',
18+
self::Technical => 'Technical & Product Documentation',
19+
self::Strategy => 'Product Planning & Strategy',
20+
self::Research => 'Research, Testing & UX',
21+
self::Analysis => 'Analysis & Reporting',
22+
self::Community => 'Community Templates',
23+
};
24+
}
25+
26+
public function icon(): string
27+
{
28+
return match ($this) {
29+
self::Core => '📄',
30+
self::Technical => '🔧',
31+
self::Strategy => '🎯',
32+
self::Research => '🔬',
33+
self::Analysis => '📊',
34+
self::Community => '👥',
35+
};
36+
}
37+
}

app/Jobs/GenerateFeedbackJob.php

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
<?php
2+
3+
namespace App\Jobs;
4+
5+
use App\Enums\FeedbackType;
6+
use App\Jobs\Concerns\ResolvesAiProvider;
7+
use App\Models\Document;
8+
use Illuminate\Bus\Queueable;
9+
use Illuminate\Contracts\Queue\ShouldQueue;
10+
use Illuminate\Foundation\Bus\Dispatchable;
11+
use Illuminate\Queue\InteractsWithQueue;
12+
use Illuminate\Queue\Middleware\RateLimited;
13+
use Illuminate\Queue\SerializesModels;
14+
use Illuminate\Support\Facades\Cache;
15+
use Prism\Prism\Facades\Prism;
16+
17+
class GenerateFeedbackJob implements ShouldQueue
18+
{
19+
use Dispatchable, InteractsWithQueue, Queueable, ResolvesAiProvider, SerializesModels;
20+
21+
public int $tries = 3;
22+
23+
/** @var array<int, int> */
24+
public array $backoff = [10, 30, 60];
25+
26+
public function __construct(
27+
public string $documentId,
28+
public FeedbackType $feedbackType,
29+
public string $cacheKey,
30+
) {
31+
$this->afterCommit();
32+
}
33+
34+
/**
35+
* @return array<int, object>
36+
*/
37+
public function middleware(): array
38+
{
39+
return [new RateLimited('llm:requests')];
40+
}
41+
42+
public function handle(): void
43+
{
44+
$document = Document::with(['currentVersion', 'project'])->findOrFail($this->documentId);
45+
$project = $document->project;
46+
47+
$system = view('prompts.feedback.system')->render();
48+
$prompt = view("prompts.feedback.{$this->feedbackType->value}", [
49+
'content' => $document->currentVersion->content_md,
50+
'documentType' => strtolower($document->type->label()),
51+
])->render();
52+
53+
$provider = $this->resolveProvider($project->preferred_provider ?? 'anthropic');
54+
$model = $project->preferred_model ?? 'claude-sonnet-4-20250514';
55+
56+
$response = Prism::text()
57+
->using($provider, $model)
58+
->withMaxTokens(2000)
59+
->withSystemPrompt($system)
60+
->withPrompt($prompt)
61+
->withClientOptions(['timeout' => 60])
62+
->asText();
63+
64+
Cache::put($this->cacheKey, [
65+
'feedback' => $response->text,
66+
'type' => $this->feedbackType->value,
67+
'generated_at' => now()->toIso8601String(),
68+
], now()->addHour());
69+
}
70+
}

app/Jobs/GeneratePrdJob.php

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,10 @@ class GeneratePrdJob implements ShouldBeUnique, ShouldQueue
2828

2929
public int $tries = 5;
3030

31+
/** @var array<int, int> */
3132
public array $backoff = [10, 30, 60, 120, 300];
3233

33-
public int $uniqueFor = 3600; // 1 hour
34+
public int $uniqueFor = 3600;
3435

3536
public function __construct(public string $planRunId)
3637
{
@@ -42,6 +43,9 @@ public function uniqueId(): string
4243
return $this->planRunId.':prd';
4344
}
4445

46+
/**
47+
* @return array<int, object>
48+
*/
4549
public function middleware(): array
4650
{
4751
return [new RateLimited('llm:requests')];
@@ -68,9 +72,15 @@ public function handle(): void
6872
]);
6973

7074
try {
75+
// Load template if set
76+
$template = $run->project->prdTemplate;
77+
7178
$providerEnum = $this->resolveProvider($run->provider);
7279
$system = view('prompts.prd.system')->render();
73-
$prompt = view('prompts.prd.user', ['project' => $run->project])->render();
80+
$prompt = view('prompts.prd.user', [
81+
'project' => $run->project,
82+
'template' => $template,
83+
])->render();
7484

7585
$response = Prism::text()
7686
->using($providerEnum, $run->model)
@@ -109,6 +119,11 @@ public function handle(): void
109119
// Point document to new version
110120
$doc->update(['current_version_id' => $version->id]);
111121

122+
// Record template usage
123+
if ($template) {
124+
$template->recordUsage();
125+
}
126+
112127
// Mark step as succeeded
113128
$step->update([
114129
'status' => PlanRunStepStatus::Succeeded,

app/Jobs/GenerateTasksJob.php

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
use Prism\Prism\Enums\Provider;
2525
use Prism\Prism\Exceptions\PrismRateLimitedException;
2626
use Prism\Prism\Facades\Prism;
27-
use Relaticle\Flowforge\Services\Rank;
27+
use Relaticle\Flowforge\Services\DecimalPosition;
2828
use Throwable;
2929

3030
class GenerateTasksJob implements ShouldBeUnique, ShouldQueue
@@ -146,8 +146,8 @@ private function persistTasks(string $projectId, TaskSet $taskSet, array $struct
146146
$tasks = $structured['tasks'] ?? [];
147147
$tempIdMap = [];
148148

149-
// Generate lexicographic positions for proper Flowforge ordering
150-
$currentRank = Rank::forEmptySequence();
149+
// Generate decimal positions for Flowforge ordering
150+
$currentPosition = DecimalPosition::forEmptyColumn();
151151

152152
foreach ($tasks as $data) {
153153
$task = Task::create([
@@ -165,11 +165,11 @@ private function persistTasks(string $projectId, TaskSet $taskSet, array $struct
165165
'source_refs' => $data['source_refs'] ?? [],
166166
'labels' => $data['labels'] ?? [],
167167
'depends_on' => [],
168-
'position' => $currentRank->get(),
168+
'position' => $currentPosition,
169169
]);
170170

171171
// Generate next position
172-
$currentRank = Rank::after($currentRank);
172+
$currentPosition = DecimalPosition::after($currentPosition);
173173

174174
if (isset($data['temp_id'])) {
175175
$tempIdMap[$data['temp_id']] = $task->id;

app/Jobs/GenerateTechSpecJob.php

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,10 @@ class GenerateTechSpecJob implements ShouldBeUnique, ShouldQueue
2828

2929
public int $tries = 5;
3030

31+
/** @var array<int, int> */
3132
public array $backoff = [10, 30, 60, 120, 300];
3233

33-
public int $uniqueFor = 3600; // 1 hour
34+
public int $uniqueFor = 3600;
3435

3536
public function __construct(public string $planRunId)
3637
{
@@ -42,6 +43,9 @@ public function uniqueId(): string
4243
return $this->planRunId.':tech';
4344
}
4445

46+
/**
47+
* @return array<int, object>
48+
*/
4549
public function middleware(): array
4650
{
4751
return [new RateLimited('llm:requests')];
@@ -72,11 +76,15 @@ public function handle(): void
7276

7377
$prdText = $prdDoc?->currentVersion?->content_md ?? '';
7478

79+
// Load template if set
80+
$template = $run->project->techTemplate;
81+
7582
$providerEnum = $this->resolveProvider($run->provider);
7683
$system = view('prompts.tech.system')->render();
7784
$prompt = view('prompts.tech.user', [
7885
'project' => $run->project,
7986
'prd' => $prdText,
87+
'template' => $template,
8088
])->render();
8189

8290
$response = Prism::text()
@@ -116,6 +124,11 @@ public function handle(): void
116124
// Point document to new version
117125
$doc->update(['current_version_id' => $version->id]);
118126

127+
// Record template usage
128+
if ($template) {
129+
$template->recordUsage();
130+
}
131+
119132
// Mark step as succeeded
120133
$step->update([
121134
'status' => PlanRunStepStatus::Succeeded,

0 commit comments

Comments
 (0)