diff --git a/src/Command/OllamaEmbedding.php b/src/Command/OllamaChat.php
similarity index 55%
rename from src/Command/OllamaEmbedding.php
rename to src/Command/OllamaChat.php
index 9ec87ea66ace41231f5b655f94c6bcfa53cb7573..656210f4772228125c39fe5b8bf6fd8f14ac0df6 100644
--- a/src/Command/OllamaEmbedding.php
+++ b/src/Command/OllamaChat.php
@@ -5,26 +5,25 @@ declare(strict_types=1);
 namespace Distantmagic\Resonance\Command;
 
 use Distantmagic\Resonance\Attribute\ConsoleCommand;
-use Distantmagic\Resonance\Command;
 use Distantmagic\Resonance\CoroutineCommand;
-use Distantmagic\Resonance\JsonSerializer;
+use Distantmagic\Resonance\OllamaChatSession;
 use Distantmagic\Resonance\OllamaClient;
-use Distantmagic\Resonance\OllamaEmbeddingRequest;
 use Distantmagic\Resonance\SwooleConfiguration;
-use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Helper\QuestionHelper;
 use Symfony\Component\Console\Input\InputInterface;
 use Symfony\Component\Console\Input\InputOption;
 use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\Console\Question\Question;
 
 #[ConsoleCommand(
-    name: 'ollama:embedding',
-    description: 'Generate LLM embedding'
+    name: 'ollama:chat',
+    description: 'Chat with LLM model through Ollama'
 )]
