Skip to content

Commit 68b81e0

Browse files
committed
refactor(polyglot,instructor): clean attempt lifecycle
1 parent c101161 commit 68b81e0

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+773
-219
lines changed

.beads/issues.jsonl

Lines changed: 26 additions & 0 deletions
Large diffs are not rendered by default.

docs/release-notes/v1.9.1.mdx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
- Prevent null dereference when finalizing streaming responses with no current attempt
66
- packages/polyglot/src/Inference/Data/InferenceExecution.php
7-
- Null-safe access to `currentAttempt` in `withFinalizedPartialResponse()` and `withFailedFinalizedResponse()`
7+
- Null-safe access to `currentAttempt` in `withFinalizedPartialResponse()` and `withFailedAttempt()`
88
- packages/polyglot/src/Inference/Data/InferenceAttempt.php
99
- Null-safe `withFinalizedPartialResponse()` using empty `PartialInferenceResponseList` when needed
1010

@@ -36,4 +36,3 @@
3636
- This release focuses on correctness and immutability:
3737
- Eliminates potential fatals in streaming finalization and console display
3838
- Standardizes usage accumulation to immutable patterns across modules
39-

packages/instructor/src/Config/StructuredOutputConfig.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,10 @@ public function useObjectReferences(): bool {
186186
return $this->useObjectReferences;
187187
}
188188

189+
/**
190+
* Maximum number of retries after the first attempt.
191+
* Total attempts allowed = maxRetries + 1.
192+
*/
189193
public function maxRetries(): int {
190194
return $this->maxRetries;
191195
}

packages/instructor/src/Data/ResponseModel.php

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -133,16 +133,18 @@ public function withToolDescription(string $toolDescription) : static {
133133
}
134134

