Repository: echolabsdev/prism (php)

Issue:
Support for `object` and `array` types in Tool params.
OpenAI (and others I believe) allow you to us `object` and `array` types for Tool params. They require additional fields to build the param data, for example;

```
'tags' => [
    'type' => 'array',
    'items' => [
        'type' => 'string',
        'description' => 'Tags or categories for the recipe',
    ],
],
```

```
'nutrition' => [
    'type' => 'object',
    'properties' => [
        'calories' => [
            'type' => 'integer',
            'description' => 'Calories per serving',
        ],
        'fat' => [
            'type' => 'number',
            'description' => 'Fat content in grams per serving',
        ],
        'carbs' => [
            'type' => 'number',
            'description' => 'Carbohydrate content in grams per serving',
        ],
        'protein' => [
            'type' => 'number',
            'description' => 'Protein content in grams per serving',
        ],
    ],
],
```

It might be helpful to consider the building of ValueObjects for params in the future, this could make it easier to define the params without a large list of args to the `withParameter` method.

Pull request description:
Tool Parameter Refactor
## Description

Closes #15 

Refactors tool parameters into a fluent builder pattern and updates existing provider tool maps to adjust.

## Breaking Changes

- `Tool::withParameter()` signature changed

Patch:
diff --git a/README.md b/README.md
index f140503..3974b46 100644
--- a/README.md
+++ b/README.md
@@ -65,7 +65,7 @@ echo $prism()->text;
 
 ## Authors
 