-final class OllamaEmbedding extends CoroutineCommand
+final class OllamaChat extends CoroutineCommand
 {
     public function __construct(
-        private JsonSerializer $jsonSerializer,
-        private OllamaClient $ollamaClient,
+        protected OllamaClient $ollamaClient,
         SwooleConfiguration $swooleConfiguration,
     ) {
         parent::__construct($swooleConfiguration);
@@ -32,7 +31,6 @@ final class OllamaEmbedding extends CoroutineCommand
 
     protected function configure(): void
     {
-        $this->addArgument('prompt', InputArgument::REQUIRED);
         $this->addOption(
             default: 'mistral',
             mode: InputOption::VALUE_REQUIRED,
@@ -48,25 +46,25 @@ final class OllamaEmbedding extends CoroutineCommand
         $model = $input->getOption('model');
 
         /**
-         * @var string $prompt
+         * @var QuestionHelper $helper
          */
-        $prompt = $input->getArgument('prompt');
+        $helper = $this->getHelper('question');
+        $userInputQuestion = new Question('> ');
 
-        $embeddingRequest = new OllamaEmbeddingRequest(
+        $chatSession = new OllamaChatSession(
             model: $model,
-            prompt: $prompt,
+            ollamaClient: $this->ollamaClient,
         );
 
-        $embeddingResponse = $this
-            ->ollamaClient
-            ->generateEmbedding($embeddingRequest)
-        ;
+        while (true) {
+            $userMessageContent = $helper->ask($input, $output, $userInputQuestion);
 
-        $output->writeln(
-            $this
-                ->jsonSerializer
-                ->serialize($embeddingResponse)
-        );
+            foreach ($chatSession->respond($userMessageContent) as $value) {
+                $output->write((string) $value);
+            }
+
+            $output->writeln('');
+        }
 
         return Command::SUCCESS;
     }
diff --git a/src/Command/OllamaCompletion.php b/src/Command/OllamaGenerate.php
similarity index 64%
rename from src/Command/OllamaCompletion.php
rename to src/Command/OllamaGenerate.php
index 0d7e5e6d932b13e4c4b3a3264014b6621a0e0727..9cbc8bb10776f23b9500c011b862dd44361b8057 100644
--- a/src/Command/OllamaCompletion.php
+++ b/src/Command/OllamaGenerate.php
@@ -4,25 +4,20 @@ declare(strict_types=1);
 
 namespace Distantmagic\Resonance\Command;
 
-use Distantmagic\Resonance\Attribute\ConsoleCommand;
-use Distantmagic\Resonance\Command;
 use Distantmagic\Resonance\CoroutineCommand;
 use Distantmagic\Resonance\OllamaClient;
-use Distantmagic\Resonance\OllamaCompletionRequest;
 use Distantmagic\Resonance\SwooleConfiguration;
 use Symfony\Component\Console\Input\InputArgument;
 use Symfony\Component\Console\Input\InputInterface;
 use Symfony\Component\Console\Input\InputOption;
 use Symfony\Component\Console\Output\OutputInterface;
 
-#[ConsoleCommand(
-    name: 'ollama:completion',
-    description: 'Generate LLM completion'
-)]
-final class OllamaCompletion extends CoroutineCommand
+abstract class OllamaGenerate extends CoroutineCommand
 {
+    abstract protected function executeOllamaCommand(InputInterface $input, OutputInterface $output, string $model, string $prompt): int;
+
     public function __construct(
-        private OllamaClient $ollamaClient,
+        protected OllamaClient $ollamaClient,
         SwooleConfiguration $swooleConfiguration,
     ) {
         parent::__construct($swooleConfiguration);
@@ -50,15 +45,6 @@ final class OllamaCompletion extends CoroutineCommand
          */
         $prompt = $input->getArgument('prompt');
 
-        $completionRequest = new OllamaCompletionRequest(
-            model: $model,
-            prompt: $prompt,
-        );
-
-        foreach ($this->ollamaClient->generateCompletion($completionRequest) as $token) {
-            $output->write($token);
-        }
-
-        return Command::SUCCESS;
+        return $this->executeOllamaCommand($input, $output, $model, $prompt);
     }
 }
diff --git a/src/Command/OllamaGenerate/Completion.php b/src/Command/OllamaGenerate/Completion.php
new file mode 100644
index 0000000000000000000000000000000000000000..9d9d523b0b20146a572e250914682f846a1e10a7
--- /dev/null
+++ b/src/Command/OllamaGenerate/Completion.php
@@ -0,0 +1,33 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Distantmagic\Resonance\Command\OllamaGenerate;
+
+use Distantmagic\Resonance\Attribute\ConsoleCommand;
+use Distantmagic\Resonance\Command;
+use Distantmagic\Resonance\Command\OllamaGenerate;
+use Distantmagic\Resonance\OllamaCompletionRequest;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+
+#[ConsoleCommand(
+    name: 'ollama:completion',
+    description: 'Generate LLM completion'
+)]
+final class Completion extends OllamaGenerate
+{
+    protected function executeOllamaCommand(InputInterface $input, OutputInterface $output, string $model, string $prompt): int
+    {
+        $completionRequest = new OllamaCompletionRequest(
+            model: $model,
+            prompt: $prompt,
+        );
+
+        foreach ($this->ollamaClient->generateCompletion($completionRequest) as $token) {
+            $output->write((string) $token);
+        }
+
+        return Command::SUCCESS;
+    }
+}
diff --git a/src/Command/OllamaGenerate/Embedding.php b/src/Command/OllamaGenerate/Embedding.php
new file mode 100644
index 0000000000000000000000000000000000000000..9c5ec24e0393b0b3a5f8cbc51096d1f4024c07de
--- /dev/null
+++ b/src/Command/OllamaGenerate/Embedding.php
@@ -0,0 +1,51 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Distantmagic\Resonance\Command\OllamaGenerate;
+
+use Distantmagic\Resonance\Attribute\ConsoleCommand;
+use Distantmagic\Resonance\Command;
+use Distantmagic\Resonance\Command\OllamaGenerate;
+use Distantmagic\Resonance\JsonSerializer;
+use Distantmagic\Resonance\OllamaClient;
+use Distantmagic\Resonance\OllamaEmbeddingRequest;
+use Distantmagic\Resonance\SwooleConfiguration;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+
+#[ConsoleCommand(
+    name: 'ollama:embedding',
+    description: 'Generate LLM embedding'
+)]
+final class Embedding extends OllamaGenerate
+{
+    public function __construct(
+        private JsonSerializer $jsonSerializer,
+        OllamaClient $ollamaClient,
+        SwooleConfiguration $swooleConfiguration,
+    ) {
+        parent::__construct($ollamaClient, $swooleConfiguration);
+    }
+
+    protected function executeOllamaCommand(InputInterface $input, OutputInterface $output, string $model, string $prompt): int
+    {
+        $embeddingRequest = new OllamaEmbeddingRequest(
+            model: $model,
+            prompt: $prompt,
+        );
+
+        $embeddingResponse = $this
+            ->ollamaClient
+            ->generateEmbedding($embeddingRequest)
+        ;
+
+        $output->writeln(
+            $this
+                ->jsonSerializer
+                ->serialize($embeddingResponse)
+        );
+
+        return Command::SUCCESS;
+    }
+}
diff --git a/src/OllamaChatMessage.php b/src/OllamaChatMessage.php
new file mode 100644
index 0000000000000000000000000000000000000000..a50091df4fb7d6b3dca77af61b212f209a7c8811
--- /dev/null
+++ b/src/OllamaChatMessage.php
@@ -0,0 +1,29 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Distantmagic\Resonance;
+
+use JsonSerializable;
+use Stringable;
+
+readonly class OllamaChatMessage implements JsonSerializable, Stringable
+{
+    public function __construct(
+        public string $content,
+        public OllamaChatRole $role,
+    ) {}
+
+    public function __toString(): string
+    {
+        return $this->content;
+    }
+
+    public function jsonSerialize(): array
+    {
+        return [
+            'content' => $this->content,
+            'role' => $this->role->value,
+        ];
+    }
+}
diff --git a/src/OllamaChatRequest.php b/src/OllamaChatRequest.php
new file mode 100644
index 0000000000000000000000000000000000000000..c87c64eb5e86f2c77b95eb697e8fd54a01136970
--- /dev/null
+++ b/src/OllamaChatRequest.php
@@ -0,0 +1,30 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Distantmagic\Resonance;
+
+use JsonSerializable;
+
+readonly class OllamaChatRequest implements JsonSerializable
+{
+    /**
+     * @param array<OllamaChatMessage> $messages
+     */
+    public function __construct(
+        public string $model,
+        public array $messages,
+        public OllamaRequestOptions $options = new OllamaRequestOptions(),
+    ) {}
+
+    public function jsonSerialize(): array
+    {
+        return [
+            'model' => $this->model,
+            'messages' => $this->messages,
+            'options' => $this->options,
+            'raw' => true,
+            'stream' => true,
+        ];
+    }
+}
diff --git a/src/OllamaChatRole.php b/src/OllamaChatRole.php
new file mode 100644
index 0000000000000000000000000000000000000000..0a66628a5d3c0c6245891814c391387b0590eb9f
--- /dev/null
+++ b/src/OllamaChatRole.php
@@ -0,0 +1,12 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Distantmagic\Resonance;
+
+enum OllamaChatRole: string
+{
+    case Assistant = 'assistant';
+    case System = 'system';
+    case User = 'user';
+}
diff --git a/src/OllamaChatSession.php b/src/OllamaChatSession.php
new file mode 100644
index 0000000000000000000000000000000000000000..2ff877d706ff477fe8a31ad9645035ecf250c96b
--- /dev/null
+++ b/src/OllamaChatSession.php
@@ -0,0 +1,41 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Distantmagic\Resonance;
+
+use Ds\Set;
+use Generator;
+
+readonly class OllamaChatSession
+{
+    /**
+     * @var Set<OllamaChatMessage>
+     */
+    private Set $messages;
+
+    public function __construct(
+        public string $model,
+        public OllamaClient $ollamaClient,
+    ) {
+        $this->messages = new Set();
+    }
+
+    /**
+     * @return Generator<OllamaChatToken>
+     */
+    public function respond(string $userMessageContent): Generator
+    {
+        $this
+            ->messages
+            ->add(new OllamaChatMessage($userMessageContent, OllamaChatRole::User))
+        ;
+
+        $chatRequest = new OllamaChatRequest(
+            model: $this->model,
+            messages: $this->messages->toArray(),
+        );
+
+        yield from $this->ollamaClient->generateChatCompletion($chatRequest);
+    }
+}
diff --git a/src/OllamaChatToken.php b/src/OllamaChatToken.php
new file mode 100644
index 0000000000000000000000000000000000000000..9e77141a0516c9dfc2be26394fd67ff9b47cf67f
--- /dev/null
+++ b/src/OllamaChatToken.php
@@ -0,0 +1,21 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Distantmagic\Resonance;
+
+use DateTimeImmutable;
+use Stringable;
+
+readonly class OllamaChatToken implements Stringable
+{
+    public function __construct(
+        public DateTimeImmutable $createdAt,
+        public OllamaChatMessage $message,
+    ) {}
+
+    public function __toString(): string
+    {
+        return (string) $this->message;
+    }
+}
diff --git a/src/OllamaClient.php b/src/OllamaClient.php
index 28a2fb85207e6990d7571022882cd10fbf0642d6..2d351aa0f4c33307ec27ed3920db1ccbe5a3ed45 100644
--- a/src/OllamaClient.php
+++ b/src/OllamaClient.php
@@ -5,8 +5,11 @@ declare(strict_types=1);
 namespace Distantmagic\Resonance;
 
 use CurlHandle;
