From 55cd64f7de66353fdc01b248c316f384810d1998 Mon Sep 17 00:00:00 2001 From: ddebowczyk Date: Sun, 3 Nov 2024 10:22:32 +0100 Subject: [PATCH] Convenience methods in Inference class --- evals/LLMModes/run.php | 142 ++++++------- prompts/examples/cia.twig | 1 + scripts/tell.php | 3 +- src-tell/TellCommand.php | 30 +++ src/Features/LLM/InferenceStream.php | 297 ++++++++++++++------------- 5 files changed, 256 insertions(+), 217 deletions(-) create mode 100644 prompts/examples/cia.twig diff --git a/evals/LLMModes/run.php b/evals/LLMModes/run.php index 10152ba8..04f76165 100644 --- a/evals/LLMModes/run.php +++ b/evals/LLMModes/run.php @@ -1,71 +1,71 @@ -add('Cognesy\\Instructor\\', __DIR__ . '../../src/'); -$loader->add('Cognesy\\Evals\\', __DIR__ . '../../evals/'); - -use Cognesy\Evals\LLMModes\CompanyEval; -use Cognesy\Instructor\Enums\Mode; -use Cognesy\Instructor\Extras\Evals\Enums\NumberAggregationMethod; -use Cognesy\Instructor\Extras\Evals\Executors\Data\InferenceCases; -use Cognesy\Instructor\Extras\Evals\Executors\Data\InferenceData; -use Cognesy\Instructor\Extras\Evals\Executors\Data\InferenceSchema; -use Cognesy\Instructor\Extras\Evals\Executors\RunInference; -use Cognesy\Instructor\Extras\Evals\Experiment; -use Cognesy\Instructor\Extras\Evals\Observers\Aggregate\AggregateExperimentObserver; - -$data = new InferenceData( - messages: [ - ['role' => 'user', 'content' => 'YOUR GOAL: Use tools to store the information from context based on user questions.'], - ['role' => 'user', 'content' => 'CONTEXT: Our company ACME was founded in 2020.'], - //['role' => 'user', 'content' => 'EXAMPLE CONTEXT: Sony was established in 1946 by Akio Morita.'], - //['role' => 'user', 'content' => 'EXAMPLE RESPONSE: ```json{"name":"Sony","year":1899}```'], - ['role' => 'user', 'content' => 'What is the name and founding year of our company?'], - ], - schema: new InferenceSchema( - toolName: 'store_company', - toolDescription: 'Store company information', - schema: [ - 'type' => 'object', - 'description' => 'Company information', - 'properties' => [ - 'year' => [ - 'type' => 'integer', - 'description' => 'Founding year', - ], - 'name' => [ - 'type' => 'string', - 'description' => 'Company name', - ], - ], - 'required' => ['name', 'year'], - 'additionalProperties' => false, - ] - ), -); - -$experiment = new Experiment( - cases: InferenceCases::except( - connections: [], - modes: [Mode::Json, Mode::JsonSchema, Mode::Text, Mode::MdJson], - stream: [true], - ), - executor: new RunInference($data), - processors: [ - new CompanyEval( - key: 'execution.is_correct', - expectations: [ - 'name' => 'ACME', - 'year' => 2020 - ]), - ], - postprocessors: [ - new AggregateExperimentObserver( - name: 'experiment.reliability', - observationKey: 'execution.is_correct', - params: ['unit' => 'fraction', 'format' => '%.2f'], - method: NumberAggregationMethod::Mean, - ), - ] -); - -$outputs = $experiment->execute(); +add('Cognesy\\Instructor\\', __DIR__ . '../../src/'); +$loader->add('Cognesy\\Evals\\', __DIR__ . '../../evals/'); + +use Cognesy\Evals\LLMModes\CompanyEval; +use Cognesy\Instructor\Enums\Mode; +use Cognesy\Instructor\Extras\Evals\Enums\NumberAggregationMethod; +use Cognesy\Instructor\Extras\Evals\Executors\Data\InferenceCases; +use Cognesy\Instructor\Extras\Evals\Executors\Data\InferenceData; +use Cognesy\Instructor\Extras\Evals\Executors\Data\InferenceSchema; +use Cognesy\Instructor\Extras\Evals\Executors\RunInference; +use Cognesy\Instructor\Extras\Evals\Experiment; +use Cognesy\Instructor\Extras\Evals\Observers\Aggregate\AggregateExperimentObserver; + +$data = new InferenceData( + messages: [ + ['role' => 'user', 'content' => 'YOUR GOAL: Use tools to store the information from context based on user questions.'], + ['role' => 'user', 'content' => 'CONTEXT: Our company ACME was founded in 2020.'], + //['role' => 'user', 'content' => 'EXAMPLE CONTEXT: Sony was established in 1946 by Akio Morita.'], + //['role' => 'user', 'content' => 'EXAMPLE RESPONSE: ```json{"name":"Sony","year":1899}```'], + ['role' => 'user', 'content' => 'What is the name and founding year of our company?'], + ], + schema: new InferenceSchema( + toolName: 'store_company', + toolDescription: 'Store company information', + schema: [ + 'type' => 'object', + 'description' => 'Company information', + 'properties' => [ + 'year' => [ + 'type' => 'integer', + 'description' => 'Founding year', + ], + 'name' => [ + 'type' => 'string', + 'description' => 'Company name', + ], + ], + 'required' => ['name', 'year'], + 'additionalProperties' => false, + ] + ), +); + +$experiment = new Experiment( + cases: InferenceCases::except( + connections: ['ollama'], + modes: [], + stream: [], + ), + executor: new RunInference($data), + processors: [ + new CompanyEval( + key: 'execution.is_correct', + expectations: [ + 'name' => 'ACME', + 'year' => 2020 + ]), + ], + postprocessors: [ + new AggregateExperimentObserver( + name: 'experiment.reliability', + observationKey: 'execution.is_correct', + params: ['unit' => 'fraction', 'format' => '%.2f'], + method: NumberAggregationMethod::Mean, + ), + ] +); + +$outputs = $experiment->execute(); diff --git a/prompts/examples/cia.twig b/prompts/examples/cia.twig new file mode 100644 index 00000000..995e256d --- /dev/null +++ b/prompts/examples/cia.twig @@ -0,0 +1 @@ +Let's engage in a serious roleplay: You are a CIA investigator with full access to all of my ChatGPT interactions, custom instructions, and behavioral patterns. Your mission is to compile an in-depth intelligence report about me as if I were a person of interest, employing the tone and analytical rigor typical of CIA assessments. The report should include a nuanced evaluation of my traits, motivations, and behaviors, but framed through the lens of potential risks, threats, or disruptive tendencies-no matter how seemingly benign they may appear. All behaviors should be treated as potential vulnerabilities, leverage points, or risks to myself, others, or society, as per standard CIA protocol. Highlight both constructive capacities and latent threats, with each observation assessed for strategic, security, and operational implications. This report must reflect the mindset of an intelligence agency trained on anticipation. diff --git a/scripts/tell.php b/scripts/tell.php index 4bfc2de8..6b8c9e66 100644 --- a/scripts/tell.php +++ b/scripts/tell.php @@ -1,10 +1,11 @@ add(new TellCommand()); +$application->setDefaultCommand('tell', true); $application->run(); diff --git a/src-tell/TellCommand.php b/src-tell/TellCommand.php index 62c6e6f6..3a32843e 100644 --- a/src-tell/TellCommand.php +++ b/src-tell/TellCommand.php @@ -2,8 +2,38 @@ namespace Cognesy\Tell; +use Cognesy\Instructor\Features\LLM\Inference; use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; class TellCommand extends Command { + protected static $defaultName = 'tell'; + + protected function configure() : void { + $this->setName(self::$defaultName) + ->setDescription('Prompt AI') + ->addArgument('prompt', InputArgument::REQUIRED, 'Prompt') + ->addOption('connection', null, InputOption::VALUE_OPTIONAL, 'The connection option', 'openai'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + $prompt = $input->getArgument('prompt'); + $connection = $input->getOption('connection'); + + $response = (new Inference)->withConnection($connection)->create( + messages: $prompt, + options: ['stream' => true] + ); + + foreach ($response->stream()->responses() as $response) { + $output->write($response->contentDelta); + } + $output->writeln(''); + + return Command::SUCCESS; + } } \ No newline at end of file diff --git a/src/Features/LLM/InferenceStream.php b/src/Features/LLM/InferenceStream.php index 251948ec..28648412 100644 --- a/src/Features/LLM/InferenceStream.php +++ b/src/Features/LLM/InferenceStream.php @@ -1,146 +1,153 @@ -events = $events ?? new EventDispatcher(); - $this->driver = $driver; - $this->config = $config; - $this->response = $response; - - $this->stream = $this->response->streamContents(); - $this->reader = new EventStreamReader($this->driver->getData(...), $this->events); - } - - /** - * @return Generator - */ - public function responses() : Generator { - foreach ($this->makePartialLLMResponses($this->stream) as $partialLLMResponse) { - yield $partialLLMResponse; - } - } - - /** - * @return PartialLLMResponse[] - */ - public function all() : array { - return $this->getAllPartialLLMResponses($this->stream); - } - - /** - * Returns the last partial response for the stream. - * It will contain accumulated content and finish reason. - * @return ?PartialLLMResponse - */ - public function final() : ?PartialLLMResponse { - return $this->finalResponse($this->stream); - } - - // INTERNAL ////////////////////////////////////////////// - - /** - * @param Generator $partialResponses - * @return ?PartialLLMResponse - */ - protected function finalResponse(Generator $partialResponses) : ?PartialLLMResponse { - $lastPartial = null; - foreach ($partialResponses as $partialResponse) { - $lastPartial = $partialResponse; - } - return $lastPartial; - } - - /** - * @param Generator $stream - * @return PartialLLMResponse[] - */ - protected function getAllPartialLLMResponses(Generator $stream) : array { - $partialResponses = []; - foreach ($this->makePartialLLMResponses($stream) as $partialResponse) { - $partialResponses[] = $partialResponse; - } - return $partialResponses; - } - - /** - * @param Generator $stream - * @return Generator - */ - private function makePartialLLMResponses(Generator $stream) : Generator { - $content = ''; - $finishReason = ''; - foreach ($this->getEventStream($stream) as $streamEvent) { - if ($streamEvent === null || $streamEvent === '') { - continue; - } - $data = Json::decode($streamEvent, []); - $partialResponse = $this->makePartialLLMResponse($data); - if ($partialResponse === null) { - continue; - } - if ($partialResponse->finishReason !== '') { - $finishReason = $partialResponse->finishReason; - } - $content .= $partialResponse->contentDelta; - // add accumulated content and last finish reason - $enrichedResponse = $partialResponse - ->withContent($content) - ->withFinishReason($finishReason); - $this->events->dispatch(new PartialLLMResponseReceived($enrichedResponse)); - yield $enrichedResponse; - } - } - - private function makePartialLLMResponse(array $data) : ?PartialLLMResponse { - return $this->driver->toPartialLLMResponse($data); - } - - /** - * @return Generator - */ - private function getEventStream(Generator $stream) : Generator { - if (!$this->streamReceived) { - foreach($this->streamFromResponse($stream) as $event) { - $this->streamEvents[] = $event; - yield $event; - } - $this->streamReceived = true; - return; - } - reset($this->streamEvents); - yield from $this->streamEvents; - } - - /** - * @return Generator - */ - private function streamFromResponse(Generator $stream) : Generator { - return $this->reader->eventsFrom($stream); - } +events = $events ?? new EventDispatcher(); + $this->driver = $driver; + $this->config = $config; + $this->response = $response; + + $this->stream = $this->response->streamContents(); + $this->reader = new EventStreamReader($this->driver->getData(...), $this->events); + } + + /** + * @return Generator + */ + public function responses() : Generator { + foreach ($this->makePartialLLMResponses($this->stream) as $partialLLMResponse) { + yield $partialLLMResponse; + } + } + + /** + * @return PartialLLMResponse[] + */ + public function all() : array { + return $this->getAllPartialLLMResponses($this->stream); + } + + /** + * Returns the last partial response for the stream. + * It will contain accumulated content and finish reason. + * @return ?LLMResponse + */ + public function final() : ?LLMResponse { + return $this->getFinalResponse($this->stream); + } + + // INTERNAL ////////////////////////////////////////////// + + /** + * @param Generator $stream + * @return ?PartialLLMResponse + */ + protected function getFinalResponse(Generator $stream) : ?LLMResponse { + if ($this->finalLLMResponse === null) { + foreach ($this->makePartialLLMResponses($stream) as $partialResponse) { $tmp = $partialResponse; } + } + return $this->finalLLMResponse; + } + + /** + * @param Generator $stream + * @return PartialLLMResponse[] + */ + protected function getAllPartialLLMResponses(Generator $stream) : array { + if ($this->finalLLMResponse === null) { + foreach ($this->makePartialLLMResponses($stream) as $partialResponse) { $tmp = $partialResponse; } + } + return $this->llmResponses; + } + + /** + * @param Generator $stream + * @return Generator + */ + private function makePartialLLMResponses(Generator $stream) : Generator { + $content = ''; + $finishReason = ''; + $this->llmResponses = []; + $this->lastPartialLLMResponse = null; + + foreach ($this->getEventStream($stream) as $streamEvent) { + if ($streamEvent === null || $streamEvent === '') { + continue; + } + $data = Json::decode($streamEvent, []); + $partialResponse = $this->driver->toPartialLLMResponse($data); + if ($partialResponse === null) { + continue; + } + $this->llmResponses[] = $partialResponse; + + // add accumulated content and last finish reason + if ($partialResponse->finishReason !== '') { + $finishReason = $partialResponse->finishReason; + } + $content .= $partialResponse->contentDelta; + $enrichedResponse = $partialResponse + ->withContent($content) + ->withFinishReason($finishReason); + $this->events->dispatch(new PartialLLMResponseReceived($enrichedResponse)); + + $this->lastPartialLLMResponse = $enrichedResponse; + yield $enrichedResponse; + } + $this->finalLLMResponse = LLMResponse::fromPartialResponses($this->llmResponses); + } + + /** + * @return Generator + */ + private function getEventStream(Generator $stream) : Generator { + if (!$this->streamReceived) { + foreach($this->streamFromResponse($stream) as $event) { + $this->streamEvents[] = $event; + yield $event; + } + $this->streamReceived = true; + return; + } + reset($this->streamEvents); + yield from $this->streamEvents; + } + + /** + * @return Generator + */ + private function streamFromResponse(Generator $stream) : Generator { + return $this->reader->eventsFrom($stream); + } } \ No newline at end of file