Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Teachers Aid Laravel Package

particle-academy/teachers-aid

Teachers Aid by Particle Academy is a Laravel package that provides practical, teacher-focused tools and framework-native helpers. It’s built for quick integration, sensible defaults, and less boilerplate when adding classroom workflows to existing applications.

View on GitHub
Deep Wiki
Context7

Getting Started

TAC (Teachers-Aid Catalog) lets an LLM agent propose changes to your catalog — courses, modules, lessons, tests, and whatever else you configure — while your app remains the only thing that writes. Every mutating tool records a ChangeOperation into a ChangePlan; PlanApplier is the sole writer once a human (or your own policy) presses Apply.

Minimal setup:

composer require your-vendor/teachers-aid   # v0.3.0+
php artisan vendor:publish --tag=teachers-aid-config

Then edit config/teachers-aid.php:

return [
    // Which Eloquent models the agent may create/update/delete.
    'entities' => [
        'course'   => \App\Models\Course::class,
        'module'   => \App\Models\Module::class,
        'lesson'   => \App\Models\Lesson::class,
        'test'     => \App\Models\Test::class,
        // 'question' => \App\Models\Question::class, // add if you use propose_question
    ],

    // The blast radius for *links* (attach/detach), as `entities` is for records.
    'relations' => [
        'course.curricula' => null, // or an allow-list class/policy
    ],

    // Optional since 0.3.0; may be a closure. Only respondTo() requires one.
    'driver' => \App\Tac\OpenAiDriver::class,
];

Where to look first, in order:

  1. config/teachers-aid.php — entities, relations, driver, plus GuardsDeletion.
  2. TeachersAid::TOOL_NAMES and TeachersAid::tools() — the exact surface the model sees. This is your contract; read it before writing prompts.
  3. PlanApplier — the only writer. Understand its transaction boundaries and how it resolves ref/$ref.
  4. ChangeOperation::attach() / detach() — new in 0.3.0, and the easiest place to over-grant.

First use case: the one the release notes call out. Before 0.3.0, "rename the Trespass course" produced a proposal to create a second course because the agent had no way to learn an id. Now the canonical flow is read → propose:

$plan = TeachersAid::plan();

$plan = $plan->withTools(['catalog_search', 'catalog_get', 'propose_update']);
// model calls catalog_search("Trespass") -> gets id 412
// model calls propose_update(entity: 'course', id: 412, attributes: ['title' => 'Trespass (2025)'])

Ship that one flow before anything else; it exercises read tools, the id-resolution path, validation-on-propose, and the apply step in a single loop.

Implementation Patterns

Propose-then-apply, always

The invariant is unchanged in 0.3.0 and is the whole design: no tool writes. Each call appends a ChangeOperation to the pending ChangePlan. You render the plan to a teacher, they approve, and you call the applier.

$plan = TeachersAid::plan()->run($userMessage);   // driver responds, tools record proposals

return view('tac.review', ['plan' => $plan]);      // human review

// on POST /plan/{plan}/apply
app(PlanApplier::class)->apply($plan);

Treat $plan as the unit of work and the unit of authorization. Never let the agent's turn and the apply share a request if a human is meant to see the diff.

Let a runtime own the tool loop (tools() + PrismTools::live())

This is the headline integration change. If you already run prism-harness (or any loop you own), you don't hand the turn to TAC — you hand TAC's tools to your runtime:

$plan = TeachersAid::plan(); // or the pending plan from the previous turn

$tools = TeachersAid::tools($plan);        // pass the plan to keep building across turns
$system = TeachersAid::systemPrompt();
$prompt = TeachersAid::prompt($userMessage);  // or build your own

// Individual tool, made live so it actually performs the read / records the proposal:
$searchTool = PrismTools::live('catalog_search');

Two rules of thumb:

  • Always thread the pending plan through tools(). Passing nothing starts a fresh plan, and the model's second turn silently forgets the course it just created — you get duplicate creates, the exact failure 0.3.0 set out to fix.
  • Use TOOL_NAMES for allow-lists and UI, not for dispatch. It's a stable enumeration you can map to feature flags or role permissions.
$allowed = array_values(array_intersect(
    TeachersAid::TOOL_NAMES,
    config('tac.roles.teacher.tools')   // e.g. reads + propose_*, no propose_delete
));

