Skip to content

Commit 2357bbd

Browse files
committed
Added new example of language program
1 parent fe6432d commit 2357bbd

File tree

4 files changed

+204
-29
lines changed

4 files changed

+204
-29
lines changed

README.md

+6
Original file line numberDiff line numberDiff line change
@@ -791,6 +791,12 @@ If you want to help, check out some of the issues. All contributions are welcome
791791
This project is licensed under the terms of the MIT License.
792792

793793

794+
## Support
795+
796+
If you have any questions or need help, please reach out to me on [Twitter](https://twitter.com/ddebowczyk) or [GitHub](https://github.com/cognesy/instructor-php/issues).
797+
798+
799+
794800
## Contributors
795801

796802
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->

examples/02_Advanced/LanguagePrograms/run.php

+29-29
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,8 @@
3838
class EmailAnalysis extends SignatureData {
3939
#[InputField('content of email')]
4040
public string $text;
41-
4241
#[OutputField('identify most relevant email topic: sales, support, other, spam')]
4342
public string $topic;
44-
4543
#[OutputField('one word sentiment: positive, neutral, negative')]
4644
public string $sentiment;
4745

@@ -62,19 +60,14 @@ public function __construct(
6260
class EmailStats extends SignatureData {
6361
#[InputField('directory containing emails')]
6462
public string $directory;
65-
6663
#[OutputField('number of emails')]
6764
public int $emails;
68-
6965
#[OutputField('number of spam emails')]
7066
public int $spam;
71-
7267
#[OutputField('average sentiment ratio')]
7368
public float $sentimentRatio;
74-
7569
#[OutputField('spam ratio')]
7670
public float $spamRatio;
77-
7871
#[OutputField('category counts')]
7972
public CategoryCount $categories;
8073

@@ -83,56 +76,56 @@ static public function for(string $directory) : static {
8376
}
8477
}
8578

79+
// MODULE DECLARATIONS ////////////////////////////////////////////////////////////////////
80+
8681
class ReadEmails extends Module {
8782
public function __construct(
8883
private array $directoryContents = []
8984
) {}
9085

9186
public function signature() : string|Signature {
92-
return 'directory -> emails';
87+
return 'directory -> emails : string[]';
9388
}
9489

95-
public function forward(string $directory) : array {
90+
protected function forward(string $directory) : array {
9691
return $this->directoryContents[$directory];
9792
}
9893
}
9994

10095
class ParseEmail extends Module {
10196
public function signature() : string|Signature {
102-
return 'email -> sender, body';
97+
return 'email -> sender, subject, body';
10398
}
10499

105100
protected function forward(string $email) : array {
106101
$parts = explode(',', $email);
107102
return [
108103
'sender' => trim(explode(':', $parts[0])[1]),
109-
'body' => trim(explode(':', $parts[1])[1]),
104+
'subject' => trim(explode(':', $parts[1])[1]),
105+
'body' => trim(explode(':', $parts[2])[1]),
110106
];
111107
}
112108
}
113109

114110
class GetStats extends Module {
115-
private ReadEmails $readEmails;
116-
private ParseEmail $parseEmail;
117-
private Predict $analyseEmail;
118-
119-
public function __construct(Instructor $instructor, array $directoryContents = []) {
120-
$this->readEmails = new ReadEmails($directoryContents);
121-
$this->parseEmail = new ParseEmail();
122-
$this->analyseEmail = new Predict(signature: EmailAnalysis::class, instructor: $instructor);
123-
}
111+
public function __construct(
112+
private ReadEmails $readEmails,
113+
private ParseEmail $parseEmail,
114+
private Predict $analyseEmail,
115+
) {}
124116

125117
public function signature() : string|Signature {
126118
return EmailStats::class;
127119
}
128120

129-
public function forward(string $directory) : EmailStats {
121+
protected function forward(string $directory) : EmailStats {
130122
$emails = $this->readEmails->withArgs(directory: $directory)->get('emails');
131123
$aggregateSentiment = 0;
132124
$categories = new CategoryCount;
133125
foreach ($emails as $email) {
134126
$parsedEmail = $this->parseEmail->withArgs(email: $email);
135-
$emailAnalysis = $this->analyseEmail->with(EmailAnalysis::for($parsedEmail->get('body')));
127+
$emailData = EmailAnalysis::for(text: $parsedEmail->get('body'));
128+
$emailAnalysis = $this->analyseEmail->with($emailData);
136129
$topic = $emailAnalysis->get('topic');
137130
$sentiment = $emailAnalysis->get('sentiment');
138131
$topic = (in_array($topic, ['sales', 'support', 'spam'])) ? $topic : 'other';
@@ -160,19 +153,26 @@ public function forward(string $directory) : EmailStats {
160153
}
161154

162155
$directoryContents['inbox'] = [
163-
'sender: [email protected], body: I am happy about the discount you offered and accept contract renewal',
164-
'sender: xxx, body: Get Viagra and Ozempic for free',
165-
'sender: [email protected], body: My internet connection keeps failing',
166-
'sender: [email protected], body: How long do I have to wait for the pricing of custom support service?!?',
167-
'sender: [email protected], body: 2 weeks of waiting and still no improvement of my connection',
156+
'sender: [email protected], subject: Offer, body: I am happy about the discount you offered and accept contract renewal',
157+
'sender: xxx, subject: Free!!!, body: Get Ozempic for free',
158+
'sender: [email protected], subject: Problem, body: My internet connection keeps failing',
159+
'sender: [email protected], subject: Still no pricing, body: How long do I have to wait for the pricing of custom support service?!?',
160+
'sender: [email protected], subject: Slow connection, body: 2 weeks of waiting and still no improvement of my connection',
168161
];
169162

163+
// PREPARE DEPENDENCIES
164+
170165
$instructor = (new Instructor);
171-
$getStats = new GetStats($instructor, $directoryContents);
166+
$readEmails = new ReadEmails($directoryContents);
167+
$parseEmail = new ParseEmail();
168+
$analyseEmail = new Predict(signature: EmailAnalysis::class, instructor: $instructor);
169+
$getStats = new GetStats($readEmails, $parseEmail, $analyseEmail);
170+
171+
// EXECUTE LANGUAGE PROGRAM
172+
172173
$emailStats = $getStats->with(EmailStats::for('inbox'));
173174

174175
echo "Results:\n";
175176
dump($emailStats->get());
176177
?>
177178
```
178-
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
# Language programs
2+
3+
Instructor provides an addon allowing to implement complex processing flows
4+
using LLM in a modular way. This addon to Instructor has been inspired by DSPy
5+
library for Python (https://github.com/stanfordnlp/dspy).
6+
7+
Key components of language program:
8+
- Module subclasses - encapsulate processing logic
9+
- Signatures - define input and output for data processed by modules
10+
11+
NOTE: Other concepts from DSPy (optimizer, compiler, evaluator) have not been implemented yet.
12+
13+
Module consists of 3 key parts:
14+
- __construct() - initialization of module, prepare dependencies, setup submodules
15+
- signature() - define input and output for data processed by module
16+
- forward() - processing logic, return output data
17+
18+
`Predict` class is a special module, that uses Instructor's structured processing
19+
capabilities to execute inference on provided inputs and return output in a requested
20+
format.
21+
22+
```php
23+
<?php
24+
25+
use Cognesy\Instructor\Extras\Module\Addons\Predict\Predict;
26+
use Cognesy\Instructor\Extras\Module\Core\Module;
27+
use Cognesy\Instructor\Extras\Module\Signature\Attributes\InputField;
28+
use Cognesy\Instructor\Extras\Module\Signature\Attributes\OutputField;
29+
use Cognesy\Instructor\Extras\Module\CallData\SignatureData;
30+
use Cognesy\Instructor\Instructor;
31+
use Cognesy\Instructor\Schema\Attributes\Description;
32+
33+
$loader = require 'vendor/autoload.php';
34+
$loader->add('Cognesy\\Instructor\\', __DIR__ . '../../src/');
35+
36+
// DATA MODEL DECLARATIONS ////////////////////////////////////////////////////////////////
37+
38+
//#[Description('extract email details from text')]
39+
class ParsedEmail extends SignatureData {
40+
#[InputField('text containing email')]
41+
public string $text;
42+
43+
#[OutputField('email address of sender')]
44+
public string $senderEmail;
45+
#[OutputField('subject of the email')]
46+
public string $subject;
47+
#[OutputField('body of the email')]
48+
public string $body;
49+
}
50+
51+
class FixedEmail extends SignatureData {
52+
#[InputField('subject of the email')]
53+
public string $subject;
54+
#[InputField('body of the email')]
55+
public string $body;
56+
57+
#[OutputField('subject of the email with fixed spelling mistakes')]
58+
public string $fixedSubject;
59+
#[OutputField('body of the email with fixed spelling mistakes')]
60+
public string $fixedBody;
61+
}
62+
63+
class EmailTranslation extends SignatureData {
64+
#[InputField('subject of email')]
65+
public string $subject;
66+
#[InputField('body of email')]
67+
public string $body;
68+
#[InputField('language to translate to')]
69+
public string $language;
70+
71+
#[OutputField('translated subject of email')]
72+
public string $translatedSubject;
73+
#[OutputField('translated body of email')]
74+
public string $translatedBody;
75+
}
76+
77+
class Email {
78+
public function __construct(
79+
public string $senderEmail,
80+
public string $subject,
81+
public string $body
82+
) {}
83+
}
84+
85+
class EmailProcessingResults {
86+
public function __construct(
87+
public Email $original,
88+
public Email $fixed,
89+
public Email $translated
90+
) {}
91+
}
92+
93+
// MODULE DECLARATIONS ////////////////////////////////////////////////////////////////////
94+
95+
class ProcessEmail extends Module {
96+
public function __construct(
97+
private Predict $parse,
98+
private Predict $fix,
99+
private Predict $translate
100+
) {}
101+
102+
public function signature(): string {
103+
return 'text: string, language: string -> EmailProcessingResults';
104+
}
105+
106+
public function forward(string $text, string $language): EmailProcessingResults {
107+
$parsedEmail = $this->parse->with(
108+
ParsedEmail::fromArgs(
109+
text: $text
110+
)
111+
)->result();
112+
113+
$fixedEmail = $this->fix->with(
114+
FixedEmail::fromArgs(
115+
subject: $parsedEmail->subject,
116+
body: $parsedEmail->body
117+
)
118+
)->result();
119+
120+
$translatedEmail = $this->translate->with(
121+
EmailTranslation::fromArgs(
122+
subject: $fixedEmail->fixedSubject,
123+
body: $fixedEmail->fixedBody,
124+
language: $language
125+
)
126+
)->result();
127+
128+
return new EmailProcessingResults(
129+
new Email(
130+
$parsedEmail->senderEmail,
131+
$parsedEmail->subject,
132+
$parsedEmail->body
133+
),
134+
new Email(
135+
$parsedEmail->senderEmail,
136+
$fixedEmail->fixedSubject,
137+
$fixedEmail->fixedBody
138+
),
139+
new Email(
140+
$parsedEmail->senderEmail,
141+
$translatedEmail->translatedSubject,
142+
$translatedEmail->translatedBody
143+
)
144+
);
145+
}
146+
}
147+
148+
// PREPARE DEPENDENCIES ///////////////////////////////////////////////////////////////////
149+
150+
$instructor = new Instructor();
151+
152+
$extractEmail = new Predict(signature: ParsedEmail::class, instructor: $instructor);
153+
$fixEmail = new Predict(signature: FixedEmail::class, instructor: $instructor);
154+
$translateEmail = new Predict(signature: EmailTranslation::class, instructor: $instructor);
155+
156+
157+
// EXECUTE LANGUAGE PROGRAM ///////////////////////////////////////////////////////////////
158+
159+
$text = 'sender: [email protected], subject: Ofer, body: Im hapy abut the discount you offered and accept contract renewal';
160+
$language = 'French';
161+
162+
$parseAndTranslate = new ProcessEmail($extractEmail, $fixEmail, $translateEmail);
163+
$result = $parseAndTranslate->withArgs(text: $text, language: $language)->result();
164+
165+
echo "Results:\n";
166+
dump($result);
167+
?>
168+
```

src/Extras/Module/Addons/Predict/Predict.php

+1
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ public function forward(array $args, object $targetObject): mixed {
6565
count($args) === 0 => throw new \Exception('Empty input'),
6666
count($args) === 1 => reset($args),
6767
default => match(true) {
68+
is_array($args) => $args,
6869
is_array($args[0]) => $args[0],
6970
is_string($args[0]) => $args[0],
7071
default => throw new Exception('Invalid input - should be string or messages array'),

0 commit comments

Comments
 (0)