Skip to content

Commit

Permalink
Merge pull request #3 from LlmLaraHub/ReplaceWithFunction
Browse files Browse the repository at this point in the history
Get Tools/Functions working on Claude, OpenAi and maybe even Ollama!
  • Loading branch information
alnutile authored Apr 5, 2024
2 parents 4ab9e6f + e36b4b7 commit b9d8e8e
Show file tree
Hide file tree
Showing 51 changed files with 1,515 additions and 159 deletions.
44 changes: 44 additions & 0 deletions app/Events/ChatUiUpdateEvent.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php

namespace App\Events;

use App\Models\Chat;
use App\Models\Collection;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

class ChatUiUpdateEvent implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;

/**
* Create a new event instance.
*/
public function __construct(public Collection $collection, public Chat $chat, public string $updateMessage)
{
//
}

/**
* Get the channels the event should broadcast on.
*
* @return array<int, \Illuminate\Broadcasting\Channel>
*/
public function broadcastOn(): array
{
return [
new PrivateChannel('collection.chat.'.$this->collection->id.'.'.$this->chat->id),
];
}

/**
* The event's broadcast name.
*/
public function broadcastAs(): string
{
return 'update';
}
}
18 changes: 16 additions & 2 deletions app/Http/Controllers/ChatController.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@

namespace App\Http\Controllers;

use App\Domains\Messages\RoleEnum;
use App\Events\ChatUpdatedEvent;
use App\Http\Resources\ChatResource;
use App\Http\Resources\CollectionResource;
use App\Http\Resources\MessageResource;
use App\LlmDriver\Requests\MessageInDto;
use App\Models\Chat;
use App\Models\Collection;
use Facades\App\Domains\Messages\SearchOrSummarizeChatRepo;
use Facades\App\LlmDriver\Orchestrate;

class ChatController extends Controller
{
Expand Down Expand Up @@ -44,7 +46,19 @@ public function chat(Chat $chat)
'input' => 'required|string',
]);

$response = SearchOrSummarizeChatRepo::search($chat, $validated['input']);
$chat->addInput(
message: $validated['input'],
role: RoleEnum::User,
show_in_thread: true);

$messagesArray = [];

$messagesArray[] = MessageInDto::from([
'content' => $validated['input'],
'role' => 'user',
]);

$response = Orchestrate::handle($messagesArray, $chat);

ChatUpdatedEvent::dispatch($chat->chatable, $chat);

Expand Down
15 changes: 15 additions & 0 deletions app/Http/Controllers/CollectionController.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace App\Http\Controllers;

use App\Domains\Documents\StatusEnum;
use App\Domains\Documents\TypesEnum;
use App\Http\Resources\ChatResource;
use App\Http\Resources\CollectionResource;
Expand Down Expand Up @@ -110,4 +111,18 @@ public function filesUpload(Collection $collection)

return back();
}

public function resetCollectionDocument(Collection $collection, Document $document)
{
$document->document_chunks()->delete();
$document->status = StatusEnum::Running;
$document->document_chunk_count = 0;
$document->update();

ProcessFileJob::dispatch($document);

request()->session()->flash('flash.banner', 'Document reset process running!');

return back();
}
}
49 changes: 46 additions & 3 deletions app/LlmDriver/BaseClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,46 @@ public function embedData(string $data): EmbeddingsResponseDto
]);
}

/**
* This is to get functions out of the llm
* if none are returned your system
* can error out or try another way.
*
* @param MessageInDto[] $messages
*/
public function functionPromptChat(array $messages, array $only = []): array
{
if (! app()->environment('testing')) {
sleep(2);
}

Log::info('LlmDriver::MockClient::functionPromptChat', $messages);

$data = get_fixture('openai_response_with_functions_summarize_collection.json');

$functions = [];

foreach (data_get($data, 'choices', []) as $choice) {
foreach (data_get($choice, 'message.toolCalls', []) as $tool) {
if (data_get($tool, 'type') === 'function') {
$name = data_get($tool, 'function.name', null);
if (! in_array($name, $only)) {
$functions[] = [
'name' => $name,
'arguments' => json_decode(data_get($tool, 'function.arguments', []), true),
];
}
}
}
}

/**
* @TODO
* make this a dto
*/
return $functions;
}

/**
* @param MessageInDto[] $messages
*/
Expand All @@ -37,9 +77,7 @@ public function chat(array $messages): CompletionResponse

Log::info('LlmDriver::MockClient::completion');

$data = <<<'EOD'
Voluptate irure cillum dolor anim officia reprehenderit dolor. Eiusmod veniam nostrud consectetur incididunt proident id. Anim adipisicing pariatur amet duis Lorem sunt veniam veniam est. Deserunt ea aliquip cillum pariatur consectetur. Dolor in reprehenderit adipisicing consectetur cupidatat ad cupidatat reprehenderit. Nostrud mollit voluptate aliqua anim pariatur excepteur eiusmod velit quis exercitation tempor quis excepteur.
EOD;
$data = fake()->sentences(3, true);

return new CompletionResponse($data);
}
Expand All @@ -63,4 +101,9 @@ protected function getConfig(string $driver): array
{
return config("llmdriver.drivers.$driver");
}

public function getFunctions(): array
{
return [];
}
}
149 changes: 129 additions & 20 deletions app/LlmDriver/ClaudeClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,26 +38,7 @@ public function chat(array $messages): CompletionResponse
* in betwee a user row with some copy to make it work like "And the user search results had"
* using the Laravel Collection library
*/
$messages = collect($messages)->map(function ($item) {
if ($item->role === 'system') {
$item->role = 'assistant';
}

return $item->toArray();
})->reverse()->values();