Read before write, as prompt policy

The read tools (catalog_overview, catalog_search, catalog_get) never write, so they're free to expose broadly — including to read-only roles. Make "search before you propose" an explicit instruction in your system prompt; the tools exist, but the model still needs to be told to use them. A cheap trick is to make catalog_overview the first tool in the list so the model sees the shape of the catalog before it invents field names.

catalog_overview is also your best debugging aid when a proposal comes back Refused: … — it tells you what the agent thought the schema was.

Attach/detach through configured relations

ChangeOperation::attach() / detach() link existing records without touching either record's own fields — a course into a curriculum, a teacher onto a course. The relations config is the allow-list:

'relations' => [
    'course.curricula'  => null,        // open
    'course.teachers'   => \App\Tac\Policies\TeacherLink::class,
],

Keep it narrower than entities. entities governs which records exist; relations governs which edges exist, and edges are where accidental data damage hides (a detach that silently unenrols a cohort). If you can't articulate why a relation needs to be writable, leave it out — an unlisted relation is refused at propose time, not at apply time, which is the right place to fail.

Veto deletes with GuardsDeletion

The default guard allows everything, which matches the pre-0.3.0 applier. Replace it as soon as you expose propose_delete to anyone:

class ProtectPublished implements GuardsDeletion
{
    public function allows(ChangeOperation $op): bool
    {
        $model = $op->target();
        return ! ($model?->is_published ?? false);
    }
}

Note it's asked twice — when the delete is proposed, and again when the plan is applied. That's deliberate: the world can change between review and apply. Write the guard to be safe against a stale plan (re-fetch, don't trust the in-memory model), and make it return false rather than throwing so the applier can report a clean partial failure.

Handling Refused: responses

Since 0.3.0, invalid proposals are answered to the model as Refused: <reason> instead of being recorded and blowing up at Apply. That's a behavioural change in your loop: refusals are now conversational turns, not exceptions. Log them — a burst of refusals is the best signal that your prompt, your entities config, or the model's schema understanding is wrong.

// In your driver/tool wrapper: surface refusals as observations, not 500s.
// Count them per plan; >3 for the same entity usually means a config gap.

Gotchas and Tips

Config omissions now refuse instead of silently recording. The explicit warning in the release notes: if your entities config doesn't list question, propose_question is refused to the model. If you relied on a question entity working without listing it, this is the one thing that will bite you on upgrade — audit entities against the tools you expose. Also note module was added to the default entities; if you'd deliberately excluded it, your merge now includes it.

is_published is refused as a proposal field. Publishing is now a first-class concern, not an attribute the agent can flip. Route it through your own UI/action, or add an explicit entity-level carve-out — don't try to smuggle it into a generic propose_create.

ref / $ref discipline. A reused ref or a $ref pointing at nothing created is refused at propose time. This is a feature — it's the mechanism by which "create module, then create lesson inside it" works in one plan — but it means the model must be consistent across turns. Threading the pending plan through tools() (above) is what makes that possible.

propose_lesson no longer forces content_type: text. The column default applies now. If your lessons table has no default, or your default isn't text, add a default or set the field explicitly in the prompt. Any test asserting content_type === 'text' after a proposed lesson will fail for the wrong reason.

The driver is optional, and may be a closure. Only respondTo() needs one. That means TeachersAid::plan() + tools() + your own loop works with no driver configured — but calling respondTo() without one fails late and confusingly. Set it in config, or set it in a service provider, but don't leave it half-configured in production.

Validation moved earlier, not away. Everything is still validated at apply time too. Propose-time validation is a courtesy to the model, not a security boundary. Keep PlanApplier's failures visible — a plan that was valid at propose time can still fail at apply (deleted parent, unique constraint, guard veto).

Use systemPrompt() even when you write your own prompt. It carries the propose-then-apply contract and the tool semantics the model needs; diverging from it is how you get an agent that tries to "just apply" changes. Extend it, don't replace it.

Read tools for free, mutating tools by role. catalog_* never writes, so there's no reason to gate them behind the same permission as propose_delete. Split your allow-lists along that line and you get useful read-only agent surfaces for support staff.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin
skyraptor/chatter
edlugz/sasapay
abbasudo/artisan-route