├── ChatGPT.php ├── README.md ├── conversations.php ├── create_title.php ├── get_response.php ├── index.html ├── load_chat.php ├── new_chat.php ├── php-gpt-pdfread ├── gpt.php ├── lookup.php └── pdfread.php ├── send_message.php └── style.css /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /conversations.php: -------------------------------------------------------------------------------- 1 | '.htmlspecialchars( $chat["name"] ).''; 8 | } -------------------------------------------------------------------------------- /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; -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | GPT-FileChat 8 | 9 | 10 | 11 | 12 | 20 |
21 | 22 |
23 | 24 | -------------------------------------------------------------------------------- /load_chat.php: -------------------------------------------------------------------------------- 1 | '; 8 | foreach( $chat["messages"] as $message ) { 9 | echo '
'.nl2br( htmlspecialchars( $message["content"] ) ).'
'; 10 | } 11 | echo ''; 12 | 13 | ?> 14 |
15 | 16 |
17 | Upload file: 18 |
19 |
20 | 21 | 22 |
23 |
-------------------------------------------------------------------------------- /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 ''; -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | } --------------------------------------------------------------------------------