135135
/** @param array<string, mixed> $values */
136-
public function setPropertyValues(array $values) : void {
136+
public function withPropertyValues(array $values) : static {
137137
if (!is_object($this->instance)) {
138-
return;
138+
return $this;
139139
}
140+
$instance = clone $this->instance;
140141
foreach ($values as $name => $value) {
141-
if (property_exists($this->instance, $name)) {
142+
if (property_exists($instance, $name)) {
142143
/** @phpstan-ignore-next-line */
143-
$this->instance->$name = $value;
144+
$instance->$name = $value;
144145
}
145146
}
147+
return $this->with(instance: $instance);
146148
}
147149

148150
// CONVERSION //////////////////////////////////////////////////////
@@ -200,7 +202,7 @@ public function toolChoice() : string|array {
200202
public function toArray() : array {
201203
return [
202204
'class' => $this->class,
203-
'instance' => get_object_vars($this->instance),
205+
'instance' => is_object($this->instance) ? get_object_vars($this->instance) : [],
204206
'schema' => $this->schema->toArray(),
205207
'jsonSchema' => $this->jsonSchema,
206208
'schemaName' => $this->schemaName,

packages/instructor/src/Extraction/ResponseExtractor.php

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,11 @@ public static function defaultStreamingExtractors(): array
130130
public function extract(InferenceResponse $response, OutputMode $mode): Result
131131
{
132132
// 1. DATA ACCESS: Get content string based on mode (uniform for all modes)
133-
$content = $this->getContentString($response, $mode);
133+
$contentResult = $this->getContentString($response, $mode);
134+
if ($contentResult->isFailure()) {
135+
return Result::failure($contentResult->error());
136+
}
137+
$content = $contentResult->unwrap();
134138

135139
if (empty($content)) {
136140
return Result::failure('Empty response content');
@@ -169,31 +173,36 @@ public function extractors(): array
169173
/**
170174
* Uniform data access - no special cases, just different sources.
171175
*/
172-
private function getContentString(InferenceResponse $response, OutputMode $mode): string
176+
/**
177+
* @return Result<string, Throwable>
178+
*/
179+
private function getContentString(InferenceResponse $response, OutputMode $mode): Result
173180
{
174181
return match ($mode) {
175182
OutputMode::Tools => $this->getToolCallContent($response),
176-
default => $response->content(),
183+
default => Result::success($response->content()),
177184
};
178185
}
179186

180187
/**
181188
* Get content from tool calls as JSON string.
189+
*
190+
* @return Result<string, Throwable>
182191
*/
183-
private function getToolCallContent(InferenceResponse $response): string
192+
private function getToolCallContent(InferenceResponse $response): Result
184193
{
185194
$toolCalls = $response->toolCalls();
186195

187196
if ($toolCalls->isEmpty()) {
188197
// Fallback for providers that return tool-call JSON in content.
189-
return $response->content();
198+
return Result::success($response->content());
190199
}
191200

192201
if ($toolCalls->hasSingle()) {
193-
return json_encode($toolCalls->first()?->args() ?? [], JSON_THROW_ON_ERROR);
202+
return Result::try(fn() => json_encode($toolCalls->first()?->args() ?? [], JSON_THROW_ON_ERROR));
194203
}
195204

196-
return json_encode($toolCalls->toArray(), JSON_THROW_ON_ERROR);
205+
return Result::try(fn() => json_encode($toolCalls->toArray(), JSON_THROW_ON_ERROR));
197206
}
198207

199208
/**

packages/instructor/src/StructuredOutput.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -121,9 +121,9 @@ public function withHttpClient(HttpClient $httpClient): static {
121121
return $this;
122122
}
123123

124-
public function withHttpClientPreset(string $string): static {
124+
public function withHttpClientPreset(string $preset): static {
125125
$builder = new HttpClientBuilder(events: $this->events);
126-
$this->httpClient = $builder->withPreset($string)->create();
126+
$this->httpClient = $builder->withPreset($preset)->create();
127127
return $this;
128128
}
129129

@@ -132,7 +132,7 @@ public function withDebugPreset(string $preset): static {
132132
return $this;
133133
}
134134

135-
public function withClientInstance(string $driverName, object $clientInstance): self {
135+
public function withClientInstance(string $driverName, object $clientInstance): static {
136136
$builder = new HttpClientBuilder(events: $this->events);
137137
$this->httpClient = $builder->withClientInstance(
138138
driverName: $driverName,
@@ -222,7 +222,7 @@ public function withCachedContext(
222222
string $system = '',
223223
string $prompt = '',
224224
array $examples = [],
225-
): ?self {
225+
): static {
226226
$this->requestBuilder->withCachedContext($messages, $system, $prompt, $examples);
227227
return $this;
228228
}

packages/instructor/src/StructuredOutputStream.php

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ class StructuredOutputStream
3535
private StructuredOutputExecution $execution;
3636
private InferenceResponse|null $lastResponse = null;
3737
private ResponseCachePolicy $cachePolicy;
38+
private bool $started = false;
3839

3940
/**
4041
* @param Generator<StructuredOutputExecution> $stream
@@ -141,9 +142,8 @@ public function finalValue() : mixed {
141142
* Processes response stream and returns only the final response.
142143
*/
143144
public function finalResponse() : InferenceResponse {
144-
foreach ($this->streamResponses() as $partialResponse) {
145+
foreach ($this->streamResponses() as $_) {
145146
// Just consume the stream, processStream() handles the updates
146-
$tmp = $partialResponse;
147147
}
148148
if (is_null($this->lastResponse)) {
149149
throw new RuntimeException('Expected final InferenceResponse, got null');
@@ -153,8 +153,8 @@ public function finalResponse() : InferenceResponse {
153153

154154
/**
155155
* Returns raw stream for custom processing.
156-
* Processing with this method does not trigger any events or dispatch any notifications.
157-
* It also does not update usage data on the stream object.
156+
* StructuredOutputStarted may have already been dispatched when the stream was created.
157+
* Processing with this method does not emit response update events or usage updates.
158158
*
159159
* @return Generator<StructuredOutputExecution>
160160
*/
@@ -200,7 +200,10 @@ private function streamResponses(): Generator {
200200
* @return Generator<StructuredOutputExecution> A generator yielding structured output execution updates.
201201
*/
202202
private function getStream(StructuredOutputExecution $execution) : Generator {
203-
$this->events->dispatch(new StructuredOutputStarted(['request' => $execution->request()->toArray()]));
203+
if (!$this->started) {
204+
$this->events->dispatch(new StructuredOutputStarted(['request' => $execution->request()->toArray()]));
205+
$this->started = true;
206+
}
204207

205208
return match($this->shouldCache()) {
206209
false => $this->streamWithoutCaching($execution),

packages/instructor/src/Validation/ResponseValidator.php

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,14 @@ protected function validateObject(object $response) : ValidationResult {
7272
$validator instanceof CanValidateObject => $validator,
7373
default => throw new Exception('Validator must implement CanValidateObject interface'),
7474
};
75-
// TODO: how do we handle exceptions here?
76-
$results[] = $validator->validate($response);
75+
try {
76+
$results[] = $validator->validate($response);
77+
} catch (\Throwable $error) {
78+
$results[] = ValidationResult::invalid(
79+
new ValidationError(field: 'exception', value: null, message: $error->getMessage()),
80+
'Validator threw an exception',
81+
);
82+
}
7783
}
7884
return ValidationResult::merge($results);
7985
}

packages/instructor/src/Validation/ValidationResult.php

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,15 @@ static public function valid(): ValidationResult {
2323
return new ValidationResult();
2424
}
2525

26-
static public function invalid(string|array $errors, string $message = ''): ValidationResult {
27-
if (is_string($errors)) {
28-
$errors = [$errors];
29-
}
30-
if (count($errors) === 0) {
26+
/**
27+
* @param ValidationError|ValidationError[] $errors
28+
*/
29+
static public function invalid(ValidationError|array $errors, string $message = ''): ValidationResult {
30+
$normalized = self::normalizeErrors($errors);
31+
if (count($normalized) === 0) {
3132
throw new \InvalidArgumentException('Errors must be provided when creating an invalid ValidationResult');
3233
}
33-
return new ValidationResult($errors, $message);
34+
return new ValidationResult($normalized, $message);
3435
}
3536

3637
static public function make(array $errors = [], string $message = ''): ValidationResult {
@@ -57,6 +58,18 @@ static public function merge(array $validationResults, string $message = ''): Va
5758
};
5859
}
5960

61+
/**
62+
* @param ValidationError|ValidationError[] $errors
63+
* @return ValidationError[]
64+
*/
65+
private static function normalizeErrors(ValidationError|array $errors): array {
66+
$list = $errors instanceof ValidationError ? [$errors] : $errors;
67+
return array_values(array_filter(
68+
$list,
69+
fn (mixed $error): bool => $error instanceof ValidationError
70+
));
71+
}
72+
6073
/// CONVENIENCE METHODS //////////////////////////////////////////////////////////////////
6174

6275
public function isValid(): bool {
@@ -89,4 +102,4 @@ public function toArray(): array {
89102
'message' => $this->message,
90103
];
91104
}
92-
}
105+
}

packages/instructor/src/Validation/Validators/SelfValidator.php

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use Cognesy\Instructor\Validation\Contracts\CanValidateObject;
66
use Cognesy\Instructor\Validation\Contracts\CanValidateSelf;
7+
use Cognesy\Instructor\Validation\ValidationError;
78
use Cognesy\Instructor\Validation\ValidationResult;
89

910
class SelfValidator implements CanValidateObject
@@ -14,8 +15,12 @@ public function validate(object $dataObject): ValidationResult {
1415
return $dataObject->validate();
1516
}
1617
return ValidationResult::invalid(
17-
['Object does not implement CanValidateSelf interface'],
18+
new ValidationError(
19+
field: 'object',
20+
value: $dataObject,
21+
message: 'Object does not implement CanValidateSelf interface',
22+
),
1823
'Validation failed',
1924
);
2025
}
21-
}
26+
}

0 commit comments

Comments
 (0)