+use DateTimeImmutable;
 use Distantmagic\Resonance\Attribute\Singleton;
 use Generator;
+use JsonSerializable;
+use Psr\Log\LoggerInterface;
 use RuntimeException;
 use Swoole\Coroutine\Channel;
 
@@ -17,6 +20,7 @@ readonly class OllamaClient
 
     public function __construct(
         private JsonSerializer $jsonSerializer,
+        private LoggerInterface $logger,
         private OllamaLinkBuilder $ollamaLinkBuilder,
     ) {
         $this->ch = curl_init();
@@ -33,53 +37,56 @@ readonly class OllamaClient
     }
 
     /**
-     * @return Generator<string>
+     * @return Generator<OllamaChatToken>
      */
-    public function generateCompletion(OllamaCompletionRequest $request): Generator
+    public function generateChatCompletion(OllamaChatRequest $request): Generator
     {
-        $channel = new Channel(1);
-        $requestData = json_encode($request);
-
-        $cid = go(function () use ($channel, $requestData) {
-            try {
-                curl_setopt($this->ch, CURLOPT_URL, $this->ollamaLinkBuilder->build('/api/generate'));
-                curl_setopt($this->ch, CURLOPT_POSTFIELDS, $requestData);
-                curl_setopt($this->ch, CURLOPT_RETURNTRANSFER, false);
-                curl_setopt($this->ch, CURLOPT_WRITEFUNCTION, function (CurlHandle $ch, string $data) use ($channel) {
-                    if (!empty($data)) {
-                        $channel->push(
-                            $this
-                                ->jsonSerializer
-                                ->unserialize($data)
-                        );
-                    }
-
-                    return strlen($data);
-                });
-
-                if (!curl_exec($this->ch)) {
-                    throw new CurlException($this->ch);
-                }
+        $channel = $this->streamJson($request, '/api/chat');
 
-                $this->assertStatusCode(200);
-            } finally {
-                curl_setopt($this->ch, CURLOPT_WRITEFUNCTION, null);
+        /**
+         * @var SwooleChannelIterator<object{ error: string }|object{
+         *   created_at: string,
+         *   message: object{
+         *     content: string,
+         *     role: string,
+         *   },
+         *   response: string,
+         * }>
+         */
+        $swooleChannelIterator = new SwooleChannelIterator($channel);
 
-                $channel->close();
+        foreach ($swooleChannelIterator as $data) {
+            if (isset($data->error)) {
+                $this->logger->error($data->error);
+            } else {
+                yield new OllamaChatToken(
+                    createdAt: new DateTimeImmutable($data->created_at),
+                    message: new OllamaChatMessage(
+                        content: $data->message->content,
+                        role: OllamaChatRole::from($data->message->role),
+                    )
+                );
             }
-        });
-
-        if (!is_int($cid)) {
-            throw new RuntimeException('Unable to start a coroutine');
         }
+    }
+
+    /**
+     * @return Generator<OllamaCompletionToken>
+     */
+    public function generateCompletion(OllamaCompletionRequest $request): Generator
+    {
+        $channel = $this->streamJson($request, '/api/generate');
 
         /**
-         * @var SwooleChannelIterator<object{ response: string }>
+         * @var SwooleChannelIterator<object{ created_at: string, response: string }>
          */
         $swooleChannelIterator = new SwooleChannelIterator($channel);
 
         foreach ($swooleChannelIterator as $token) {
-            yield $token->response;
+            yield new OllamaCompletionToken(
+                createdAt: new DateTimeImmutable($token->created_at),
+                response: $token->response,
+            );
         }
     }
 
@@ -129,4 +136,49 @@ readonly class OllamaClient
             $statusCode,
         ));
     }
