Skip to content

Commit

Permalink
Better XML markup for templating chat message sequences
Browse files Browse the repository at this point in the history
  • Loading branch information
ddebowczyk committed Nov 12, 2024
1 parent 718e5b0 commit 3d76886
Show file tree
Hide file tree
Showing 23 changed files with 1,478 additions and 837 deletions.
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
"php": "^8.2",
"ext-fileinfo": "*",
"ext-simplexml": "*",
"ext-xmlreader": "*",
"adbario/php-dot-notation": "^3.3",
"aimeos/map": "^3.8",
"guzzlehttp/guzzle": "^7.8",
Expand All @@ -89,7 +90,7 @@
"symfony/serializer": "^6.4 || ^7.0",
"symfony/type-info": "^7.1",
"symfony/validator": "^6.4 || ^7.0",
"vlucas/phpdotenv": "^5.6"
"vlucas/phpdotenv": "^5.6",
},
"scripts": {
"tests": "@php vendor/bin/pest",
Expand Down
44 changes: 39 additions & 5 deletions docs/advanced/prompts.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,8 @@ for LLM chat APIs.

```twig
<chat>
<system>You are a helpful assistant.</system>
<user>What is the capital of {{ country }}?</user>
<message role="system">You are a helpful assistant.</message>
<message role="user">What is the capital of {{ country }}?</message>
</chat>
```

Expand All @@ -76,8 +76,8 @@ schema:
required: [name]
---#}
<chat>
<system>You are a helpful assistant.</system>
<user>What is the capital of {{ country }}?</user>
<message role="system">You are a helpful assistant.</message>
<message role="user">What is the capital of {{ country }}?</message>
</chat>
```

Expand Down Expand Up @@ -176,6 +176,30 @@ echo $prompt->toText(); // Outputs: "Hello, World!"
?>
```

### In Memory Prompts

If you need to create an inline prompt (without saving it to a library), you can use following syntax:

```php
<?php
$prompt = Prompt::twig() // or Prompt::blade() for Blade syntax
->withTemplateContent('Hello, {{ name }}!')
->withValues(['name' => 'World'])
->toText();
?>
```

There's shorter syntax for creating in-memory prompts:

```php
<?php
$prompt = Prompt::twig() // or Prompt::blade() for Blade syntax
->from('Hello, {{ name }}!')
->with(['name' => 'World'])
->toText();
?>
```

### Handling Template Variables

To check which variables are available in a prompt template:
Expand Down Expand Up @@ -212,12 +236,22 @@ echo $prompt->toText(); // Outputs: "Hello, World!"

The Prompt class also supports converting templates containing chat-specific markup into structured messages:

Here is an example XML that can be used to generate a sequence of chat messages:
```xml
<chat>
<message role="system">You are a helpful assistant.</message>
<message role="user">Hello, {{ name }}</message>
</chat>
```

And here is how you use `Prompt` class to convert XML template into a sequence of messages:

```php
<?php
use Cognesy\Instructor\Utils\Messages\Messages;

$prompt = Prompt::using('demo-blade')
->withTemplateContent('<system>You are a helpful assistant.</system><user>Hello, {{ $name }}</user>')
->withTemplateContent('<chat><message role="system">You are a helpful assistant.</message><message role="user">Hello, {{ $name }}</message></chat>')
->withValues(['name' => 'assistant']);

$messages = $prompt->toMessages();
Expand Down
Empty file added prompts/system/mode_json.twig
Empty file.
Empty file.
Empty file added prompts/system/mode_mdjson.twig
Empty file.
Empty file added prompts/system/mode_text.twig
Empty file.
1 change: 1 addition & 0 deletions prompts/system/mode_tools.twig
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

135 changes: 99 additions & 36 deletions src/Extras/Prompt/Prompt.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@
use Cognesy\Instructor\Extras\Prompt\Data\PromptEngineConfig;
use Cognesy\Instructor\Utils\Messages\Message;
use Cognesy\Instructor\Utils\Messages\Messages;
use Cognesy\Instructor\Utils\Messages\Script;
use Cognesy\Instructor\Utils\Str;
use Cognesy\Instructor\Utils\Xml;
use Cognesy\Instructor\Utils\Xml\Xml;
use Cognesy\Instructor\Utils\Xml\XmlElement;
use InvalidArgumentException;

class Prompt
Expand All @@ -20,6 +22,7 @@ class Prompt
private string $templateContent;
private array $variableValues;
private string $rendered;
private $tags = ['chat', 'message', 'content', 'section'];

public function __construct(
string $path = '',
Expand Down Expand Up @@ -121,6 +124,10 @@ public function toMessages() : Messages {
return $this->makeMessages($this->rendered());
}

public function toScript() : Script {
return $this->makeScript($this->rendered());
}

public function toArray() : array {
return $this->toMessages()->toArray();
}
Expand Down Expand Up @@ -149,33 +156,7 @@ public function validationErrors() : array {
$infoVars = $this->info()->variableNames();
$templateVars = $this->variables();
$valueKeys = array_keys($this->variableValues);

$messages = [];
foreach($infoVars as $var) {
if (!in_array($var, $valueKeys)) {
$messages[] = "$var: variable defined in template info, but value not provided";
}
if (!in_array($var, $templateVars)) {
$messages[] = "$var: variable defined in template info, but not used";
}
}
foreach($valueKeys as $var) {
if (!in_array($var, $infoVars)) {
$messages[] = "$var: value provided, but not defined in template info";
}
if (!in_array($var, $templateVars)) {
$messages[] = "$var: value provided, but not used in template content";
}
}
foreach($templateVars as $var) {
if (!in_array($var, $infoVars)) {
$messages[] = "$var: variable used in template, but not defined in template info";
}
if (!in_array($var, $valueKeys)) {
$messages[] = "$var: variable used in template, but value not provided";
}
}
return $messages;
return $this->validateVariables($infoVars, $templateVars, $valueKeys);
}

// INTERNAL ///////////////////////////////////////////////////
Expand All @@ -195,9 +176,16 @@ private function makeMessages(string $text) : Messages {
};
}

private function makeScript(string $text) : Script {
return match(true) {
$this->containsXml($text) && $this->hasChatRoles($text) => $this->makeScriptFromXml($text),
default => Messages::fromString($text),
};
}

private function hasChatRoles(string $text) : bool {
$roleStrings = [
'<chat>', '<user>', '<assistant>', '<system>', '<section>', '<message>'
'<chat>', '<message>', '<section>'
];
if (Str::containsAny($text, $roleStrings)) {
return true;
Expand All @@ -209,15 +197,90 @@ private function containsXml(string $text) : bool {
return preg_match('/<[^>]+>/', $text) === 1;
}

private function makeScriptFromXml(string $text) : Script {
$xml = Xml::from($text)->withTags($this->tags)->toXmlElement();
$script = new Script();
$section = $script->section('messages');
foreach ($xml->children() as $element) {
if ($element->tag() === 'section') {
$section = $script->section($element->attribute('name') ?? 'messages');
continue;
}
if ($element->tag() !== 'message') {
continue;
}
$section->appendMessage(Message::make(
role: $element->attribute('role', 'user'),
content: match(true) {
$element->hasChildren() => $this->getMessageContent($element),
default => $element->content(),
}
));
}
return $script;
}

private function makeMessagesFromXml(string $text) : Messages {
$xml = Xml::from($text)->withTags($this->tags)->toXmlElement();
$messages = new Messages();
$xml = match(Str::contains($text, '<chat>')) {
true => Xml::from($text)->toArray(),
default => Xml::from($text)->wrapped('chat')->toArray(),
};
// TODO: validate
foreach ($xml as $key => $message) {
$messages->appendMessage(Message::make($key, $message));
foreach ($xml->children() as $element) {
if ($element->tag() !== 'message') {
continue;
}
$messages->appendMessage(Message::make(
role: $element->attribute('role', 'user'),
content: match(true) {
$element->hasChildren() => $this->getMessageContent($element),
default => $element->content(),
}
));
}
return $messages;
}

private function getMessageContent(XmlElement $element) : array {
$content = [];
foreach ($element->children() as $child) {
if ($child->tag() !== 'content') {
continue;
}
// check if content type is text, image or audio
$type = $child->attribute('type', 'text');
$content[] = match($type) {
'image' => ['type' => 'image_url', 'image_url' => ['url' => $child->content()]],
'audio' => ['type' => 'input_audio', 'input_audio' => ['data' => $child->content(), 'format' => $child->attribute('format', 'mp3')]],
'text' => ['type' => 'text', 'text' => $child->content()],
default => throw new InvalidArgumentException("Invalid content type: $type"),
};
}
return $content;
}

private function validateVariables(array $infoVars, array $templateVars, array $valueKeys) : array {
$messages = [];
foreach($infoVars as $var) {
if (!in_array($var, $valueKeys)) {
$messages[] = "$var: variable defined in template info, but value not provided";
}
if (!in_array($var, $templateVars)) {
$messages[] = "$var: variable defined in template info, but not used";
}
}
foreach($valueKeys as $var) {
if (!in_array($var, $infoVars)) {
$messages[] = "$var: value provided, but not defined in template info";
}
if (!in_array($var, $templateVars)) {
$messages[] = "$var: value provided, but not used in template content";
}
}
foreach($templateVars as $var) {
if (!in_array($var, $infoVars)) {
$messages[] = "$var: variable used in template, but not defined in template info";
}
if (!in_array($var, $valueKeys)) {
$messages[] = "$var: variable used in template, but value not provided";
}
}
return $messages;
}
Expand Down
Loading

0 comments on commit 3d76886

Please sign in to comment.