├── 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 '
'; 78 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |