Skip to content
Merged
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
75 changes: 75 additions & 0 deletions app/Ai/Agents/Ideas/IdeaRefinementAgent.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<?php

namespace App\Ai\Agents\Ideas;

use Illuminate\Contracts\JsonSchema\JsonSchema;
use Laravel\Ai\Attributes\Model;
use Laravel\Ai\Attributes\Provider;
use Laravel\Ai\Contracts\Agent;
use Laravel\Ai\Contracts\Conversational;
use Laravel\Ai\Contracts\HasStructuredOutput;
use Laravel\Ai\Contracts\HasTools;
use Laravel\Ai\Contracts\Tool;
use Laravel\Ai\Messages\Message;
use Laravel\Ai\Promptable;
use Stringable;

#[Provider('ollama')]
#[Model('llama3.1')]
class IdeaRefinementAgent implements Agent, Conversational, HasStructuredOutput, HasTools
{
use Promptable;

/**
* Get the instructions that the agent should follow.
*/
public function instructions(): Stringable|string
{
return <<<'TEXT'
You refine private idea drafts for a software factory.

Keep the output focused on the draft owner's context only.
Do not invent implementation details.
Return concise, actionable refinement data around:
- problem statement
- target users
- expected outcomes
- scope gaps
- follow-up questions
TEXT;
}

/**
* Get the list of messages comprising the conversation so far.
*
* @return Message[]
*/
public function messages(): iterable
{
return [];
}

/**
* Get the tools available to the agent.
*
* @return Tool[]
*/
public function tools(): iterable
{
return [];
}

/**
* Get the agent's structured output schema definition.
*/
public function schema(JsonSchema $schema): array
{
return [
'problem' => $schema->string()->required(),
'users' => $schema->string()->required(),
'outcomes' => $schema->string()->required(),
'scope_gaps' => $schema->string()->required(),
'follow_up_questions' => $schema->string()->required(),
];
}
}
119 changes: 119 additions & 0 deletions app/Http/Controllers/IdeaController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
<?php

namespace App\Http\Controllers;

use App\Models\Idea;
use App\Models\Project;
use App\Services\Ideas\IdeaRefinementService;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Gate;
use Illuminate\View\View;

