-
Notifications
You must be signed in to change notification settings - Fork 0
feat(ideas): add private draft crud #43
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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(), | ||
| ]; | ||
| } | ||
| } |
| 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'], | ||||||||||||||||||||||||||||
| ]); | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| $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
|
||||||||||||||||||||||||||||
| $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'], |
| 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); | ||
| } | ||
| } |
| 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
|
||
|
|
||
| /** | ||
| * 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); | ||
| } | ||
| } | ||
| 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', | ||
| ]); | ||
| } | ||
| } |
There was a problem hiding this comment.
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
ideastable allowssummary/detailsto 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 makingsummary/detailsnullable/optional in validation (and handling empty strings consistently) so partial drafts can be saved.