+
+    private function streamJson(JsonSerializable $request, string $path): Channel
+    {
+        $channel = new Channel(1);
+        $requestData = json_encode($request);
+
+        $cid = go(function () use ($channel, $path, $requestData) {
+            try {
+                curl_setopt($this->ch, CURLOPT_URL, $this->ollamaLinkBuilder->build($path));
+                curl_setopt($this->ch, CURLOPT_POSTFIELDS, $requestData);
+                curl_setopt($this->ch, CURLOPT_RETURNTRANSFER, false);
+                curl_setopt($this->ch, CURLOPT_WRITEFUNCTION, function (CurlHandle $ch, string $data) use ($channel) {
+                    $dataChunks = explode("\n", $data);
+
+                    foreach ($dataChunks as $dataChunk) {
+                        if (!empty($dataChunk)) {
+                            $channel->push(
+                                $this
+                                    ->jsonSerializer
+                                    ->unserialize($dataChunk)
+                            );
+                        }
+                    }
+
+                    return strlen($data);
+                });
+
+                if (!curl_exec($this->ch)) {
+                    throw new CurlException($this->ch);
+                }
+
+                $this->assertStatusCode(200);
+            } finally {
+                curl_setopt($this->ch, CURLOPT_WRITEFUNCTION, null);
+
+                $channel->close();
+            }
+        });
+
+        if (!is_int($cid)) {
+            throw new RuntimeException('Unable to start a coroutine');
+        }
+
+        return $channel;
+    }
 }