class IdeaController extends Controller
{
public function index(Request $request): View
{
Gate::authorize('viewAny', Idea::class);

$ideas = $request->user()
->ideas()
->latest('updated_at')
->latest('id')
->get();

return view('ideas.index', [
'ideas' => $ideas,
]);
}

public function create(): View
{
Gate::authorize('create', Idea::class);

return view('ideas.create');
}

public function store(Request $request): RedirectResponse
{
Gate::authorize('create', Idea::class);

$validated = $request->validate([
'title' => ['required', 'string', 'max:255'],
'summary' => ['required', 'string', 'max:500'],
'details' => ['required', 'string'],
]);
Comment on lines +41 to +45
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The draft workflow/issue text mentions saving partial work, and the ideas table allows summary/details to be NULL, but the store validation currently requires both fields. This prevents creating a minimal draft (e.g., title-only) and forces users to fill everything upfront. Consider making summary/details nullable/optional in validation (and handling empty strings consistently) so partial drafts can be saved.

Copilot uses AI. Check for mistakes.

$idea = $request->user()->ideas()->create($validated);

return redirect()
->route('ideas.edit', $idea)
->with('status', 'Idea draft created.');
}

public function edit(Idea $idea): View
{
Gate::authorize('update', $idea);

return view('ideas.edit', [
'idea' => $idea,
]);
}

public function update(Request $request, Idea $idea): RedirectResponse
{
Gate::authorize('update', $idea);

$validated = $request->validate([
'title' => ['required', 'string', 'max:255'],
'summary' => ['required', 'string', 'max:500'],
'details' => ['required', 'string'],
Comment on lines +67 to +70
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update validation requires summary and details, which means users cannot progressively fill in a draft or clear a field once saved. If drafts are intended to support partial/incremental writing, make these fields optional on update as well (and decide whether an empty string should be stored as NULL).

Suggested change
$validated = $request->validate([
'title' => ['required', 'string', 'max:255'],
'summary' => ['required', 'string', 'max:500'],
'details' => ['required', 'string'],
$request->merge([
'summary' => $request->input('summary') === '' ? null : $request->input('summary'),
'details' => $request->input('details') === '' ? null : $request->input('details'),
]);
$validated = $request->validate([
'title' => ['required', 'string', 'max:255'],
'summary' => ['nullable', 'string', 'max:500'],
'details' => ['nullable', 'string'],

Copilot uses AI. Check for mistakes.
]);

$idea->update($validated);

return redirect()
->route('ideas.edit', $idea)
->with('status', 'Idea draft updated.');
}

public function destroy(Idea $idea): RedirectResponse
{
Gate::authorize('delete', $idea);

$idea->delete();

return redirect()
->route('ideas.index')
->with('status', 'Idea draft deleted.');
}

public function refine(Idea $idea, IdeaRefinementService $service): RedirectResponse
{
Gate::authorize('update', $idea);

return redirect()
->route('ideas.edit', $idea)
->with('ideaRefinement', $service->refine($idea));
}

public function propose(Idea $idea): RedirectResponse
{
Gate::authorize('update', $idea);

$project = Project::query()->create([
'idea_id' => $idea->id,
'user_id' => $idea->user_id,
'title' => $idea->title,
'summary' => $idea->summary,
'details' => $idea->details,
'status' => 'proposed',
'proposed_at' => now(),
]);

return redirect()
->route('ideas.edit', $idea)
->with('status', 'Idea proposed as a project.')
->with('proposedProjectId', $project->id);
}
}
6 changes: 6 additions & 0 deletions app/Models/Idea.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasOne;

#[Fillable(['user_id', 'title', 'summary', 'details', 'shared_at'])]
class Idea extends Model
Expand All @@ -25,4 +26,9 @@ public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}

public function project(): HasOne
{
return $this->hasOne(Project::class);
}
}
33 changes: 33 additions & 0 deletions app/Models/Project.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

namespace App\Models;

use Database\Factories\ProjectFactory;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

#[Fillable(['idea_id', 'user_id', 'title', 'summary', 'details', 'status', 'proposed_at'])]
class Project extends Model
{
/** @use HasFactory<ProjectFactory> */
use HasFactory;

protected function casts(): array
{
return [
'proposed_at' => 'datetime',
];
}

public function idea(): BelongsTo
{
return $this->belongsTo(Idea::class);
}

public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}
65 changes: 65 additions & 0 deletions app/Policies/IdeaPolicy.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<?php

namespace App\Policies;

use App\Models\Idea;
use App\Models\User;

class IdeaPolicy
{
/**
* Determine whether the user can view any models.
*/
public function viewAny(User $user): bool
{
return true;
}

/**
* Determine whether the user can view the model.
*/
public function view(User $user, Idea $idea): bool
{
return $idea->user->is($user);
}
Comment on lines +21 to +24
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These policy checks use $idea->user->is($user), which will lazy-load the user relationship during authorization (extra query per request) when it isn’t already loaded. Since ownership is stored on the model, comparing $idea->user_id === $user->id avoids the query and keeps auth checks cheap.

Copilot uses AI. Check for mistakes.

/**
* Determine whether the user can create models.
*/
public function create(User $user): bool
{
return true;
}

/**
* Determine whether the user can update the model.
*/
public function update(User $user, Idea $idea): bool
{
return $idea->user->is($user);
}

/**
* Determine whether the user can delete the model.
*/
public function delete(User $user, Idea $idea): bool
{
return $idea->user->is($user);
}

/**
* Determine whether the user can restore the model.
*/
public function restore(User $user, Idea $idea): bool
{
return $idea->user->is($user);
}

/**
* Determine whether the user can permanently delete the model.
*/
public function forceDelete(User $user, Idea $idea): bool
{
return $idea->user->is($user);
}
}
4 changes: 4 additions & 0 deletions app/Providers/AppServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

namespace App\Providers;

use App\Models\Idea;
use App\Models\User;
use App\Policies\IdeaPolicy;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\ServiceProvider;

Expand All @@ -21,6 +23,8 @@ public function register(): void
*/
public function boot(): void
{
Gate::policy(Idea::class, IdeaPolicy::class);

Gate::before(function (User $user): ?bool {
return $user->isAn('admin') ? true : null;
});
Expand Down
43 changes: 43 additions & 0 deletions app/Services/Ideas/IdeaRefinementService.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

namespace App\Services\Ideas;

use App\Ai\Agents\Ideas\IdeaRefinementAgent;
use App\Models\Idea;
use Illuminate\Support\Arr;

class IdeaRefinementService
{
/**
* @return array{
* problem: string,
* users: string,
* outcomes: string,
* scope_gaps: string,
* follow_up_questions: string
* }
*/
public function refine(Idea $idea): array
{
$prompt = implode("\n\n", array_filter([
'Refine this private idea draft for the owner only.',
'Title: '.$idea->title,
'Summary: '.$idea->summary,
'Details: '.$idea->details,
]));

$response = IdeaRefinementAgent::make()->prompt(
$prompt,
provider: 'ollama',
model: config('ai.providers.ollama.model')
);

return Arr::only($response->toArray(), [
'problem',
'users',
'outcomes',
'scope_gaps',
'follow_up_questions',
]);
}
}
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"require": {
"php": "^8.3",
"blade-ui-kit/blade-heroicons": "^2.7",
"laravel/ai": "^0.5.0",
"laravel/fortify": "^1.36",
"laravel/framework": "^13.0",
"laravel/tinker": "^3.0",
Expand Down
Loading
Loading