Skip to content

Commit 4c43f4d

Browse files
committed
Improved tool descriptions
1 parent 6485738 commit 4c43f4d

17 files changed

+883
-37
lines changed

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@
3535
"pestphp/pest-plugin-laravel": "^3.1",
3636
"pestphp/pest-plugin-livewire": "^3.0",
3737
"laravel/pint": "^1.21",
38-
"larastan/larastan": "^3.4"
38+
"larastan/larastan": "^3.4",
39+
"kirschbaum-development/laravel-loop": "^0.1.1"
3940
},
4041
"config": {
4142
"allow-plugins": {

src/DescribeFilamentResourceTool.php

Lines changed: 37 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -38,18 +38,10 @@ public function build(): PrismTool
3838
{
3939
return app(PrismTool::class)
4040
->as($this->getName())
41-
->for('Describes the structure, fields, columns, actions, and relationships for a given Filament resource')
41+
->for('Describes the structure, fields, columns, actions, and relationships for a given Filament resource. Always call the list_filament_resources tool before calling this tool.')
4242
->withStringParameter('resource', 'The class name of the resource to describe.', required: true)
4343
->using(function (string $resource) {
44-
$resource = $this->getResourceInstance($resource);
45-
46-
return json_encode([
47-
'resource' => class_basename($resource),
48-
'model' => $resource::getModel(),
49-
// 'form' => $this->extractFormSchema($resource),
50-
'table' => $this->extractTableSchema($resource),
51-
'relationships' => $this->extractRelationshipsInfo($resource),
52-
]);
44+
return json_encode($this->describe($resource));
5345
});
5446
}
5547

@@ -58,15 +50,28 @@ public function getName(): string
5850
return 'describe_filament_resource';
5951
}
6052

61-
protected function extractBasicInfo(Resource $resource): array
53+
public function describe(string $resourceClass): array
54+
{
55+
$resource = $this->getResourceInstance($resourceClass);
56+
57+
return [
58+
'resource' => class_basename($resource),
59+
'model' => $resourceClass::getModel(),
60+
// 'form' => $this->extractFormSchema($resourceClass),
61+
'table' => $this->extractTableSchema($resource),
62+
'relationships' => $this->extractRelationshipsInfo($resource),
63+
];
64+
}
65+
66+
public function extractBasicInfo(Resource $resource): array
6267
{
6368
return [
6469
'resource' => class_basename($resource),
6570
'model' => $resource::getModel(),
6671
];
6772
}
6873

69-
protected function extractFormSchema(Resource $resource): array
74+
public function extractFormSchema(Resource $resource): array
7075
{
7176
$livewireComponent = new class extends LivewireComponent implements HasForms
7277
{
@@ -84,7 +89,7 @@ protected function extractFormSchema(Resource $resource): array
8489
return ['fields' => $fields];
8590
}
8691

87-
protected function extractNavigationInfo(Resource $resource): array
92+
public function extractNavigationInfo(Resource $resource): array
8893
{
8994
return [
9095
'group' => $resource::getNavigationGroup(),
@@ -93,7 +98,7 @@ protected function extractNavigationInfo(Resource $resource): array
9398
];
9499
}
95100

96-
protected function extractRelationshipsInfo(Resource $resource): array
101+
public function extractRelationshipsInfo(Resource $resource): array
97102
{
98103
if (! method_exists($resource, 'getRelations')) {
99104
return [];
@@ -108,13 +113,16 @@ protected function extractRelationshipsInfo(Resource $resource): array
108113
// Relationship details are often defined within the manager or inferred by naming.
109114
// This requires more specific introspection or assumptions based on conventions.
110115
// Placeholder: Use manager class name as key.
111-
$relationName = $manager->getRelationshipName(); // Assuming this method exists or convention
116+
$relationName = $manager->getRelationshipName();
117+
$modelClass = $resource::getModel();
118+
$modelInstance = new $modelClass();
119+
$relation = $modelInstance->$relationName();
112120

113121
$relationships[$relationName] = [
114-
'type' => 'hasMany', // Placeholder - determining type requires deeper inspection
122+
'type' => class_basename($relation),
115123
'manager' => $managerClass,
116-
// 'model' => $manager->getRelatedModel(), // Requires standard method
117-
// 'foreignKey' => $manager->getForeignKey(), // Requires standard method
124+
'model' => get_class($relation->getRelated()),
125+
'foreignKey' => $relation->getForeignKeyName(),
118126
];
119127
} catch (\Throwable $e) {
120128
// Log error if manager instantiation fails
@@ -124,7 +132,7 @@ protected function extractRelationshipsInfo(Resource $resource): array
124132
return $relationships;
125133
}
126134

127-
protected function extractTableSchema(Resource $resource): array
135+
public function extractTableSchema(Resource $resource): array
128136
{
129137
try {
130138
$livewireComponent = new class extends LivewireComponent implements HasTable
@@ -152,18 +160,14 @@ public function makeFilamentTranslatableContentDriver(): ?TranslatableContentDri
152160
->map(fn (array $column) => [
153161
'name' => $column['name'],
154162
'label' => $column['label'],
155-
'type' => 'searchable_column', // Indicate this is derived from a searchable column
163+
'type' => 'searchable_column',
156164
])
157-
->keyBy('name') // Key by name to potentially merge/override later if needed
165+
->keyBy('name')
158166
->all();
159167

160-
$filters = array_merge($searchableColumnFilters, $existingFilters); // Merge, giving priority to existing explicit filters if names collide
161-
162-
// $rowActions = collect($table->getActions()) // Actions column actions
163-
// ->map(fn (Action $action) => $this->mapTableAction($action))
164-
// ->all();
168+
$filters = array_merge($searchableColumnFilters, $existingFilters);
165169

166-
$bulkActions = collect($table->getBulkActions()) // Bulk actions
170+
$bulkActions = collect($table->getBulkActions())
167171
->flatMap(function ($action) {
168172
if ($action instanceof BulkActionGroup) {
169173
return collect($action->getActions())
@@ -188,7 +192,7 @@ public function makeFilamentTranslatableContentDriver(): ?TranslatableContentDri
188192
}
189193
}
190194

191-
protected function mapComponentType(Component $component): string
195+
public function mapComponentType(Component $component): string
192196
{
193197
return match (true) {
194198
$component instanceof TextInput => 'text',
@@ -203,7 +207,7 @@ protected function mapComponentType(Component $component): string
203207
};
204208
}
205209

206-
protected function mapFilterType(BaseFilter $filter): string
210+
public function mapFilterType(BaseFilter $filter): string
207211
{
208212
return match (true) {
209213
$filter instanceof TernaryFilter => 'boolean',
@@ -213,7 +217,7 @@ protected function mapFilterType(BaseFilter $filter): string
213217
};
214218
}
215219

216-
protected function mapFormComponent(Component $component, Resource $resource): ?array
220+
public function mapFormComponent(Component $component, Resource $resource): ?array
217221
{
218222
$baseInfo = [
219223
'name' => $component->getName(),
@@ -246,7 +250,7 @@ protected function mapFormComponent(Component $component, Resource $resource): ?
246250
return $baseInfo;
247251
}
248252

249-
protected function mapTableAction(Action|BulkAction $action): string
253+
public function mapTableAction(Action|BulkAction $action): string
250254
{
251255
// Map common actions to simple strings, fallback to action name
252256
$name = $action->getName();
@@ -258,7 +262,7 @@ protected function mapTableAction(Action|BulkAction $action): string
258262
// Could potentially add more details like label, icon, color if needed
259263
}
260264

261-
protected function mapTableColumn(Column $column): array
265+
public function mapTableColumn(Column $column): array
262266
{
263267
$baseInfo = [
264268
'name' => $column->getName(),
@@ -271,7 +275,7 @@ protected function mapTableColumn(Column $column): array
271275
return $baseInfo;
272276
}
273277

274-
protected function mapTableFilter(BaseFilter $filter): array
278+
public function mapTableFilter(BaseFilter $filter): array
275279
{
276280
$baseInfo = [
277281
'name' => $filter->getName(),

src/ExecuteResourceActionTool.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ public function build(): PrismTool
2424
{
2525
return app(PrismTool::class)
2626
->as($this->getName())
27-
->for('Executes a specified action on a Filament resource. Always double check with the user before executing any action.')
27+
->for('Executes a specified action on a Filament resource. Always double check with the user before executing any action. Always call the describe_filament_resource tool before calling this tool.')
2828
->withStringParameter('resource', 'The class name of the resource to execute action on.', required: true)
2929
->withStringParameter('action', 'The name of the action to execute.', required: true)
3030
->withStringParameter('actionType', 'The type of action: "bulk".', required: false)

src/GetFilamentResourceDataTool.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ public function build(): PrismTool
2222
{
2323
return app(PrismTool::class)
2424
->as($this->getName())
25-
->for('Gets the data for a given Filament resource, applying optional filters provided in the describe_filament_resource tool.')
25+
->for('Gets the data for a given Filament resource, applying optional filters provided in the describe_filament_resource tool. Always call the describe_filament_resource tool before calling this tool. Try to use the available filters to get the data you need.')
2626
->withStringParameter('resource', 'The resource class name of the resource to get data for, from the list_filament_resources tool.', required: true)
2727
->withStringParameter('filters', 'JSON string of filters to apply (e.g., \'{"status": "published", "author_id": [1, 2]}\').', required: false)
2828
->using(function (string $resource, ?string $filters = null) {

src/ListFilamentResourcesTool.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ public function build(): PrismTool
2525
{
2626
return app(PrismTool::class)
2727
->as('list_filament_resources')
28-
->for('Lists all available Filament resources. Filament resources are used to list, fetch and manage data for a given data resource (database table, model, etc.). You cannot use a resource that is not listed here.')
28+
->for('Lists all available Filament resources. Filament resources are used to list, fetch and manage data for a given data resource (database table, model, etc.). You cannot use a resource that is not listed here. Always call this tool first to know which resources are available.')
2929
->using(function () {
3030
return collect($this->getResources())->map(
3131
fn (string $resource) => $resource
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
<?php
2+
3+
use Tests\Feature\TestUserResource;
4+
use Tests\Feature\TestPostWithRelationsResource;
5+
use Tests\Feature\TestUserWithRelationsResource;
6+
use Kirschbaum\Loop\Filament\DescribeFilamentResourceTool;
7+
8+
it('can instantiate the describe filament resource tool', function () {
9+
$tool = new DescribeFilamentResourceTool();
10+
11+
expect($tool)->toBeInstanceOf(DescribeFilamentResourceTool::class);
12+
expect($tool->getName())->toBe('describe_filament_resource');
13+
});
14+
15+
it('can extract table schema using the tool', function () {
16+
$tool = new DescribeFilamentResourceTool();
17+
$resource = app(TestUserResource::class);
18+
19+
$tableSchema = $tool->extractTableSchema($resource);
20+
21+
expect($tableSchema)->toBeArray()
22+
->and($tableSchema)->toHaveKeys(['columns', 'filters', 'actions'])
23+
->and($tableSchema['columns'])->toBeArray()
24+
->and($tableSchema['columns'])->toHaveCount(3);
25+
26+
$columnNames = collect($tableSchema['columns'])->pluck('name')->toArray();
27+
expect($columnNames)->toContain('name', 'email', 'created_at');
28+
});
29+
30+
it('can extract column properties through the tool', function () {
31+
$tool = new DescribeFilamentResourceTool();
32+
$resource = app(TestUserResource::class);
33+
34+
$tableSchema = $tool->extractTableSchema($resource);
35+
$columns = $tableSchema['columns'];
36+
37+
$nameColumn = collect($columns)->firstWhere('name', 'name');
38+
expect($nameColumn)->toBeArray()
39+
->and($nameColumn['searchable'])->toBeTrue()
40+
->and($nameColumn['sortable'])->toBeTrue();
41+
42+
$emailColumn = collect($columns)->firstWhere('name', 'email');
43+
expect($emailColumn)->toBeArray()
44+
->and($emailColumn['searchable'])->toBeTrue()
45+
->and($emailColumn['sortable'])->toBeTrue();
46+
});
47+
48+
it('can extract bulk actions through the tool', function () {
49+
$tool = new DescribeFilamentResourceTool();
50+
$resource = app(TestUserResource::class);
51+
52+
$tableSchema = $tool->extractTableSchema($resource);
53+
$bulkActions = $tableSchema['actions']['bulk'];
54+
55+
expect($bulkActions)->toBeArray()
56+
->and($bulkActions)->toContain('delete');
57+
});
58+
59+
it('can extract resource relationships through the tool', function () {
60+
$tool = new DescribeFilamentResourceTool();
61+
$resource = app(TestUserWithRelationsResource::class);
62+
63+
$relationships = $tool->extractRelationshipsInfo($resource);
64+
65+
expect($relationships)->toBeArray()
66+
->and($relationships)->toHaveKeys(['posts', 'comments']);
67+
68+
expect($relationships['posts'])->toBeArray()
69+
->and($relationships['posts']['type'])->toBe('HasMany')
70+
->and($relationships['posts']['manager'])->toBe(\Tests\Feature\TestPostsRelationManager::class);
71+
72+
expect($relationships['comments'])->toBeArray()
73+
->and($relationships['comments']['type'])->toBe('HasMany')
74+
->and($relationships['comments']['manager'])->toBe(\Tests\Feature\TestCommentsRelationManager::class);
75+
});
76+
77+
it('can describe a complete resource using the tool', function () {
78+
$tool = new DescribeFilamentResourceTool();
79+
80+
$result = $tool->describe(TestUserWithRelationsResource::class);
81+
82+
expect($result)->toBeArray()
83+
->and($result)->toHaveKeys(['resource', 'model', 'table', 'relationships'])
84+
->and($result['resource'])->toBe('TestUserWithRelationsResource')
85+
->and($result['model'])->toBe(\Tests\Feature\TestUser::class)
86+
->and($result['table'])->toBeArray()
87+
->and($result['relationships'])->toBeArray();
88+
89+
// Verify table structure
90+
expect($result['table']['columns'])->toHaveCount(3);
91+
expect($result['table']['actions']['bulk'])->toContain('delete');
92+
93+
// Verify relationships structure
94+
expect($result['relationships'])->toHaveKeys(['posts', 'comments']);
95+
});
96+
97+
it('can extract different relationship types including belongsTo', function () {
98+
$tool = new DescribeFilamentResourceTool();
99+
$resource = app(TestPostWithRelationsResource::class);
100+
101+
$relationships = $tool->extractRelationshipsInfo($resource);
102+
103+
expect($relationships)->toBeArray()
104+
->and($relationships)->toHaveKeys(['comments', 'category']);
105+
106+
expect($relationships['comments'])->toBeArray()
107+
->and($relationships['comments']['type'])->toBe('HasMany')
108+
->and($relationships['comments']['manager'])->toBe(\Tests\Feature\TestCommentsRelationManager::class);
109+
110+
expect($relationships['category'])->toBeArray()
111+
->and($relationships['category']['type'])->toBe('BelongsTo')
112+
->and($relationships['category']['manager'])->toBe(\Tests\Feature\TestCategoryRelationManager::class);
113+
});
114+
115+
it('can describe a resource with mixed relationship types', function () {
116+
$tool = new DescribeFilamentResourceTool();
117+
118+
$result = $tool->describe(TestPostWithRelationsResource::class);
119+
120+
expect($result)->toBeArray()
121+
->and($result)->toHaveKeys(['resource', 'model', 'table', 'relationships'])
122+
->and($result['resource'])->toBe('TestPostWithRelationsResource')
123+
->and($result['model'])->toBe(\Tests\Feature\TestPost::class);
124+
125+
// Verify we have both relationship types
126+
$relationships = $result['relationships'];
127+
expect($relationships)->toHaveKeys(['comments', 'category'])
128+
->and($relationships['comments']['type'])->toBe('HasMany')
129+
->and($relationships['category']['type'])->toBe('BelongsTo');
130+
});

tests/Feature/TestCategory.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
namespace Tests\Feature;
4+
5+
use Illuminate\Database\Eloquent\Model;
6+
use Illuminate\Database\Eloquent\Relations\HasMany;
7+
8+
class TestCategory extends Model
9+
{
10+
protected $fillable = [
11+
'name',
12+
'description',
13+
'created_at',
14+
];
15+
16+
protected $casts = [
17+
'created_at' => 'datetime',
18+
];
19+
20+
public function posts(): HasMany
21+
{
22+
return $this->hasMany(TestPost::class);
23+
}
24+
}

0 commit comments

Comments
 (0)