Skip to content
This repository was archived by the owner on Mar 21, 2024. It is now read-only.

Commit 4d5688c

Browse files
committed
Let's go
0 parents  commit 4d5688c

File tree

10 files changed

+474
-0
lines changed

10 files changed

+474
-0
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
vendor/
2+
.idea/
3+
.DS_Store

README.md

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
![HuggingChat + PHP](logo.png)
2+
3+
# HuggingChat client
4+
5+
[![Latest Stable Version](https://img.shields.io/github/v/release/maximerenou/php-hugging-chat)](https://packagist.org/packages/maximerenou/hugging-chat)
6+
[![PHP version](https://img.shields.io/packagist/dependency-v/maximerenou/hugging-chat/php)](https://packagist.org/packages/maximerenou/hugging-chat)
7+
[![cURL extension required](https://img.shields.io/packagist/dependency-v/maximerenou/hugging-chat/ext-curl)](https://packagist.org/packages/maximerenou/hugging-chat)
8+
9+
This is an unofficial Composer package for using **HuggingChat** (OpenAssistant's LLaMA model).
10+
11+
## Installation
12+
13+
composer require maximerenou/hugging-chat
14+
15+
## Usage
16+
17+
**Demo**: run `examples/chat.php` to test it.
18+
19+
```php
20+
use MaximeRenou\HuggingChat\Client as HuggingChat;
21+
use MaximeRenou\HuggingChat\Prompt;
22+
23+
$ai = new HuggingChat();
24+
25+
$conversation = $ai->createConversation();
26+
27+
// $answer - full answer
28+
$answer = $conversation->ask(new Prompt("Hello World"));
29+
```
30+
31+
<details>
32+
<summary>Real-time / progressive answer</summary>
33+
34+
You may pass a function as second argument to get real-time progression:
35+
36+
```php
37+
// $current_answer - incomplete answer
38+
// $tokens - last tokens received
39+
$final_answer = $conversation->ask($prompt, function ($current_answer, $tokens) {
40+
echo $tokens;
41+
});
42+
```
43+
44+
</details>
45+
46+
<details>
47+
<summary>Resume a conversation</summary>
48+
49+
If you want to resume a previous conversation, you can retrieve its identifiers:
50+
51+
```php
52+
// Get current identifiers
53+
$identifiers = $conversation->getIdentifiers();
54+
55+
// ...
56+
// Resume conversation with $identifiers parameter
57+
$conversation = $ai->resumeChatConversation($identifiers);
58+
```
59+
60+
</details>
61+
62+
<details>
63+
<summary>Handle HuggingChat errors</summary>
64+
65+
The code throws exceptions when it receives an error from HuggingChat. You can therefore use a try/catch block to handle errors.
66+
67+
</details>
68+
69+
<details>
70+
<summary>Answers are sometimes malformed (or dumb)</summary>
71+
72+
That's what OpenAssistant's LLaMA model used by HuggingChat generates...
73+
74+
</details>
75+
76+
---------------------------------------
77+
78+
#### Disclaimer
79+
80+
Using HuggingChat outside huggingface.co/chat may violate HuggingFace terms. Use it at your own risk.

composer.json

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"name": "maximerenou/hugging-chat",
3+
"description": "HuggingChat client",
4+
"type": "library",
5+
"autoload": {
6+
"psr-4": {
7+
"MaximeRenou\\HuggingChat\\": "src/"
8+
}
9+
},
10+
"authors": [
11+
{
12+
"name": "Maxime Renou",
13+
"email": "[email protected]"
14+
}
15+
],
16+
"require": {
17+
"php": ">=7.1",
18+
"ext-curl": "*",
19+
"eislambey/eventsource": "^0.1.0"
20+
}
21+
}

composer.lock

Lines changed: 69 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

examples/chat.php

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
require __DIR__ . '/../vendor/autoload.php';
3+
4+
\MaximeRenou\HuggingChat\Tools::$debug = false; // Set true for verbose
5+
6+
$ai = new \MaximeRenou\HuggingChat\Client();
7+
8+
$conversation = $ai->createConversation();
9+
10+
echo 'Type "q" to quit' . PHP_EOL;
11+
12+
while (true) {
13+
echo PHP_EOL . "> ";
14+
15+
$text = rtrim(fgets(STDIN));
16+
17+
if ($text == 'q')
18+
break;
19+
20+
$prompt = new \MaximeRenou\HuggingChat\Prompt($text);
21+
22+
echo "-";
23+
24+
try {
25+
$full_answer = $conversation->ask($prompt, function ($answer, $tokens) use (&$padding) {
26+
echo $tokens;
27+
});
28+
}
29+
catch (\Exception $exception) {
30+
echo " Sorry, something went wrong: {$exception->getMessage()}.";
31+
}
32+
}
33+
34+
exit(0);

logo.png

6.99 KB
Loading

src/Client.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
namespace MaximeRenou\HuggingChat;
4+
5+
class Client
6+
{
7+
public function createConversation($identifiers = null)
8+
{
9+
return $this->resumeConversation($identifiers);
10+
}
11+
12+
public function resumeConversation($identifiers)
13+
{
14+
return new Conversation($identifiers);
15+
}
16+
}

src/Conversation.php

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
<?php
2+
3+
namespace MaximeRenou\HuggingChat;
4+
5+
use EventSource\Event;
6+
use EventSource\EventSource;
7+
8+
class Conversation
9+
{
10+
const END_CHAR = '<|endoftext|>';
11+
12+
// Conversation IDs
13+
public $id;
14+
public $cookie;
15+
16+
// Conversation data
17+
protected $current_started;
18+
protected $current_text;
19+
20+
public function __construct($identifiers = null)
21+
{
22+
if (is_array($identifiers) && ! empty($identifiers['cookie']))
23+
$this->cookie = $identifiers['cookie'];
24+
25+
if (! is_array($identifiers))
26+
$identifiers = $this->initConversation();
27+
28+
$this->id = $identifiers['id'];
29+
$this->cookie = $identifiers['cookie'];
30+
}
31+
32+
public function getIdentifiers()
33+
{
34+
return [
35+
'id' => $this->id,
36+
'cookie' => $this->cookie
37+
];
38+
}
39+
40+
public function initConversation()
41+
{
42+
$headers = [
43+
'method: POST',
44+
'accept: application/json',
45+
"referer: https://huggingface.co/chat",
46+
'content-type: application/json',
47+
];
48+
49+
if (! empty($this->cookie)) {
50+
$headers[] = "cookie: hf-chat={$this->cookie}";
51+
}
52+
53+
list($data, $request, $url, $cookies) = Tools::request("https://huggingface.co/chat/conversation", $headers, '', true);
54+
$data = json_decode($data, true);
55+
56+
if (! empty($cookies['hf-chat'])) {
57+
$this->cookie = $cookies['hf-chat'];
58+
}
59+
60+
if (! is_array($data) || empty($data['conversationId']))
61+
throw new \Exception("Failed to init conversation");
62+
63+
return [
64+
'id' => $data['conversationId'],
65+
'cookie' => $this->cookie
66+
];
67+
}
68+
69+
public function ask(Prompt $message, $callback = null)
70+
{
71+
$this->current_text = '';
72+
73+
$es = new EventSource("https://huggingface.co/chat/conversation/{$this->id}");
74+
75+
$data = [
76+
'inputs' => $message->text,
77+
'options' => [
78+
'use_cache' => $message->cache
79+
],
80+
'parameters' => [
81+
'max_new_tokens' => $message->max_new_tokens,
82+
'repetition_penalty' => $message->repetition_penalty,
83+
'return_full_text' => $message->return_full_text,
84+
'stop' => [self::END_CHAR],
85+
'temperature' => $message->temperature,
86+
'top_k' => $message->top_k,
87+
'top_p' => $message->top_p,
88+
'truncate' => $message->truncate,
89+
'watermark' => $message->watermark,
90+
],
91+
'stream' => true
92+
];
93+
94+
$es->setCurlOptions([
95+
CURLOPT_HTTPHEADER => [
96+
'method: POST',
97+
'accept: */*',
98+
"referer: https://huggingface.co/chat/conversation/{$this->id}",
99+
'content-type: application/json',
100+
"cookie: hf-chat={$this->cookie}"
101+
],
102+
CURLOPT_POST => 1,
103+
CURLOPT_POSTFIELDS => json_encode($data)
104+
]);
105+
106+
$es->onMessage(function (Event $event) use ($es, &$callback) {
107+
if ($es === 4) {
108+
$es->abort();
109+
}
110+
111+
$message = $this->handlePacket($event->data);
112+
113+
if ($message === false)
114+
return;
115+
116+
$tokens = $message['text'];
117+
118+
if ($message['final']) {
119+
$offset = strlen($this->current_text);
120+
$this->current_text = $tokens;
121+
$tokens = substr($tokens, $offset);
122+
}
123+
else {
124+
$this->current_text .= $tokens;
125+
}
126+
127+
if (($pos = strpos($this->current_text, self::END_CHAR)) !== false) {
128+
$this->current_text = substr($this->current_text, 0, $pos);
129+
}
130+
131+
$callback($this->current_text, $tokens);
132+
});
133+
134+
@$es->connect();
135+
136+
return $this->current_text;
137+
}
138+
139+
public function handlePacket($raw)
140+
{
141+
$data = json_decode($raw, true);
142+
143+
if (! $data) {
144+
return false;
145+
}
146+
147+
if (empty($data['token'])) {
148+
Tools::debug("Drop: $raw");
149+
150+
if (! empty($data['error'])) {
151+
throw new \Exception($data['error']);
152+
}
153+
154+
return false;
155+
}
156+
157+
$text = $data['token']['special'] ? $data['generated_text'] : $data['token']['text'];
158+
159+
if (($pos = strpos($text, self::END_CHAR)) !== false) {
160+
$text = substr($text, 0, $pos);
161+
}
162+
163+
return [
164+
'text' => $text,
165+
'final' => $data['token']['special']
166+
];
167+
}
168+
}

0 commit comments

Comments
 (0)