-This library is created by [TJ Miller](https://tjmiller.me) with contributions from the [Open Source Community](https://github.com/vercel/ai/graphs/contributors).
+This library is created by [TJ Miller](https://tjmiller.me) with contributions from the [Open Source Community](https://github.com/echolabsdev/prism/graphs/contributors).
 
 ## License
 
diff --git a/src/Concerns/HandlesToolCalls.php b/src/Concerns/HandlesToolCalls.php
index 5c48513..591acbe 100644
--- a/src/Concerns/HandlesToolCalls.php
+++ b/src/Concerns/HandlesToolCalls.php
@@ -16,7 +16,7 @@ trait HandlesToolCalls
     /**
      * @param  array<int, Tool>  $tools
      */
-    protected function handleToolCall(array $tools, ToolCall $toolCall): string
+    protected function handleToolCall(array $tools, ToolCall $toolCall): ?string
     {
         try {
             /** @var Tool $tool */
diff --git a/src/Concerns/HasProvider.php b/src/Concerns/HasProvider.php
index cfa84c1..cc817cb 100644
--- a/src/Concerns/HasProvider.php
+++ b/src/Concerns/HasProvider.php
@@ -11,7 +11,7 @@ trait HasProvider
 {
     protected Provider $provider;
 
-    public function using(ProviderEnum|string $provider, string $model): self
+    public function using(string|ProviderEnum $provider, string $model): self
     {
         $this->provider = app('prism-manager')->resolve($provider);
         $this->provider->usingModel($model);
diff --git a/src/Contracts/Schema.php b/src/Contracts/Schema.php
new file mode 100644
index 0000000..662217c
--- /dev/null
+++ b/src/Contracts/Schema.php
@@ -0,0 +1,15 @@
+<?php
+
+declare(strict_types=1);
+
+namespace EchoLabs\Prism\Contracts;
+
+interface Schema
+{
+    public function name(): string;
+
+    /**
+     * @return array<string, mixed>
+     */
+    public function toArray(): array;
+}
diff --git a/src/Facades/Tool.php b/src/Facades/Tool.php
index 69a9156..fb55854 100644
--- a/src/Facades/Tool.php
+++ b/src/Facades/Tool.php
@@ -4,18 +4,26 @@ declare(strict_types=1);
 
 namespace EchoLabs\Prism\Facades;
 
+use Closure;
+use EchoLabs\Prism\Contracts\Schema;
 use EchoLabs\Prism\Tool as ToolConcrete;
 
 /**
  * @method static self as(string $name)
  * @method static self for(string $description)
  * @method static self using(Closure|callable $fn)
- * @method static self withParameter(string $name, string $description, string $type = 'string', bool $required = true)
+ * @method static self withParameter(Schema $parameter, bool $required = true)
+ * @method static self withStringParameter(string $name, string $description, bool $required = true)
+ * @method static self withNumberParameter(string $name, string $description, bool $required = true)
+ * @method static self withBooleanParameter(string $name, string $description, bool $required = true)
+ * @method static self withEnumParemeter(string $name, string $description, array $options, bool $required = true)
+ * @method static self withArrayParameter(string $name, string $description, Schema $items, bool $required = true)
+ * @method static self withObjectParameter(string $name, string $description, array $properties, array $requiredFields = [], bool $allowAdditionalProperties = false, bool $required = true)
  * @method static array<int, string> requiredParameters()
- * @method static array<int, array<string, string|bool>> parameters()
+ * @method static array<string, array<string, mixed>> parameters()
  * @method static string name()
  * @method static string description()
- * @method static string __invoke(...$args)
+ * @method static string handle(string|int|float ...$args)
  */
 class Tool
 {
diff --git a/src/Providers/Anthropic/Tool.php b/src/Providers/Anthropic/Tool.php
index ca22432..e58ef83 100644
--- a/src/Providers/Anthropic/Tool.php
+++ b/src/Providers/Anthropic/Tool.php
@@ -17,13 +17,7 @@ class Tool extends ProviderTool
             'description' => $tool->description(),
             'input_schema' => [
                 'type' => 'object',
-                'properties' => collect($tool->parameters())
-                    ->keyBy('name')
-                    ->map(fn (array $field): array => [
-                        'description' => $field['description'],
-                        'type' => $field['type'],
-                    ])
-                    ->toArray(),
+                'properties' => $tool->parameters(),
                 'required' => $tool->requiredParameters(),
             ],
         ];
diff --git a/src/Providers/Ollama/Tool.php b/src/Providers/Ollama/Tool.php
index 09d5ce0..9f1eb97 100644
--- a/src/Providers/Ollama/Tool.php
+++ b/src/Providers/Ollama/Tool.php
@@ -19,13 +19,7 @@ class Tool extends ProviderTool
                 'description' => $tool->description(),
                 'parameters' => [
                     'type' => 'object',
-                    'properties' => collect($tool->parameters())
-                        ->keyBy('name')
-                        ->map(fn (array $field): array => [
-                            'description' => $field['description'],
-                            'type' => $field['type'],
-                        ])
-                        ->toArray(),
+                    'properties' => $tool->parameters(),
                     'required' => $tool->requiredParameters(),
                 ],
             ],
diff --git a/src/Providers/OpenAI/Tool.php b/src/Providers/OpenAI/Tool.php
index f06a135..6885462 100644
--- a/src/Providers/OpenAI/Tool.php
+++ b/src/Providers/OpenAI/Tool.php
@@ -19,13 +19,7 @@ class Tool extends ProviderTool
                 'description' => $tool->description(),
                 'parameters' => [
                     'type' => 'object',
-                    'properties' => collect($tool->parameters())
-                        ->keyBy('name')
-                        ->map(fn (array $field): array => [
-                            'description' => $field['description'],
-                            'type' => $field['type'],
-                        ])
-                        ->toArray(),
+                    'properties' => $tool->parameters(),
                     'required' => $tool->requiredParameters(),
                 ],
             ],
diff --git a/src/Schema/ArraySchema.php b/src/Schema/ArraySchema.php
new file mode 100644
index 0000000..3d1ea3e
--- /dev/null
+++ b/src/Schema/ArraySchema.php
@@ -0,0 +1,32 @@
+<?php
+
+declare(strict_types=1);
+
+namespace EchoLabs\Prism\Schema;
+
+use EchoLabs\Prism\Contracts\Schema;
+
+class ArraySchema implements Schema
+{
+    public function __construct(
+        public readonly string $name,
+        public readonly string $description,
+        public readonly Schema $item,
+    ) {}
+
+    #[\Override]
+    public function name(): string
+    {
+        return $this->name;
+    }
+
+    #[\Override]
+    public function toArray(): array
+    {
+        return [
+            'description' => $this->description,
+            'type' => 'array',
+            'items' => $this->item->toArray(),
+        ];
+    }
+}
diff --git a/src/Schema/BooleanSchema.php b/src/Schema/BooleanSchema.php
new file mode 100644
index 0000000..09bb99d
--- /dev/null
+++ b/src/Schema/BooleanSchema.php
@@ -0,0 +1,30 @@
+<?php
+
+declare(strict_types=1);
+
+namespace EchoLabs\Prism\Schema;
+
+use EchoLabs\Prism\Contracts\Schema;
+
+class BooleanSchema implements Schema
+{
+    public function __construct(
+        public readonly string $name,
+        public readonly string $description,
+    ) {}
+
+    #[\Override]
+    public function name(): string
+    {
+        return $this->name;
+    }
+
+    #[\Override]
+    public function toArray(): array
+    {
+        return [
+            'description' => $this->description,
+            'type' => 'boolean',
+        ];
+    }
+}
diff --git a/src/Schema/EnumSchema.php b/src/Schema/EnumSchema.php
new file mode 100644
index 0000000..0a5984d
--- /dev/null
+++ b/src/Schema/EnumSchema.php
@@ -0,0 +1,34 @@
+<?php
+
+declare(strict_types=1);
+
+namespace EchoLabs\Prism\Schema;
+
+use EchoLabs\Prism\Contracts\Schema;
+
+class EnumSchema implements Schema
+{
+    /**
+     * @param  array<int, string|int|float>  $options
+     */
+    public function __construct(
+        public readonly string $name,
+        public readonly string $description,
+        public readonly array $options,
+    ) {}
+
+    #[\Override]
+    public function name(): string
+    {
+        return $this->name;
+    }
+
+    #[\Override]
+    public function toArray(): array
+    {
+        return [
+            'description' => $this->description,
+            'enum' => $this->options,
+        ];
+    }
+}
diff --git a/src/Schema/NumberSchema.php b/src/Schema/NumberSchema.php
new file mode 100644
index 0000000..3477fef
--- /dev/null
+++ b/src/Schema/NumberSchema.php
@@ -0,0 +1,30 @@
+<?php
+
+declare(strict_types=1);
+
+namespace EchoLabs\Prism\Schema;
+
+use EchoLabs\Prism\Contracts\Schema;
+
+class NumberSchema implements Schema
+{
+    public function __construct(
+        public readonly string $name,
+        public readonly string $description,
+    ) {}
+
+    #[\Override]
+    public function name(): string
+    {
+        return $this->name;
+    }
+
+    #[\Override]
+    public function toArray(): array
+    {
+        return [
+            'description' => $this->description,
+            'type' => 'number',
+        ];
+    }
+}
diff --git a/src/Schema/ObjectSchema.php b/src/Schema/ObjectSchema.php
new file mode 100644
index 0000000..901c152
--- /dev/null
+++ b/src/Schema/ObjectSchema.php
@@ -0,0 +1,51 @@
+<?php
+
+declare(strict_types=1);
+
+namespace EchoLabs\Prism\Schema;
+
+use EchoLabs\Prism\Contracts\Schema;
+
+class ObjectSchema implements Schema
+{
+    /**
+     * @param  array<int, Schema>  $properties
+     * @param  array<int, string>  $requiredFields
+     */
+    public function __construct(
+        public readonly string $name,
+        public readonly string $description,
+        public readonly array $properties,
+        public readonly array $requiredFields = [],
+        public readonly bool $allowAdditionalProperties = false,
+    ) {}
+
+    #[\Override]
+    public function name(): string
+    {
+        return $this->name;
+    }
+
+    #[\Override]
+    public function toArray(): array
+    {
+        return [
+            'description' => $this->description,
+            'type' => 'object',
+            'properties' => $this->propertiesArray(),
+            'required' => $this->requiredFields,
+            'additionalProperties' => $this->allowAdditionalProperties,
+        ];
+    }
+
+    /**
+     * @return array<string, mixed>
+     */
+    protected function propertiesArray(): array
+    {
+        return collect($this->properties)
+            ->keyBy(fn (Schema $parameter): string => $parameter->name())
+            ->map(fn (Schema $parameter): array => $parameter->toArray())
+            ->toArray();
+    }
+}
diff --git a/src/Schema/StringSchema.php b/src/Schema/StringSchema.php
new file mode 100644
index 0000000..74ac59d
--- /dev/null
+++ b/src/Schema/StringSchema.php
@@ -0,0 +1,30 @@
+<?php
+
+declare(strict_types=1);
+
+namespace EchoLabs\Prism\Schema;
+
+use EchoLabs\Prism\Contracts\Schema;
+
+class StringSchema implements Schema
+{
+    public function __construct(
+        public readonly string $name,
+        public readonly string $description,
+    ) {}
+
+    #[\Override]
+    public function name(): string
+    {
+        return $this->name;
+    }
+
+    #[\Override]
+    public function toArray(): array
+    {
+        return [
+            'description' => $this->description,
+            'type' => 'string',
+        ];
+    }
+}
diff --git a/src/Tool.php b/src/Tool.php
index 32b42bc..976a867 100644
--- a/src/Tool.php
+++ b/src/Tool.php
@@ -5,15 +5,25 @@ declare(strict_types=1);
 namespace EchoLabs\Prism;
 
 use Closure;
+use EchoLabs\Prism\Contracts\Schema;
+use EchoLabs\Prism\Schema\ArraySchema;
+use EchoLabs\Prism\Schema\BooleanSchema;
+use EchoLabs\Prism\Schema\EnumSchema;
+use EchoLabs\Prism\Schema\NumberSchema;
+use EchoLabs\Prism\Schema\ObjectSchema;
+use EchoLabs\Prism\Schema\StringSchema;
 
 class Tool
 {
-    protected string $name;
+    protected string $name = '';
 
-    protected string $description;
+    protected string $description = '';
 
-    /** @var array<int, array<string, string|bool>> */
-    protected array $parameters;
+    /** @var array<string, array<string, mixed>> */
+    protected array $parameters = [];
+
+    /** @var array <int, string> */
+    protected array $requiredParameters = [];
 
     /** @var Closure():string|callable():string */
     protected $fn;
@@ -40,14 +50,83 @@ class Tool
         return $this;
     }
 
-    public function withParameter(string $name, string $description, string $type = 'string', bool $required = true): self
+    public function withParameter(Schema $parameter, bool $required = true): self
+    {
+        $this->parameters[$parameter->name()] = $parameter->toArray();
+
+        if ($required) {
+            $this->requiredParameters[] = $parameter->name();
+        }
+
+        return $this;
+    }
+
+    public function withStringParameter(string $name, string $description, bool $required = true): self
     {
-        $this->parameters[] = [
-            'name' => $name,
-            'description' => $description,
-            'type' => $type,
-            'required' => $required,
-        ];
+        $this->withParameter(new StringSchema($name, $description), $required);
+
+        return $this;
+    }
+
+    public function withNumberParameter(string $name, string $description, bool $required = true): self
+    {
+        $this->withParameter(new NumberSchema($name, $description), $required);
+
+        return $this;
+    }
+
+    public function withBooleanParameter(string $name, string $description, bool $required = true): self
+    {
+        $this->withParameter(new BooleanSchema($name, $description), $required);
+
+        return $this;
+    }
+
+    public function withArrayParameter(
+        string $name,
+        string $description,
+        Schema $items,
+        bool $required = true,
+    ): self {
+        $this->withParameter(new ArraySchema($name, $description, $items), $required);
+
+        return $this;
+    }
+
+    /**
+     * @param  array<int, Schema>  $properties
+     * @param  array<int, string>  $requiredFields
+     */
+    public function withObjectParameter(
+        string $name,
+        string $description,
+        array $properties,
+        array $requiredFields = [],
+        bool $allowAdditionalProperties = false,
+        bool $required = true,
+    ): self {
+
+        $this->withParameter(new ObjectSchema(
+            $name,
+            $description,
+            $properties,
+            $requiredFields,
+            $allowAdditionalProperties,
+        ), $required);
+
+        return $this;
+    }
+
+    /**
+     * @param  array<int, string|int|float>  $options
+     */
+    public function withEnumParameter(
+        string $name,
+        string $description,
+        array $options,
+        bool $required = true,
+    ): self {
+        $this->withParameter(new EnumSchema($name, $description, $options), $required);
 
         return $this;
     }
@@ -55,14 +134,12 @@ class Tool
     /** @return array<int, string> */
     public function requiredParameters(): array
     {
-        return collect($this->parameters)
-            ->filter(fn (array $params): bool => (bool) $params['required'])
-            ->keyBy('name')
-            ->keys()
-            ->all();
+        return $this->requiredParameters;
     }
 
-    /** @return array<int, array<string, string|bool>> */
+    /**
+     * @return array<string, array<string, mixed>>
+     */
     public function parameters(): array
     {
         return $this->parameters;
@@ -79,7 +156,7 @@ class Tool
     }
 
     /** @param string|int|float $args */
-    public function handle(...$args): string
+    public function handle(...$args): ?string
     {
         return call_user_func($this->fn, ...$args);
     }
diff --git a/src/ValueObjects/ToolResult.php b/src/ValueObjects/ToolResult.php
index 59c3996..f15e4af 100644
--- a/src/ValueObjects/ToolResult.php
+++ b/src/ValueObjects/ToolResult.php
@@ -4,16 +4,6 @@ declare(strict_types=1);
 
 namespace EchoLabs\Prism\ValueObjects;
 
-// {
-//   toolCallId: 'call_fnT5I1VQvJGH2AGLxG5lPKOH',
-//   toolName: 'search',
-//   args: { query: 'Detroit Tigers game time today' },
-//   result: {
-//     query: 'Detroit Tigers game time today',
-//     result: 'The tigers game is at 3pm today in Detroit'
-//   }
-// },
-
 class ToolResult
 {
     /**
@@ -24,6 +14,6 @@ class ToolResult
         public readonly string $toolCallId,
         public readonly string $toolName,
         public readonly array $args,
-        public readonly int|float|string|array $result,
+        public readonly int|float|string|array|null $result,
     ) {}
 }


Test patch:
diff --git a/tests/Generators/TextGeneratorTest.php b/tests/Generators/TextGeneratorTest.php
index 71bb908..f30c123 100644
--- a/tests/Generators/TextGeneratorTest.php
+++ b/tests/Generators/TextGeneratorTest.php
@@ -151,7 +151,7 @@ it('correctly generates a request with tools', function (): void {
 
     $tool = Tool::as('weather')
         ->for('useful when you need to search for current weather conditions')
-        ->withparameter('city', 'the city that you want the weather for')
+        ->withStringParameter('city', 'the city that you want the weather for')
         ->using(fn (string $city): string => 'the weather will be 75° and sunny');
 
     (new TextGenerator)
@@ -259,7 +259,7 @@ it('generates a response from the driver with tools and max steps', function ():
 
     $tool = Tool::as('weather')
         ->for('useful when you need to search for current weather conditions')
-        ->withparameter('city', 'the city that you want the weather for')
+        ->withStringParameter('city', 'the city that you want the weather for')
         ->using(fn (string $city): string => 'the weather will be 75° and sunny');
 
     $response = (new TextGenerator)
@@ -333,7 +333,7 @@ it('correctly stops using max steps', function (): void {
 
     $tool = Tool::as('weather')
         ->for('useful when you need to search for current weather conditions')
-        ->withparameter('city', 'the city that you want the weather for')
+        ->withStringParameter('city', 'the city that you want the weather for')
         ->using(fn (string $city): string => 'the weather will be 75° and sunny');
 
     $response = (new TextGenerator)
diff --git a/tests/Providers/AnthropicTextTest.php b/tests/Providers/AnthropicTextTest.php
index 8cd7dda..ca348ca 100644
--- a/tests/Providers/AnthropicTextTest.php
+++ b/tests/Providers/AnthropicTextTest.php
@@ -51,11 +51,11 @@ it('can generate text using multiple tools and multiple steps', function (): voi
     $tools = [
         Tool::as('weather')
             ->for('useful when you need to search for current weather conditions')
-            ->withParameter('city', 'The city that you want the weather for')
+            ->withStringParameter('city', 'the city you want the weather for')
             ->using(fn (string $city): string => 'The weather will be 75° and sunny'),
         Tool::as('search')
             ->for('useful for searching curret events or data')
-            ->withParameter('query', 'The detailed search query')
+            ->withStringParameter('query', 'The detailed search query')
             ->using(fn (string $query): string => 'The tigers game is at 3pm in detroit'),
     ];
 
diff --git a/tests/Providers/OllamaTextTest.php b/tests/Providers/OllamaTextTest.php
index 1848ff4..74bae4d 100644
--- a/tests/Providers/OllamaTextTest.php
+++ b/tests/Providers/OllamaTextTest.php
@@ -52,11 +52,11 @@ it('can generate text using multiple tools and multiple steps', function (): voi
     $tools = [
         Tool::as('weather')
             ->for('useful when you need to search for current weather conditions')
-            ->withParameter('city', 'The city that you want the weather for')
+            ->withStringParameter('city', 'The city that you want the weather for')
             ->using(fn (string $city): string => 'The weather will be 75° and sunny'),
         Tool::as('search')
             ->for('useful for searching curret events or data')
-            ->withParameter('query', 'The detailed search query')
+            ->withStringParameter('query', 'The detailed search query')
             ->using(fn (string $query): string => 'The tigers game is at 3pm in detroit'),
     ];
 
diff --git a/tests/Providers/OpenAITextTest.php b/tests/Providers/OpenAITextTest.php
index 46f41e2..f455d45 100644
--- a/tests/Providers/OpenAITextTest.php
+++ b/tests/Providers/OpenAITextTest.php
@@ -53,16 +53,16 @@ it('can generate text using multiple tools and multiple steps', function (): voi
     $tools = [
         Tool::as('weather')
             ->for('useful when you need to search for current weather conditions')
-            ->withParameter('city', 'The city that you want the weather for')
+            ->withStringParameter('city', 'The city that you want the weather for')
             ->using(fn (string $city): string => 'The weather will be 75° and sunny'),
         Tool::as('search')
             ->for('useful for searching curret events or data')
-            ->withParameter('query', 'The detailed search query')
+            ->withStringParameter('query', 'The detailed search query')
             ->using(fn (string $query): string => 'The tigers game is at 3pm in detroit'),
     ];
 
     $response = Prism::text()
-        ->using('openai', 'gpt-4')
+        ->using('openai', 'gpt-4o')
         ->withTools($tools)
         ->withMaxSteps(3)
         ->withPrompt('What time is the tigers game today and should I wear a coat?')();
diff --git a/tests/SchemaTest.php b/tests/SchemaTest.php
new file mode 100644
index 0000000..22361fb
--- /dev/null
+++ b/tests/SchemaTest.php
@@ -0,0 +1,104 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Tests;
+
+use EchoLabs\Prism\Schema\ArraySchema;
+use EchoLabs\Prism\Schema\EnumSchema;
+use EchoLabs\Prism\Schema\NumberSchema;
+use EchoLabs\Prism\Schema\ObjectSchema;
+use EchoLabs\Prism\Schema\StringSchema;
+
+it('they can have nested properties', function (): void {
+    $schema = new ObjectSchema(
+        name: 'user',
+        description: 'a user object',
+        properties: [
+            new StringSchema('name', 'the users name'),
+            new NumberSchema('age', 'the users age', false),
+            new EnumSchema(
+                name: 'status',
+                description: 'the users status',
+                options: [
+                    'active',
+                    'inactive',
+                    'suspended',
+                ]
+            ),
+            new ArraySchema(
+                name: 'hobbies',
+                description: 'the users hobbies',
+                item: new StringSchema('hobby', 'the users hobby')
+            ),
+            new ObjectSchema(
+                name: 'address',
+                description: 'the users address',
+                properties: [
+                    new StringSchema('street', 'the street part of the address'),
+                    new StringSchema('city', 'the city part of the address'),
+                    new StringSchema('country', 'the country part of the address'),
+                    new NumberSchema('zip', 'the zip code part of the address'),
+                ],
+                requiredFields: ['street', 'city', 'country', 'zip']
+            ),
+        ]
+    );
+
+    expect($schema->toArray())->toBe([
+        'description' => 'a user object',
+        'type' => 'object',
+        'properties' => [
+            'name' => [
+                'description' => 'the users name',
+                'type' => 'string',
+            ],
+            'age' => [
+                'description' => 'the users age',
+                'type' => 'number',
+            ],
+            'status' => [
+                'description' => 'the users status',
+                'enum' => [
+                    'active',
+                    'inactive',
+                    'suspended',
+                ],
+            ],
+            'hobbies' => [
+                'description' => 'the users hobbies',
+                'type' => 'array',
+                'items' => [
+                    'description' => 'the users hobby',
+                    'type' => 'string',
+                ],
+            ],
+            'address' => [
+                'description' => 'the users address',
+                'type' => 'object',
+                'properties' => [
+                    'street' => [
+                        'description' => 'the street part of the address',
+                        'type' => 'string',
+                    ],
+                    'city' => [
+                        'description' => 'the city part of the address',
+                        'type' => 'string',
+                    ],
+                    'country' => [
+                        'description' => 'the country part of the address',
+                        'type' => 'string',
+                    ],
+                    'zip' => [
+                        'description' => 'the zip code part of the address',
+                        'type' => 'number',
+                    ],
+                ],
+                'required' => ['street', 'city', 'country', 'zip'],
+                'additionalProperties' => false,
+            ],
+        ],
+        'required' => [],
+        'additionalProperties' => false,
+    ]);
+});
diff --git a/tests/ToolTest.php b/tests/ToolTest.php
index 9508f41..26ef865 100644
--- a/tests/ToolTest.php
+++ b/tests/ToolTest.php
@@ -5,13 +5,15 @@ declare(strict_types=1);
 namespace Tests;
 
 use EchoLabs\Prism\Facades\Tool as ToolFacade;
+use EchoLabs\Prism\Schema\BooleanSchema;
+use EchoLabs\Prism\Schema\StringSchema;
 use EchoLabs\Prism\Tool;
 
 it('can return tool details', function (): void {
     $searchTool = (new Tool)
         ->as('search')
         ->for('useful for searching current data')
-        ->withParameter('query', 'the search query')
+        ->withParameter(new StringSchema('query', 'the search query'))
         ->using(function (string $query): string {
             expect($query)->toBe('What time is the event?');
 
@@ -21,11 +23,9 @@ it('can return tool details', function (): void {
     expect($searchTool->name())->toBe('search');
     expect($searchTool->description())->toBe('useful for searching current data');
     expect($searchTool->parameters())->toBe([
-        [
-            'name' => 'query',
+        'query' => [
             'description' => 'the search query',
             'type' => 'string',
-            'required' => true,
         ],
     ]);
 
@@ -36,7 +36,7 @@ it('can use a closure', function (): void {
     $searchTool = (new Tool)
         ->as('search')
         ->for('useful for searching current data')
-        ->withParameter('query', 'the search query')
+        ->withParameter(new StringSchema('query', 'the search query'))
         ->using(function (string $query): string {
             expect($query)->toBe('What time is the event?');
 
@@ -51,7 +51,7 @@ it('can use a closure', function (): void {
 it('can be used via facade', function (): void {
     $searchTool = ToolFacade::as('search')
         ->for('useful for searching current data')
-        ->withParameter('query', 'the search query')
+        ->withParameter(new StringSchema('query', 'the search query'))
         ->using(function (string $query): string {
             expect($query)->toBe('What time is the event?');
 
@@ -77,9 +77,52 @@ it('can use an invokeable', function (): void {
     $searchTool = (new Tool)
         ->as('search')
         ->for('useful for searching current data')
-        ->withParameter('query', 'the search query')
+        ->withParameter(new StringSchema('query', 'the search query'))
         ->using($fn);
 
     expect($searchTool->handle('What time is the event?'))
         ->toBe('The event is at 3pm eastern');
 });
+
+it('can have fluent parameters', function (): void {
+    $tool = (new Tool)
+        ->as('test tool')
+        ->for('not really useful for anything')
+        ->withStringParameter(name: 'query', description: 'the search query', required: false)
+        ->withNumberParameter('age', 'the users age')
+        ->withBooleanParameter('active', 'active status')
+        ->withArrayParameter(
+            name: 'items',
+            description: 'user requested items',
+            items: new StringSchema('itemm', 'an item that the user requested'),
+        )
+        ->withEnumParameter('status', 'the status', ['active', 'inactive'])
+        ->withObjectParameter(
+            name: 'user',
+            description: 'the user object',
+            properties: [
+                new StringSchema('name', 'the users name'),
+                new BooleanSchema('active_status', 'user active status'),
+            ],
+            requiredFields: [
+                'name',
+            ]
+        );
+
+    $keys = [
+        'query',
+        'age',
+        'active',
+        'items',
+        'status',
+        'user',
+    ];
+
+    expect($tool->parameters())->toHaveKeys($keys);
+
+    collect($keys)->each(function ($key) use ($tool): void {
+        expect($tool->parameters()[$key])->not->toBeEmpty();
+    });
+
+    expect($tool->requiredParameters())->not->toContain('query');
+});