├── conversations.php
├── new_chat.php
├── index.html
├── create_title.php
├── load_chat.php
├── php-gpt-pdfread
├── pdfread.php
├── lookup.php
└── gpt.php
├── README.md
├── send_message.php
├── style.css
├── get_response.php
└── ChatGPT.php
/conversations.php:
--------------------------------------------------------------------------------
1 | '.htmlspecialchars( $chat["name"] ).'';
8 | }
--------------------------------------------------------------------------------
/new_chat.php:
--------------------------------------------------------------------------------
1 | "Untitled chat",
10 | "id" => $chat_id,
11 | "messages" => [],
12 | ];
13 |
14 | file_put_contents(
15 | "chats/" . $chat_id . ".json",
16 | json_encode( $chat )
17 | );
18 |
19 | echo '';
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | GPT-FileChat
8 |
9 |
10 |
11 |
12 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/create_title.php:
--------------------------------------------------------------------------------
1 | smessage(
14 | "You are a conversation title generator. Respond only with a simple title."
15 | );
16 | $chatgpt->umessage( "Please create a title for this conversation:\nQ: " . $user_message['content'] . "\nA: " . $assistant_message['content'] );
17 |
18 | $response = (array)$chatgpt->response();
19 |
20 | $title = trim( $response["content"], '"' );
21 |
22 | $chat["name"] = $title;
23 |
24 | file_put_contents(
25 | "chats/" . basename( $_GET['chat_id'] ) . ".json",
26 | json_encode( $chat )
27 | );
28 |
29 | echo $title;
--------------------------------------------------------------------------------
/load_chat.php:
--------------------------------------------------------------------------------
1 | ';
8 | foreach( $chat["messages"] as $message ) {
9 | echo ''.nl2br( htmlspecialchars( $message["content"] ) ).'
';
10 | }
11 | echo '';
12 |
13 | ?>
14 |
--------------------------------------------------------------------------------
/php-gpt-pdfread/pdfread.php:
--------------------------------------------------------------------------------
1 | $chunk_size ) {
15 | throw new \Exception( "Overlap must be smaller than chunk size" );
16 | }
17 |
18 | $chunks = [];
19 |
20 | $file = fopen( $filename, "r" );
21 | while( ! feof( $file ) ) {
22 | $chunk = fread( $file, $chunk_size );
23 | $chunks[] = $chunk;
24 | if( feof( $file ) ) {
25 | break;
26 | }
27 | fseek( $file, -$overlap, SEEK_CUR );
28 | }
29 |
30 | return $chunks;
31 | }
32 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # GPT-FileChat
2 |
3 | This simple chatbot made with HTMX, PHP and the ChatGPT API allows you to upload your own files (PDFs or text files) and ask questions about them.
4 |
5 | It uses a very simple method of asking ChatGPT for keywords related to the question and then finding the part in the text which is most relevant to those keywords. This part is then passed to ChatGPT, which will attempt to answer the question based on that.
6 |
7 | ## NOT for Production!
8 |
9 | **WARNING:** The chatbot allows for uploading of ANY files, so don't deploy it on a public server unless you implement protection against uploading unwanted files or move the uploads directory outside a public directory. Someone might upload a PHP file and run arbitrary code on your server!
10 |
11 | ## How to use
12 |
13 | ```console
14 | $ export OPENAI_API_KEY=YOUR_API_KEY
15 | $ php -S localhost:8080
16 | ```
17 |
18 | ## Dependencies
19 |
20 | It uses `pdftotext` to convert PDF files into text files. You need to install `poppler-utils`:
21 |
22 | ```console
23 | $ sudo apt install poppler-utils
24 | ```
25 |
--------------------------------------------------------------------------------
/php-gpt-pdfread/lookup.php:
--------------------------------------------------------------------------------
1 | $chunk ) {
6 | foreach( $keywords as $keyword ) {
7 | $chunk = strtolower( $chunk );
8 | $chunks[$chunk_id] = $chunk;
9 | $occurences = substr_count( $chunk, $keyword );
10 | if( ! isset( $df[$keyword] ) ) {
11 | $df[$keyword] = 0;
12 | }
13 | $df[$keyword] += $occurences;
14 | }
15 | }
16 |
17 | $results = [];
18 |
19 | foreach( $chunks as $chunk_id => $chunk ) {
20 | foreach( $keywords as $keyword ) {
21 | if( $chunk_id != 0 ) {
22 | $chunk = substr( $chunk, $crop );
23 | }
24 | if( $chunk_id != count( $chunks ) - 1 ) {
25 | $chunk = substr( $chunk, 0, -$crop );
26 | }
27 | $occurences = substr_count( $chunk, $keyword );
28 | if( ! isset( $results[$chunk_id] ) ) {
29 | $results[$chunk_id] = 0;
30 | }
31 | if( isset( $df[$keyword] ) && $df[$keyword] > 0 ) {
32 | $results[$chunk_id] += $occurences / $df[$keyword];
33 | }
34 | }
35 | }
36 |
37 | arsort( $results );
38 |
39 | return $results;
40 | }
--------------------------------------------------------------------------------
/send_message.php:
--------------------------------------------------------------------------------
1 | "user",
33 | "content" => $message
34 | ];
35 |
36 | // save message history
37 | file_put_contents(
38 | "chats/" . basename( $_POST['chat_id'] ) . ".json",
39 | json_encode( $chat )
40 | );
41 |
42 | // render message and get response
43 | echo ''.nl2br( htmlspecialchars( $message ) ).'
';
44 |
45 | // reset message form
46 | echo '';
47 |
--------------------------------------------------------------------------------
/style.css:
--------------------------------------------------------------------------------
1 | html, body {
2 | width: 100%;
3 | height: 100%;
4 | margin: 0;
5 | padding: 0;
6 | }
7 |
8 | body {
9 | font-family: sans-serif;
10 | display: flex;
11 | }
12 |
13 | nav {
14 | width: 300px;
15 | overflow-y: auto;
16 | padding: 20px;
17 | box-sizing: border-box;
18 | background: #1f1a1a;
19 | color: #fff;
20 | height: 100%;
21 | }
22 |
23 | nav button {
24 | background: #fff;
25 | color: #000;
26 | border-radius: 15px;
27 | width: 100%;
28 | box-sizing: border-box;
29 | padding: 10px;
30 | text-align: center;
31 | overflow: hidden;
32 | text-overflow: ellipsis;
33 | white-space: nowrap;
34 | }
35 |
36 | nav button:hover {
37 | background: #d4d4d4;
38 | }
39 |
40 | main {
41 | display: flex;
42 | flex-direction: column;
43 | width: 100%;
44 | height: 100%;
45 | padding: 20px;
46 | box-sizing: border-box;
47 | }
48 |
49 | main .message-div {
50 | display: flex;
51 | height: 50px;
52 | }
53 |
54 | #upload-file {
55 | margin-bottom: 10px;
56 | margin-top: 10px;
57 | }
58 |
59 | main form input[type="text"] {
60 | border-radius: 10px;
61 | border: 1px solid #ccc;
62 | padding: 10px;
63 | box-sizing: border-box;
64 | font-family: inherit;
65 | margin-right: 10px;
66 | flex: 1;
67 | }
68 |
69 | main form button {
70 | width: 100px;
71 | padding: 10px;
72 | box-sizing: border-box;
73 | border-radius: 10px;
74 | background: #0b6a04;
75 | color: #fff;
76 | border: none;
77 | }
78 |
79 | .messages {
80 | overflow-y: auto;
81 | flex: 1;
82 | }
83 |
84 | .message {
85 | border-radius: 15px;
86 | padding: 15px;
87 | background: #f4f4f4;
88 | margin-bottom: 15px;
89 | }
90 |
91 | .assistant.message {
92 | background: #d6e6d5;
93 | }
--------------------------------------------------------------------------------
/get_response.php:
--------------------------------------------------------------------------------
1 | $points ) {
23 | if( $chunk_number++ > $limit ) {
24 | break;
25 | }
26 |
27 | $answer = answer_question( $chunks[$chunk_id], $question );
28 |
29 | if( isset( $answer->name ) && $answer->name == "give_response" ) {
30 | $arguments = json_decode( $answer->arguments, true );
31 | $response = $arguments["response"];
32 | return json_encode([
33 | "status" => "OK",
34 | "response" => $response,
35 | ]);
36 | }
37 | }
38 |
39 | if( ! isset( $answer->name ) || $answer->name != "give_response" ) {
40 | return json_encode([
41 | "status" => "FAIL",
42 | "response" => "Unable to find answer",
43 | ]);
44 | }
45 | }
46 |
47 | $chat = json_decode(
48 | file_get_contents( "chats/" . basename( $_GET['chat_id'] ) . ".json" ),
49 | true,
50 | );
51 |
52 | $message = end( $chat["messages"] );
53 |
54 | $chatgpt = new ChatGPT( getenv("OPENAI_API_KEY"), $_GET['chat_id'] );
55 | $chatgpt->loadfunction( function( $chat_id ) use ( $chat ) {
56 | return $chat["messages"];
57 | } );
58 | $chatgpt->load();
59 | $chatgpt->add_function( "answer_question_from_file" );
60 | $chatgpt->umessage( $message["content"] );
61 |
62 | $response = (array)$chatgpt->response();
63 |
64 | $chat["messages"][] = $response;
65 |
66 | file_put_contents(
67 | "chats/" . basename( $_GET['chat_id'] ) . ".json",
68 | json_encode( $chat )
69 | );
70 |
71 | if( empty( $chat["name"] ) || $chat["name"] == "Untitled chat" ) {
72 | $create_title = 'hx-trigger="load" hx-get="/create_title.php?chat_id='.htmlspecialchars( $_GET['chat_id'] ).'" hx-target=".chat-'.htmlspecialchars( $_GET['chat_id'] ).'"';
73 | } else {
74 | $create_title = '';
75 | }
76 |
77 | echo ''.nl2br( htmlspecialchars( $response["content"] ) ).'
';
78 |
--------------------------------------------------------------------------------
/php-gpt-pdfread/gpt.php:
--------------------------------------------------------------------------------
1 | $keywords A list of keywords
8 | */
9 | function list_keywords( $keywords ) {
10 |
11 | }
12 |
13 | /**
14 | * Give the response to the user
15 | *
16 | * @param string $response The response to the user
17 | */
18 | function give_response( string $response ) {
19 |
20 | }
21 |
22 | /**
23 | * Get the next excerpt from the PDF
24 | *
25 | * @param bool $next Set this to true always
26 | */
27 | function next_excerpt( bool $next ) {
28 |
29 | }
30 |
31 | function get_keywords( string $question ) {
32 | $prompt = "I want to search for the answer to this question from a PDF file. Please give me a list of keywords that I could use to search for the information.
33 |
34 | ```
35 | $question
36 | ```
37 |
38 | Use the list_keywords function to respond.";
39 |
40 | $chatgpt = new ChatGPT( getenv("OPENAI_API_KEY") );
41 | $chatgpt->add_function( "list_keywords" );
42 | $chatgpt->smessage( "You a are a search keyword generator" );
43 | $chatgpt->umessage( $prompt );
44 |
45 | $response = $chatgpt->response( raw_function_response: true );
46 | $function_call = $response->function_call;
47 |
48 | $arguments = json_decode( $function_call->arguments, true );
49 | $keywords = strtolower( implode( " ", $arguments["keywords"] ) );
50 | $keywords = explode( " ", $keywords );
51 |
52 | return $keywords;
53 | }
54 |
55 | function answer_question( string $chunk, string $question ) {
56 | $prompt = "```
57 | $chunk
58 | ```
59 |
60 | Based on the above excerpt, what is the answer to the following question?
61 |
62 | ```
63 | $question
64 | ```
65 |
66 | If the answer to the question is included in the above text, respond with the give_response function. If it is not found in the given text, respond with the next_excerpt function.";
67 |
68 | $chatgpt = new ChatGPT( getenv("OPENAI_API_KEY") );
69 | $chatgpt->add_function( "give_response" );
70 | $chatgpt->add_function( "next_excerpt" );
71 | $chatgpt->smessage( "You are trying to find answers to the questions of the user from a PDF file. You will be provided with an excerpt of the PDF file. If the excerpt contains the answer to the question, use the give_response function to tell the answer to the user. Otherwise call the next_excerpt function to get the next excerpt from the PDF." );
72 | $chatgpt->umessage( $prompt );
73 |
74 | $response = $chatgpt->response( raw_function_response: true );
75 |
76 | if( ! isset( $response->function_call ) ) {
77 | return answer_question( $chunk, $question );
78 | }
79 |
80 | return $response->function_call;
81 | }
82 |
--------------------------------------------------------------------------------
/ChatGPT.php:
--------------------------------------------------------------------------------
1 | chat_id === null ) {
13 | $this->chat_id = uniqid( more_entropy: true );
14 | }
15 | }
16 |
17 | public function load() {
18 | if( is_callable( $this->loadfunction ) ) {
19 | $this->messages = ($this->loadfunction)( $this->chat_id );
20 | }
21 | }
22 |
23 | public function smessage( string $system_message ) {
24 | $message = [
25 | "role" => "system",
26 | "content" => $system_message,
27 | ];
28 |
29 | $this->messages[] = $message;
30 |
31 | if( is_callable( $this->savefunction ) ) {
32 | ($this->savefunction)( $message, $this->chat_id );
33 | }
34 | }
35 |
36 | public function umessage( string $user_message ) {
37 | $message = [
38 | "role" => "user",
39 | "content" => $user_message,
40 | ];
41 |
42 | $this->messages[] = $message;
43 |
44 | if( is_callable( $this->savefunction ) ) {
45 | ($this->savefunction)( $message, $this->chat_id );
46 | }
47 | }
48 |
49 | public function amessage( string $assistant_message ) {
50 | $message = [
51 | "role" => "assistant",
52 | "content" => $assistant_message,
53 | ];
54 |
55 | $this->messages[] = $message;
56 |
57 | if( is_callable( $this->savefunction ) ) {
58 | ($this->savefunction)( $message, $this->chat_id );
59 | }
60 | }
61 |
62 | public function fcall(
63 | string $function_name,
64 | string $function_arguments
65 | ) {
66 | $message = [
67 | "role" => "assisant",
68 | "content" => null,
69 | "function_call" => [
70 | "name" => $function_name,
71 | "arguments" => $function_arguments,
72 | ]
73 | ];
74 |
75 | $this->messages[] = $message;
76 |
77 | if( is_callable( $this->savefunction ) ) {
78 | ($this->savefunction)( $message, $this->chat_id );
79 | }
80 | }
81 |
82 | public function fresult(
83 | string $function_name,
84 | string $function_return_value
85 | ) {
86 | $message = [
87 | "role" => "function",
88 | "content" => $function_return_value,
89 | "name" => $function_name,
90 | ];
91 |
92 | $this->messages[] = $message;
93 |
94 | if( is_callable( $this->savefunction ) ) {
95 | ($this->savefunction)( $message, $this->chat_id );
96 | }
97 | }
98 |
99 | public function response( bool $raw_function_response = false ) {
100 | $fields = [
101 | "model" => "gpt-3.5-turbo-16k-0613",
102 | "messages" => $this->messages,
103 | ];
104 |
105 | $functions = $this->get_functions();
106 |
107 | if( ! empty( $functions ) ) {
108 | $fields["functions"] = $functions;
109 | $fields["function_call"] = "auto";
110 | }
111 |
112 | // make ChatGPT API request
113 | $ch = curl_init( "https://api.openai.com/v1/chat/completions" );
114 | curl_setopt( $ch, CURLOPT_HTTPHEADER, [
115 | "Content-Type: application/json",
116 | "Authorization: Bearer " . $this->api_key
117 | ] );
118 | curl_setopt( $ch, CURLOPT_POSTFIELDS, json_encode(
119 | $fields
120 | ) );
121 | curl_setopt( $ch, CURLOPT_POST, true );
122 | curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true );
123 |
124 | // get ChatGPT reponse
125 | $curl_exec = curl_exec( $ch );
126 | $response = json_decode( $curl_exec );
127 |
128 | // somewhat handle errors
129 | if( ! isset( $response->choices[0]->message ) ) {
130 | if( isset( $response->error ) ) {
131 | $error = trim( $response->error->message . " (" . $response->error->type . ")" );
132 | } else {
133 | $error = $curl_exec;
134 | }
135 | throw new \Exception( "Error in OpenAI request: " . $error );
136 | }
137 |
138 | // add response to messages
139 | $message = $response->choices[0]->message;
140 | $this->messages[] = $message;
141 |
142 | if( is_callable( $this->savefunction ) ) {
143 | ($this->savefunction)( $message, $this->chat_id );
144 | }
145 |
146 | $message = end( $this->messages );
147 |
148 | $message = $this->handle_functions( $message, $raw_function_response );
149 |
150 | return $message;
151 | }
152 |
153 | protected function handle_functions( stdClass $message, bool $raw_function_response = false ) {
154 | if( isset( $message->function_call ) ) {
155 | if( $raw_function_response ) {
156 | return $message;
157 | }
158 |
159 | // get function name and arguments
160 | $function_call = $message->function_call;
161 | $function_name = $function_call->name;
162 | $arguments = json_decode( $function_call->arguments, true );
163 |
164 | $callable = $this->get_function( $function_name );
165 |
166 | if( is_callable( $callable ) ) {
167 | $result = $callable( ...array_values( $arguments ) );
168 | } else {
169 | $result = "Function '$function_name' unavailable.";
170 | }
171 |
172 | $this->fresult( $function_name, $result );
173 |
174 | return $this->response();
175 | }
176 |
177 | return $message;
178 | }
179 |
180 | protected function get_function( string $function_name ) {
181 | foreach( $this->functions as $function ) {
182 | if( $function["name"] === $function_name ) {
183 | return $function["function"];
184 | }
185 | }
186 |
187 | return false;
188 | }
189 |
190 | protected function get_functions() {
191 | $functions = [];
192 |
193 | foreach( $this->functions as $function ) {
194 | $properties = [];
195 | $required = [];
196 |
197 | foreach( $function["parameters"] as $parameter ) {
198 | $properties[$parameter['name']] = [
199 | "type" => $parameter['type'],
200 | "description" => $parameter['description'],
201 | ];
202 |
203 | if( isset( $parameter["items"] ) ) {
204 | $properties[$parameter['name']]["items"] = $parameter["items"];
205 | }
206 |
207 | if( array_key_exists( "required", $parameter ) && $parameter["required"] !== false ) {
208 | $required[] = $parameter["name"];
209 | }
210 | }
211 |
212 | $functions[] = [
213 | "name" => $function["name"],
214 | "description" => $function["description"],
215 | "parameters" => [
216 | "type" => "object",
217 | "properties" => $properties,
218 | "required" => $required,
219 | ],
220 | ];
221 | }
222 |
223 | return $functions;
224 | }
225 |
226 | public function add_function( array|callable $function ) {
227 | if( is_callable( $function ) ) {
228 | $function = $this->parse_function( $function );
229 | }
230 | $this->functions[] = $function;
231 | }
232 |
233 | protected function parse_function( callable $function ) {
234 | $reflection = new ReflectionFunction( $function );
235 | $doc_comment = $reflection->getDocComment() ?: "";
236 | $description = $this->parse_description( $doc_comment );
237 |
238 | $function_data = [
239 | "function" => $function,
240 | "name" => $reflection->getName(),
241 | "description" => $description,
242 | "parameters" => [],
243 | ];
244 |
245 | $matches = [];
246 | preg_match_all( '/@param\s+(\S+)\s+\$(\S+)[^\S\r\n]?([^\r\n]+)?/', $doc_comment, $matches );
247 |
248 | $types = $matches[1];
249 | $names = $matches[2];
250 | $descriptions = $matches[3];
251 |
252 | $params = $reflection->getParameters();
253 | foreach( $params as $param ) {
254 | $name = $param->getName();
255 | $index = array_search( $name, $names );
256 | $description = $descriptions[$index] ?? "";
257 | $type = $param->getType()?->getName() ?? $types[$index] ?? "string";
258 |
259 | try {
260 | $param->getDefaultValue();
261 | $required = false;
262 | } catch( \ReflectionException $e ) {
263 | $required = true;
264 | }
265 |
266 | $data = [
267 | "name" => $name,
268 | "type" => $this->parse_type( $type ),
269 | "description" => $description,
270 | "required" => $required,
271 | ];
272 |
273 | if( strpos( $type, "array<" ) === 0 ) {
274 | $array_type = trim( substr( $type, 5 ), "<>" );
275 | $data["type"] = "array";
276 | $data["items"] = [
277 | "type" => $this->parse_type( $array_type ),
278 | ];
279 | }
280 |
281 | if( strpos( $type, "[]" ) !== false ) {
282 | $array_type = substr( $type, 0, -2 );
283 | $data["type"] = "array";
284 | $data["items"] = [
285 | "type" => $this->parse_type( $array_type ),
286 | ];
287 | }
288 |
289 | $function_data["parameters"][] = $data;
290 | }
291 |
292 | return $function_data;
293 | }
294 |
295 | protected function parse_type( string $type ) {
296 | return match( $type ) {
297 | "int" => "number",
298 | "integer" => "number",
299 | "string" => "string",
300 | "float" => "number",
301 | default => "string",
302 | };
303 | }
304 |
305 | protected function parse_description( string $doc_comment ) {
306 | $lines = explode( "\n", $doc_comment );
307 | $description = "";
308 |
309 | $started = false;
310 | foreach( $lines as $line ) {
311 | $matches = [];
312 | if( preg_match( '/\s+?\*\s+?([^@](.*?))?$/', $line, $matches ) === 1 ) {
313 | $description .= " ".$matches[1];
314 | $started = true;
315 | } elseif( $started ) {
316 | break;
317 | }
318 | }
319 |
320 | return trim( $description );
321 | }
322 |
323 | public function messages() {
324 | return $this->messages;
325 | }
326 |
327 | public function loadfunction( callable $loadfunction ) {
328 | $this->loadfunction = $loadfunction;
329 | }
330 |
331 | public function savefunction( callable $savefunction ) {
332 | $this->savefunction = $savefunction;
333 | }
334 | }
335 |
--------------------------------------------------------------------------------