$messages = $messages->flatMap(function ($item, $index) use ($messages) {
if ($index > 0 && $item['role'] === 'assistant' && optional($messages->get($index + 1))['role'] === 'assistant') {
return [
$item,
['role' => 'user', 'content' => 'Continuation of search results'],
];
}

return [$item];
})->toArray();

put_fixture('claude_messages_debug.json', $messages);
$messages = $this->remapMessagesForClaude($messages);

$results = $this->getClient()->post('/messages', [
'model' => $model,
Expand Down Expand Up @@ -136,8 +117,136 @@ protected function getClient()

return Http::withHeaders([
'x-api-key' => $api_token,
'anthropic-beta' => 'tools-2024-04-04',
'anthropic-version' => $this->version,
'content-type' => 'application/json',
])->baseUrl($this->baseUrl);
}

/**
* This is to get functions out of the llm
* if none are returned your system
* can error out or try another way.
*
* @param MessageInDto[] $messages
*/
public function functionPromptChat(array $messages, array $only = []): array
{
$messages = $this->remapMessagesForClaude($messages);
Log::info('LlmDriver::ClaudeClient::functionPromptChat', $messages);

$functions = $this->getFunctions();

$model = $this->getConfig('claude')['models']['completion_model'];
$maxTokens = $this->getConfig('claude')['max_tokens'];

$results = $this->getClient()->post('/messages', [
'model' => $model,
'system' => 'Return a markdown response.',
'max_tokens' => $maxTokens,
'messages' => $messages,
'tools' => $this->getFunctions(),
]);

$functions = [];

if (! $results->ok()) {
$error = $results->json()['error']['type'];
$message = $results->json()['error']['message'];
Log::error('Claude API Error ', [
'type' => $error,
'message' => $message,
]);
throw new \Exception('Claude API Error '.$message);
}

$stop_reason = $results->json()['stop_reason'];

if ($stop_reason === 'tool_use') {

foreach ($results->json()['content'] as $content) {
if (data_get($content, 'type') === 'tool_use') {
$functions[] = [
'name' => data_get($content, 'name'),
'arguments' => data_get($content, 'input'),
];
}
}
}

/**
* @TODO
* make this a dto
*/
return $functions;
}

/**
* @NOTE
* Since this abstraction layer is based on OpenAi
* Not much needs to happen here
* but on the others I might need to do XML?
*/
public function getFunctions(): array
{
$functions = LlmDriverFacade::getFunctions();

return collect($functions)->map(function ($function) {
$function = $function->toArray();
$properties = [];
$required = [];

foreach (data_get($function, 'parameters.properties', []) as $property) {
$name = data_get($property, 'name');

if (data_get($property, 'required', false)) {
$required[] = $name;
}

$properties[$name] = [
'description' => data_get($property, 'description', null),
'type' => data_get($property, 'type', 'string'),
'enum' => data_get($property, 'enum', []),
'default' => data_get($property, 'default', null),
];
}

return [
'name' => data_get($function, 'name'),
'description' => data_get($function, 'description'),
'input_schema' => [
'type' => 'object',
'properties' => $properties,
'required' => $required,
],
];
})->toArray();
}

/**
* @param MessageInDto[] $messages
*/
protected function remapMessagesForClaude(array $messages): array
{
$messages = collect($messages)->map(function ($item) {
if ($item->role === 'system') {
$item->role = 'assistant';
}

return $item->toArray();
})->reverse()->values();

$messages = $messages->flatMap(function ($item, $index) use ($messages) {
if ($index > 0 && $item['role'] === 'assistant' && optional($messages->get($index + 1))['role'] === 'assistant') {
return [
$item,
['role' => 'user', 'content' => 'Continuation of search results'],
];
}

return [$item];
})->toArray();

return $messages;
}
}
15 changes: 4 additions & 11 deletions app/LlmDriver/Functions/ArgumentCaster.php
Original file line number Diff line number Diff line change
@@ -1,22 +1,15 @@
<?php

namespace App\LlmDriver;
namespace App\LlmDriver\Functions;

use Spatie\LaravelData\Casts\Cast;
use Spatie\LaravelData\Casts\Castable;
use Spatie\LaravelData\Support\Creation\CreationContext;
use Spatie\LaravelData\Support\DataProperty;

class ArgumentCaster implements Castable
class ArgumentCaster implements Cast
{
public static function dataCastUsing(...$arguments): Cast
public function cast(DataProperty $property, mixed $value, array $properties, CreationContext $context): array
{
return new class implements Cast
{
public function cast(DataProperty $property, mixed $value, array $properties, CreationContext $context): mixed
{
return json_decode($value, true);
}
};
return json_decode($value, true);
}
}
5 changes: 2 additions & 3 deletions app/LlmDriver/Functions/FunctionCallDto.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,12 @@

namespace App\LlmDriver\Functions;

use App\LlmDriver\ArgumentCaster;
use Spatie\LaravelData\Attributes\WithCastable;
use Spatie\LaravelData\Attributes\WithCast;

class FunctionCallDto extends \Spatie\LaravelData\Data
{
public function __construct(
#[WithCastable(ArgumentCaster::class)]
#[WithCast(ArgumentCaster::class)]
public array $arguments,
public string $function_name,
) {
Expand Down
Loading

0 comments on commit b9d8e8e

Please sign in to comment.