diff --git a/src/OllamaCompletionToken.php b/src/OllamaCompletionToken.php
new file mode 100644
index 0000000000000000000000000000000000000000..bb4947d21d85606b584779ef96a0934fd9568546
--- /dev/null
+++ b/src/OllamaCompletionToken.php
@@ -0,0 +1,21 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Distantmagic\Resonance;
+
+use DateTimeImmutable;
+use Stringable;
+
+readonly class OllamaCompletionToken implements Stringable
+{
+    public function __construct(
+        public DateTimeImmutable $createdAt,
+        public string $response,
+    ) {}
+
+    public function __toString(): string
+    {
+        return $this->response;
+    }
+}
diff --git a/src/OllamaRequestOptions.php b/src/OllamaRequestOptions.php
index 0f8136076975daf355025334ad47e39ccb6a7e4f..24a5a62c10d4dd624283fdb4cc97d5d82432b638 100644
--- a/src/OllamaRequestOptions.php
+++ b/src/OllamaRequestOptions.php
@@ -9,7 +9,8 @@ use JsonSerializable;
 readonly class OllamaRequestOptions implements JsonSerializable
 {
     public function __construct(
-        public float $temperature = 0.8,
+        public float $numPredict = -1,
+        public float $temperature = 0.5,
         public OllamaRequestStopDelimiter $stopDelimiter = new OllamaRequestStopDelimiter(),
     ) {}
 
@@ -17,6 +18,7 @@ readonly class OllamaRequestOptions implements JsonSerializable
     {
         $ret = [];
 
+        $ret['num_predict'] = $this->numPredict;
         $ret['stop'] = $this->stopDelimiter;
         $ret['temperature'] = $this->temperature;