-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Added: StructuredOutputParser which takes "plain php class" converts …
…it into a json schema, and marshals the JSON response back into an instance of that class. Moved OutputParser tests into its own folder, added new method on LLM interace that takes a prompttemplate instead of a string.
- Loading branch information
1 parent
c0043f0
commit 941c51f
Showing
12 changed files
with
369 additions
and
65 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
<?php | ||
|
||
namespace Mindwave\Mindwave\Prompts\OutputParsers; | ||
|
||
use Illuminate\Support\Collection; | ||
use Mindwave\Mindwave\Contracts\OutputParser; | ||
use ReflectionClass; | ||
|
||
class StructuredOutputParser implements OutputParser | ||
{ | ||
protected $schema; | ||
|
||
public function __construct($schema = null) | ||
{ | ||
$this->schema = $schema; | ||
} | ||
|
||
public function fromClass($schema): self | ||
{ | ||
$this->schema = $schema; | ||
|
||
return $this; | ||
} | ||
|
||
public function getSchemaStructure(): array | ||
{ | ||
$reflectionClass = new ReflectionClass($this->schema); | ||
$properties = []; | ||
$required = []; | ||
|
||
foreach ($reflectionClass->getProperties() as $property) { | ||
$propertyName = $property->getName(); | ||
$propertyType = $property->getType()->getName(); | ||
|
||
if ($property->getType()->allowsNull() === false) { | ||
$required[] = $propertyName; | ||
} | ||
|
||
$properties[$propertyName] = [ | ||
'type' => match ($propertyType) { | ||
'string', 'int', 'float', 'bool' => $propertyType, | ||
'array', Collection::class => 'array', | ||
default => 'object', | ||
}, | ||
]; | ||
} | ||
|
||
return [ | ||
'properties' => $properties, | ||
'required' => $required, | ||
]; | ||
} | ||
|
||
public function getFormatInstructions(): string | ||
{ | ||
$schema = json_encode($this->getSchemaStructure()); | ||
|
||
return trim(' | ||
RESPONSE FORMAT INSTRUCTIONS | ||
---------------------------- | ||
The output should be formatted as a JSON instance that conforms to the JSON schema below. | ||
As an example, for the schema {{"properties": {{"foo": {{"title": "Foo", "description": "a list of strings", "type": "array", "items": {{"type": "string"}}}}}}, "required": ["foo"]}}}} | ||
the object {{"foo": ["bar", "baz"]}} is a well-formatted instance of the schema. The object {{"properties": {{"foo": ["bar", "baz"]}}}} is not well-formatted. | ||
Here is the output schema: | ||
```json | ||
'.$schema.' | ||
``` | ||
Remember to respond with a JSON blob, and NOTHING else.'); | ||
} | ||
|
||
public function parse(string $text): mixed | ||
{ | ||
$reflectionClass = new ReflectionClass($this->schema); | ||
$data = json_decode($text, true); | ||
|
||
if (! $data) { | ||
// TODO(29 May 2023) ~ Helge: Throw custom exception | ||
return null; | ||
} | ||
|
||
$instance = new $this->schema(); | ||
|
||
foreach ($data as $key => $value) { | ||
|
||
$type = $reflectionClass->getProperty($key)->getType(); | ||
|
||
// TODO(29 May 2023) ~ Helge: There are probably libraries that do this in a more clever way, but this is fine for now. | ||
$instance->{$key} = match ($type->getName()) { | ||
'bool' => boolval($value), | ||
'int' => intval($value), | ||
'float' => floatval($value), | ||
Collection::class => collect($value), | ||
default => $value, | ||
}; | ||
} | ||
|
||
return $instance; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
<?php | ||
|
||
use Illuminate\Support\Collection; | ||
use Illuminate\Support\Facades\Config; | ||
use Mindwave\Mindwave\Facades\Mindwave; | ||
use Mindwave\Mindwave\Prompts\OutputParsers\StructuredOutputParser; | ||
use Mindwave\Mindwave\Prompts\PromptTemplate; | ||
|
||
it('can use a structured output parser', function () { | ||
Config::set('mindwave-vectorstore.default', 'array'); | ||
Config::set('mindwave-embeddings.embeddings.openai.api_key', env('MINDWAVE_OPENAI_API_KEY')); | ||
Config::set('mindwave-llm.llms.openai_chat.api_key', env('MINDWAVE_OPENAI_API_KEY')); | ||
|
||
class Person | ||
{ | ||
public string $name; | ||
|
||
public ?int $age; | ||
|
||
public ?bool $hasBusiness; | ||
|
||
public ?array $interests; | ||
|
||
public ?Collection $tags; | ||
} | ||
|
||
$model = Mindwave::llm(); | ||
$parser = new StructuredOutputParser(Person::class); | ||
|
||
$result = $model->run(PromptTemplate::create( | ||
'Generate random details about a fictional person', $parser | ||
)); | ||
|
||
expect($result)->toBeInstanceOf(Person::class); | ||
|
||
dump($result); | ||
}); | ||
|
||
it('We can parse a small recipe into an object', function () { | ||
Config::set('mindwave-vectorstore.default', 'array'); | ||
Config::set('mindwave-embeddings.embeddings.openai.api_key', env('MINDWAVE_OPENAI_API_KEY')); | ||
Config::set('mindwave-llm.llms.openai_chat.api_key', env('MINDWAVE_OPENAI_API_KEY')); | ||
Config::set('mindwave-llm.llms.openai_chat.max_tokens', 2500); | ||
Config::set('mindwave-llm.llms.openai_chat.temperature', 0.2); | ||
|
||
class Recipe | ||
{ | ||
public string $dishName; | ||
|
||
public ?string $description; | ||
|
||
public ?int $portions; | ||
|
||
public ?array $steps; | ||
} | ||
|
||
// Source: https://sugarspunrun.com/the-best-pizza-dough-recipe/ | ||
$rawRecipeText = file_get_contents(__DIR__.'/data/samples/pizza-recipe.txt'); | ||
|
||
$template = PromptTemplate::create( | ||
template: 'Extract details from this recipe: {recipe}', | ||
outputParser: new StructuredOutputParser(Recipe::class) | ||
); | ||
|
||
$result = Mindwave::llm()->run($template, [ | ||
'recipe' => $rawRecipeText, | ||
]); | ||
|
||
expect($result)->toBeInstanceOf(Recipe::class); | ||
|
||
dump($result); | ||
}); |
15 changes: 15 additions & 0 deletions
15
tests/Prompts/OutputParser/CommaSeparatedListOutputParserTest.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
<?php | ||
|
||
use Mindwave\Mindwave\Prompts\OutputParsers\CommaSeparatedListOutputParser; | ||
|
||
it('can parse comma separated output', function () { | ||
|
||
$parser = new CommaSeparatedListOutputParser(); | ||
|
||
expect($parser->parse('monsters, bananas, flies, sausages'))->toEqual([ | ||
'monsters', | ||
'bananas', | ||
'flies', | ||
'sausages', | ||
]); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
<?php | ||
|
||
use Mindwave\Mindwave\Prompts\OutputParsers\JsonListOutputParser; | ||
use Mindwave\Mindwave\Prompts\PromptTemplate; | ||
|
||
it('json list output parser generates a list from constructor', function () { | ||
$outputParser = new JsonListOutputParser(); | ||
$prompt = PromptTemplate::create( | ||
template: 'Generate 10 keywords for {topic}', | ||
outputParser: $outputParser | ||
)->format([ | ||
'topic' => 'Mindwave', | ||
]); | ||
|
||
expect($prompt)->toContain('Generate 10 keywords for Mindwave'); | ||
expect($prompt)->toContain($outputParser->getFormatInstructions()); | ||
}); | ||
|
||
it('json list output parser generates a list from method', function () { | ||
$outputParser = new JsonListOutputParser(); | ||
|
||
$prompt = PromptTemplate::create( | ||
template: 'Generate 10 keywords for {topic}', | ||
)->withOutputParser($outputParser)->format([ | ||
'topic' => 'Laravel', | ||
]); | ||
|
||
expect($prompt)->toContain('Generate 10 keywords for Laravel'); | ||
expect($prompt)->toContain($outputParser->getFormatInstructions()); | ||
}); | ||
|
||
it('can parse json array as array', function () { | ||
|
||
$parser = new JsonListOutputParser(); | ||
|
||
expect($parser->parse('```json{"data": ["monsters", "bananas", "flies", "sausages"]}```'))->toEqual([ | ||
'monsters', | ||
'bananas', | ||
'flies', | ||
'sausages', | ||
]); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
<?php | ||
|
||
use Mindwave\Mindwave\Prompts\OutputParsers\JsonOutputParser; | ||
use Mindwave\Mindwave\Prompts\PromptTemplate; | ||
|
||
it('can parse a response', function () { | ||
$prompt = PromptTemplate::create('Test prompt', new JsonOutputParser()) | ||
->parse('```json { "hello": "world", "nice":["mindwave", "package"] } ```'); | ||
|
||
expect($prompt) | ||
->toBeArray() | ||
->and($prompt) | ||
->toHaveKey('hello', 'world') | ||
->toHaveKey('nice', ['mindwave', 'package']); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
<?php | ||
|
||
use Illuminate\Support\Collection; | ||
use Mindwave\Mindwave\Prompts\OutputParsers\StructuredOutputParser; | ||
|
||
class Person | ||
{ | ||
public string $name; | ||
|
||
public ?int $age; | ||
|
||
public ?bool $hasBusiness; | ||
|
||
public ?array $interests; | ||
|
||
public ?Collection $tags; | ||
} | ||
|
||
it('can convert a class into a schema for StructuredOutputParser', function () { | ||
$parser = new StructuredOutputParser(Person::class); | ||
|
||
expect($parser->getSchemaStructure()) | ||
->toBe([ | ||
'properties' => [ | ||
'name' => ['type' => 'string'], | ||
'age' => ['type' => 'int'], | ||
'hasBusiness' => ['type' => 'bool'], | ||
'interests' => ['type' => 'array'], | ||
'tags' => ['type' => 'array'], | ||
], | ||
'required' => ['name'], | ||
]); | ||
}); | ||
|
||
it('can parse response into class instance', function () { | ||
$parser = new StructuredOutputParser(Person::class); | ||
|
||
/** @var Person $person */ | ||
$person = $parser->parse('{"name": "Lila Jones", "age": 28, "hasBusiness": true, "interests": ["hiking", "reading", "painting"], "tags": ["adventurous", "creative", "entrepreneur"]}'); | ||
|
||
expect($person)->toBeInstanceOf(Person::class); | ||
expect($person->name)->toBe('Lila Jones'); | ||
expect($person->age)->toBe(28); | ||
expect($person->hasBusiness)->toBe(true); | ||
expect($person->interests)->toBe(['hiking', 'reading', 'painting']); | ||
expect($person->tags)->toEqual(collect(['adventurous', 'creative', 'entrepreneur'])); | ||
}); | ||
|
||
it('can returns null if parsing data fails.', function () { | ||
$parser = new StructuredOutputParser(Person::class); | ||
|
||
$person = $parser->parse('broken and invalid data'); | ||
|
||
expect($person)->toBeNull(Person::class); | ||
}); |
Oops, something went wrong.