diff --git a/docs/cookbook/examples/troubleshooting/debugging.mdx b/docs/cookbook/examples/troubleshooting/debugging.mdx index 7bf68862..527e3364 100644 --- a/docs/cookbook/examples/troubleshooting/debugging.mdx +++ b/docs/cookbook/examples/troubleshooting/debugging.mdx @@ -31,7 +31,7 @@ class User { } $instructor = (new Instructor)->withClient(new OpenAIClient( - apiKey: Env::get('OPENAI_API_KEY'),// . 'invalid', // intentionally invalid API key + apiKey: Env::get('OPENAI_API_KEY'),// . 'invalid', // intentionally invalid API key baseUri: Env::get('OPENAI_BASE_URI'), )); diff --git a/src-hub/Commands/GenerateDocs.php b/src-hub/Commands/GenerateDocs.php index 8daa651f..a2741463 100644 --- a/src-hub/Commands/GenerateDocs.php +++ b/src-hub/Commands/GenerateDocs.php @@ -1,7 +1,7 @@ data = $data; - } - - public static function makeWith(InstructorInfo $data) : Instructor { - return (new static($data))->make(); - } - - public function make() : Instructor { - $instructor = new Instructor( - events: $this->data->events, - config: $this->data->config, - ); - $instructor->withClient($this->data->client); - $instructor->withDebug($this->data->debug ?? false, $this->data->stopOnDebug ?? false); - $instructor->withCache($this->data->cache ?? false); - return $instructor; - } -} diff --git a/src/Core/Factories/StreamFactory.php b/src/Core/Factories/StreamFactory.php deleted file mode 100644 index 75a5f64f..00000000 --- a/src/Core/Factories/StreamFactory.php +++ /dev/null @@ -1,19 +0,0 @@ -eventDispatcher); - } -} diff --git a/src/Core/Response/ResponseGenerator.php b/src/Core/Response/ResponseGenerator.php index f59d9434..c6afc4d0 100644 --- a/src/Core/Response/ResponseGenerator.php +++ b/src/Core/Response/ResponseGenerator.php @@ -11,8 +11,8 @@ use Cognesy\Instructor\Exceptions\JsonParsingException; use Cognesy\Instructor\Transformation\ResponseTransformer; use Cognesy\Instructor\Utils\Chain; -use Cognesy\Instructor\Utils\Json; -use Cognesy\Instructor\Utils\Result; +use Cognesy\Instructor\Utils\Json\Json; +use Cognesy\Instructor\Utils\Result\Result; use Cognesy\Instructor\Validation\ResponseValidator; use Cognesy\Instructor\Validation\ValidationResult; use Exception; diff --git a/src/Core/StreamResponse/PartialsGenerator.php b/src/Core/StreamResponse/PartialsGenerator.php index ed7780ec..2889c658 100644 --- a/src/Core/StreamResponse/PartialsGenerator.php +++ b/src/Core/StreamResponse/PartialsGenerator.php @@ -24,8 +24,8 @@ use Cognesy\Instructor\Transformation\ResponseTransformer; use Cognesy\Instructor\Utils\Arrays; use Cognesy\Instructor\Utils\Chain; -use Cognesy\Instructor\Utils\Json; -use Cognesy\Instructor\Utils\Result; +use Cognesy\Instructor\Utils\Json\Json; +use Cognesy\Instructor\Utils\Result\Result; use Exception; use Generator; diff --git a/src/Core/StreamResponse/Traits/ValidatesPartialResponse.php b/src/Core/StreamResponse/Traits/ValidatesPartialResponse.php index 380286a9..9fd0cd32 100644 --- a/src/Core/StreamResponse/Traits/ValidatesPartialResponse.php +++ b/src/Core/StreamResponse/Traits/ValidatesPartialResponse.php @@ -6,8 +6,8 @@ use Cognesy\Instructor\Exceptions\JsonParsingException; use Cognesy\Instructor\Utils\Arrays; use Cognesy\Instructor\Utils\Chain; -use Cognesy\Instructor\Utils\Json; -use Cognesy\Instructor\Utils\Result; +use Cognesy\Instructor\Utils\Json\Json; +use Cognesy\Instructor\Utils\Result\Result; use Exception; trait ValidatesPartialResponse diff --git a/src/Data/Messages/Traits/Message/HandlesCreation.php b/src/Data/Messages/Traits/Message/HandlesCreation.php index 304ce051..a64be4db 100644 --- a/src/Data/Messages/Traits/Message/HandlesCreation.php +++ b/src/Data/Messages/Traits/Message/HandlesCreation.php @@ -2,7 +2,7 @@ namespace Cognesy\Instructor\Data\Messages\Traits\Message; -use Cognesy\Instructor\Contracts\CanProvideMessage\CanProvideMessage; +use Cognesy\Instructor\Contracts\CanProvideMessage; use Cognesy\Instructor\Data\Messages\Message; use Cognesy\Instructor\Data\Messages\Utils\Text; use Exception; diff --git a/src/Data/Messages/Traits/Messages/HandlesCreation.php b/src/Data/Messages/Traits/Messages/HandlesCreation.php index e65538d2..00632475 100644 --- a/src/Data/Messages/Traits/Messages/HandlesCreation.php +++ b/src/Data/Messages/Traits/Messages/HandlesCreation.php @@ -2,7 +2,7 @@ namespace Cognesy\Instructor\Data\Messages\Traits\Messages; -use Cognesy\Instructor\Contracts\CanProvideMessage\CanProvideMessage; +use Cognesy\Instructor\Contracts\CanProvideMessage; use Cognesy\Instructor\Contracts\CanProvideMessages; use Cognesy\Instructor\Data\Messages\Message; use Cognesy\Instructor\Data\Messages\Messages; diff --git a/src/Data/Messages/Utils/Text.php b/src/Data/Messages/Utils/Text.php index dc198165..1a868939 100644 --- a/src/Data/Messages/Utils/Text.php +++ b/src/Data/Messages/Utils/Text.php @@ -4,7 +4,7 @@ use BackedEnum; use Closure; -use Cognesy\Instructor\Utils\Json; +use Cognesy\Instructor\Utils\Json\Json; // TODO: this should be moved to a chain-like component, so the way we handle inputs can be customized class Text diff --git a/src/Data/Traits/Example/HandlesConversion.php b/src/Data/Traits/Example/HandlesConversion.php index 4dd97493..3feb7207 100644 --- a/src/Data/Traits/Example/HandlesConversion.php +++ b/src/Data/Traits/Example/HandlesConversion.php @@ -3,7 +3,7 @@ use BackedEnum; use Cognesy\Instructor\Data\Messages\Messages; -use Cognesy\Instructor\Utils\Json; +use Cognesy\Instructor\Utils\Json\Json; use Cognesy\Instructor\Utils\TemplateUtil; trait HandlesConversion diff --git a/src/Data/Traits/Example/HandlesCreation.php b/src/Data/Traits/Example/HandlesCreation.php index 27d03ee4..965181a3 100644 --- a/src/Data/Traits/Example/HandlesCreation.php +++ b/src/Data/Traits/Example/HandlesCreation.php @@ -1,7 +1,7 @@ $carry && is_string($item), true); if (!$areKeysStrings) { - return Result::failure("Call must use named parameters: make(argName: argValue, ...)"); + return Result\Result::failure("Call must use named parameters: make(argName: argValue, ...)"); } $diff = array_diff($argNames, $expectedNames); if (count($diff) > 0) { - return Result::failure("Unexpected input fields: " . implode(', ', $diff)); + return Result\Result::failure("Unexpected input fields: " . implode(', ', $diff)); } return Result::success(true); } diff --git a/src/Extras/LegacyModule/Utils/InputOutputMapper.php b/src/Extras/LegacyModule/Utils/InputOutputMapper.php index 9e570873..530faed7 100644 --- a/src/Extras/LegacyModule/Utils/InputOutputMapper.php +++ b/src/Extras/LegacyModule/Utils/InputOutputMapper.php @@ -3,7 +3,7 @@ use Cognesy\Instructor\Extras\Module\Call\Contracts\CanBeProcessed; use Cognesy\Instructor\Extras\Module\CallData\Contracts\HasInputOutputData; -use Cognesy\Instructor\Utils\Json; +use Cognesy\Instructor\Utils\Json\Json; use Exception; class InputOutputMapper diff --git a/src/Extras/Maybe/Maybe.php b/src/Extras/Maybe/Maybe.php index 9125e441..12c9f035 100644 --- a/src/Extras/Maybe/Maybe.php +++ b/src/Extras/Maybe/Maybe.php @@ -6,9 +6,8 @@ use Cognesy\Instructor\Deserialization\Deserializers\SymfonyDeserializer; use Cognesy\Instructor\Schema\Data\TypeDetails; use Cognesy\Instructor\Schema\Factories\SchemaFactory; -use Cognesy\Instructor\Schema\Factories\TypeDetailsFactory; use Cognesy\Instructor\Schema\Visitors\SchemaToJsonSchema; -use Cognesy\Instructor\Utils\Json; +use Cognesy\Instructor\Utils\Json\Json; class Maybe implements CanProvideJsonSchema, CanDeserializeSelf { diff --git a/src/Extras/Module/Core/Traits/ModuleCall/HandlesOutputs.php b/src/Extras/Module/Core/Traits/ModuleCall/HandlesOutputs.php index d980fc4f..bce1fa91 100644 --- a/src/Extras/Module/Core/Traits/ModuleCall/HandlesOutputs.php +++ b/src/Extras/Module/Core/Traits/ModuleCall/HandlesOutputs.php @@ -1,7 +1,7 @@ k = $k; - $this->b = $b; - if (empty($stopwords)) { - $stopwords = $this->englishStopwords; - } - $this->stopwords = array_flip(array_map('mb_strtolower', $stopwords)); - } - - /** - * Preprocess documents and build TF-IDF dictionaries - * - * @param array $docs Array of documents - */ - public function preprocess(array $docs): void { - $N = count($docs); - $totalLength = 0; - $termFrequencies = []; - - foreach ($docs as $docId => $doc) { - $words = $this->tokenize($doc); - $docLength = count($words); - $totalLength += $docLength; - - foreach ($words as $word) { - $this->tfDictionary[$docId][$word] = ($this->tfDictionary[$docId][$word] ?? 0) + 1; - $termFrequencies[$word][$docId] = true; - } - } - - $this->avgDocLength = $totalLength / $N; - - foreach ($termFrequencies as $term => $termDocs) { - $this->idfDictionary[$term] = log(($N - count($termDocs) + 0.5) / (count($termDocs) + 0.5) + 1); - } - } - - /** - * Tokenize a document or query into words - * - * @param string $text - * @return array - */ - private function tokenize(string $text): array { - // Simple tokenization by splitting on whitespace and converting to lowercase - $words = preg_split('/\s+/', mb_strtolower($text)); - - // Remove stopwords - return array_values(array_diff($words, array_keys($this->stopwords))); - } - - /** - * Convert a string query into an array of keywords - * - * @param string $query - * @return array - */ - public function processQuery(string $query): array { - $keywords = $this->tokenize($query); - - // Remove duplicates and reindex array - return array_values(array_unique($keywords)); - } - - /** - * Calculate relevance score - * - * @param float $idf Inverse document frequency of the keyword - * @param float $tf Term frequency of the keyword in the document - * @param float $docLength Length of the current document - * @return float - */ - public function score(float $idf, float $tf, float $docLength): float { - $L = $docLength / $this->avgDocLength; - return ($idf * ($this->k + 1) * $tf) / ($this->k * (1.0 - $this->b + $this->b * $L) + $tf); - } - - /** - * Calculate relevance of a document for given keywords - * - * @param array $keywords Array of keywords - * @param int $docId Document ID - * @return float - */ - public function documentScore(array $keywords, int $docId): float { - $score = 0; - $docLength = array_sum($this->tfDictionary[$docId]); - - foreach ($keywords as $keyword) { - $idf = $this->idfDictionary[$keyword] ?? 0; - $tf = $this->tfDictionary[$docId][$keyword] ?? 0; - $score += $this->score($idf, $tf, $docLength); - } - - return $score; - } - - /** - * Search for documents relevant to given keywords or query string - * - * @param array|string $query Array of keywords or a string query - * @return array Sorted array of document IDs and their relevance scores - */ - public function search($query): array { - $keywords = is_array($query) ? $query : $this->processQuery($query); - - $scores = []; - - foreach ($this->tfDictionary as $docId => $terms) { - $scores[$docId] = $this->documentScore($keywords, $docId); - } - - arsort($scores); - return $scores; - } - - /** - * Set stopwords - * - * @param array $stopwords Array of stopwords to ignore in queries - * @return self - */ - public function setStopwords(array $stopwords): self { - $this->stopwords = array_flip(array_map('mb_strtolower', $stopwords)); - return $this; - } - - /** - * Set the k parameter - * The k parameter controls the impact of term frequency saturation. It determines how much the score - * should increase when a term appears multiple times in a document. - * - * @param float $k - * @return self - */ - public function setK(float $k): self { - $this->k = $k; - return $this; - } - - /** - * Set the b parameter - * The b parameter controls the scaling by document length. It determines how much to penalize or - * favor documents based on their length compared to the average document length. - * - * @param float $b - * @return self - */ - public function setB(float $b): self { - $this->b = $b; - return $this; - } -} diff --git a/src/Utils/Chain.php b/src/Utils/Chain.php index e6315f9d..afbad909 100644 --- a/src/Utils/Chain.php +++ b/src/Utils/Chain.php @@ -2,6 +2,9 @@ namespace Cognesy\Instructor\Utils; +use Cognesy\Instructor\Utils\Result\Failure; +use Cognesy\Instructor\Utils\Result\Result; +use Cognesy\Instructor\Utils\Result\Success; use Exception; /** diff --git a/src/Utils/Color.php b/src/Utils/Cli/Color.php similarity index 94% rename from src/Utils/Color.php rename to src/Utils/Cli/Color.php index 9cc434ce..43955ae3 100644 --- a/src/Utils/Color.php +++ b/src/Utils/Cli/Color.php @@ -1,5 +1,5 @@ - */ - private array $items; - private string $class; - - /** - * @param string $class - * @param array $items - */ - public function __construct(string $class, array $items = []) { - $this->class = $class; - $this->items = $items; - } - - /** - * @param string $class - * @return static - */ - public static function of(string $class): self { - return new self($class); - } - - public function getType(): string { - return $this->class; - } - - /** - * @param array $items - * @return static - */ - public function add(array $items): self { - $newItems = array_filter($items, fn($item) => $item instanceof $this->class); - if (count($items) !== count($newItems)) { - throw new \InvalidArgumentException("All items must be of type {$this->class}"); - } - return new self($this->class, array_merge($this->items, $newItems)); - } - - public function count(): int { - return count($this->items); - } - - /** - * @param int $offset - */ - public function offsetExists(mixed $offset): bool { - return isset($this->items[$offset]); - } - - /** - * @param int $offset - * @return T|null - */ - public function offsetGet(mixed $offset): mixed { - return $this->items[$offset] ?? null; - } - - /** - * @param int $offset - * @param T $value - */ - public function offsetSet(mixed $offset, mixed $value): void { - $this->items[$offset] = $value; - } - - /** - * @param int $offset - */ - public function offsetUnset(mixed $offset): void { - unset($this->items[$offset]); - } - - /** - * @return Traversable - */ - public function getIterator(): Traversable { - return new ArrayIterator($this->items); - } -} \ No newline at end of file diff --git a/src/Utils/Json.php b/src/Utils/Json/Json.php similarity index 94% rename from src/Utils/Json.php rename to src/Utils/Json/Json.php index 56fb2626..6b39a1f8 100644 --- a/src/Utils/Json.php +++ b/src/Utils/Json/Json.php @@ -1,6 +1,6 @@ */ -// function getNameLength(?string $name): Optional { -// return Optional::of($name) -// ->apply(fn($n) => trim($n)) -// ->apply(fn($n) => strlen($n)); -// } -// } -// -// $nameLength = $person->getNameLength(null)->getOrElse(0); -////////////////////////////////////////////////////////// -use JetBrains\PhpStorm\Deprecated; - -/** - * @template T The type of the value in case of success. - */ -#[Deprecated('Not used - may be removed in the future.')] -class Optional { - private mixed $value; - - /** - * @param T $value - */ - private function __construct(mixed $value) { - $this->value = $value; - } - - /** - * @param mixed $value - * @return self - */ - public static function of(mixed $value): self { - return new self($value); - } - - public function exists(): bool { - return $this->value !== null; - } - - /** - * @param T $default - * @return T - */ - public function getOrElse(mixed $default) : mixed { - return $this->exists() ? $this->value : $default; - } - - /** - * @param callable $f - * @return self - */ - public function apply(callable $f): self { - if (!$this->exists()) { - return self::of(null); - } - - return self::of($f($this->value)); - } -} - diff --git a/src/Utils/Failure.php b/src/Utils/Result/Failure.php similarity index 89% rename from src/Utils/Failure.php rename to src/Utils/Result/Failure.php index d1030e97..b20a5575 100644 --- a/src/Utils/Failure.php +++ b/src/Utils/Result/Failure.php @@ -1,5 +1,5 @@ tryExtractToolCall($content); - $name = $this->tryExtractToolName($xml); - $args = $this->tryExtractXmlArgs($content); - if (empty($args)) { - $args = $this->tryExtractJsonArgs($content); - } - return [$name, $args]; - } - - protected function tryExtractToolCall(string $content) : string { - $pattern = '/.*<\/function_calls>/s'; - preg_match($pattern, $content, $matches); - $xmlString = $matches[0] ?? ''; - return trim($xmlString); - } - - protected function tryExtractToolName(string $content) : string { - $pattern = '/(.*?)<\/tool_name>/s'; - preg_match($pattern, $content, $matches); - $toolName = $matches[1] ?? ''; - return trim($toolName); - } - - protected function tryExtractXmlArgs(string $content) : string { - $pattern = '/(.*?)<\/extracted_object>/s'; - preg_match($pattern, $content, $matches); - $object = $matches[1] ?? ''; - return trim($object); - } - - protected function tryExtractJsonArgs(string $content) : string { - return trim(Json::find($content)); - } -} \ No newline at end of file diff --git a/src/Validation/Exceptions/ValidationException.php b/src/Validation/Exceptions/ValidationException.php index 06e4a8b4..de72b596 100644 --- a/src/Validation/Exceptions/ValidationException.php +++ b/src/Validation/Exceptions/ValidationException.php @@ -1,7 +1,7 @@ assertInstanceOf(Collection::class, $collection); -}); - -test('can create a new collection with items', function () { - $item1 = new stdClass(); - $item2 = new stdClass(); - $collection = new Collection(stdClass::class, [$item1, $item2]); - - $this->assertCount(2, $collection); - $this->assertSame($item1, $collection[0]); - $this->assertSame($item2, $collection[1]); -}); - -test('can create a new collection using static method', function () { - $collection = Collection::of(stdClass::class); - - $this->assertInstanceOf(Collection::class, $collection); -}); - -test('can add items to collection', function () { - $item1 = new stdClass(); - $item2 = new stdClass(); - $collection = Collection::of(stdClass::class)->add([$item1, $item2]); - - $this->assertCount(2, $collection); - $this->assertSame($item1, $collection[0]); - $this->assertSame($item2, $collection[1]); -}); - -test('throws exception when adding invalid item type', function () { - $collection = Collection::of(stdClass::class); - - $this->expectException(InvalidArgumentException::class); - $collection->add([new stdClass(), new DateTime()]); -}); - -test('can access items using array access', function () { - $item1 = new stdClass(); - $item2 = new stdClass(); - $collection = new Collection(stdClass::class, [$item1, $item2]); - - $this->assertTrue(isset($collection[0])); - $this->assertSame($item1, $collection[0]); - $this->assertFalse(isset($collection[2])); -}); - -test('can modify items using array access', function () { - $item1 = new stdClass(); - $item2 = new stdClass(); - $collection = new Collection(stdClass::class, [$item1]); - - $collection[0] = $item2; - - $this->assertSame($item2, $collection[0]); -}); - -test('can iterate over collection', function () { - $item1 = new stdClass(); - $item2 = new stdClass(); - $collection = new Collection(stdClass::class, [$item1, $item2]); - - $items = []; - foreach ($collection as $item) { - $items[] = $item; - } - - $this->assertSame([$item1, $item2], $items); -}); \ No newline at end of file diff --git a/tests/Feature/Utils/ConsoleTest.php b/tests/Feature/Utils/ConsoleTest.php index e155f940..3c646bca 100644 --- a/tests/Feature/Utils/ConsoleTest.php +++ b/tests/Feature/Utils/ConsoleTest.php @@ -1,7 +1,7 @@ toBe($expected); }); diff --git a/tests/Feature/Utils/JsonParserTest.php b/tests/Feature/Utils/JsonParserTest.php index 7754bbdc..20e478cd 100644 --- a/tests/Feature/Utils/JsonParserTest.php +++ b/tests/Feature/Utils/JsonParserTest.php @@ -1,6 +1,6 @@ assertTrue($optional->exists()); - $this->assertSame('value', $optional->getOrElse('default')); -}); - -test('it can create an Optional instance with null', function () { - $optional = Optional::of(null); - - $this->assertFalse($optional->exists()); - $this->assertSame('default', $optional->getOrElse('default')); -}); - -test('it can apply a function to the value', function () { - $optional = Optional::of(' hello '); - - $trimmed = $optional->apply(fn($value) => trim($value)); - - $this->assertTrue($trimmed->exists()); - $this->assertSame('hello', $trimmed->getOrElse('default')); -}); - -test('it can chain multiple functions', function () { - $optional = Optional::of(' hello '); - - $lengthOfTrimmed = $optional - ->apply(fn($value) => trim($value)) - ->apply(fn($value) => strlen($value)); - - $this->assertTrue($lengthOfTrimmed->exists()); - $this->assertSame(5, $lengthOfTrimmed->getOrElse(-1)); -}); - -test('it returns an Optional with null when applying a function to null', function () { - $optional = Optional::of(null); - - $applied = $optional->apply(fn($value) => strlen($value)); - - $this->assertFalse($applied->exists()); - $this->assertSame(0, $applied->getOrElse(0)); -}); \ No newline at end of file diff --git a/tests/Feature/Utils/PartialJsonTest.php b/tests/Feature/Utils/PartialJsonTest.php index 31fa8092..c5b6ec22 100644 --- a/tests/Feature/Utils/PartialJsonTest.php +++ b/tests/Feature/Utils/PartialJsonTest.php @@ -1,5 +1,6 @@