From 8316d4ea8d0a8dd8ac646ef3112019386138f04f Mon Sep 17 00:00:00 2001 From: Ace_Radom Date: Wed, 26 Apr 2023 17:17:55 +0200 Subject: [PATCH 01/16] change main exe's name --- CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index aa17d87..b6eb457 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -18,8 +18,8 @@ add_library(iniparser SHARED include_directories(${CMAKE_SOURCE_DIR}/include) add_subdirectory(${CMAKE_SOURCE_DIR}/src) -add_executable(main main.c ${UTILS}) -target_link_libraries(main +add_executable(cgpterm main.c ${UTILS}) +target_link_libraries(cgpterm utils # ncursesw ezylog From cfbfaf857ebd83d98ad9eb37c270e0c2a34b5063 Mon Sep 17 00:00:00 2001 From: Ace_Radom Date: Wed, 26 Apr 2023 18:05:10 +0200 Subject: [PATCH 02/16] delete cvector lib (unused) --- include/cvector.h | 37 --------- src/CMakeLists.txt | 2 - src/cvector/CMakeLists.txt | 18 ----- src/cvector/cvector.c | 150 ------------------------------------- 4 files changed, 207 deletions(-) delete mode 100644 include/cvector.h delete mode 100644 src/cvector/CMakeLists.txt delete mode 100644 src/cvector/cvector.c diff --git a/include/cvector.h b/include/cvector.h deleted file mode 100644 index 29fb712..0000000 --- a/include/cvector.h +++ /dev/null @@ -1,37 +0,0 @@ -#ifndef _CVECTOR_VECTOR_H_ -#define _CVECTOR_VECTOR_H_ - -#include -#include -#include -#include - -#ifdef __cplusplus -extern "C" { -#endif - -typedef struct { - long long cap; // capacity - long long len; // length - void** items; // items -} c_vector; - -void cv_init( c_vector* __v , long long __cap ); -void cv_resize( c_vector* __v , long long __cap ); -void cv_push_front( c_vector* __v , void* __item ); -void cv_push_back( c_vector* __v , void* __item ); -void* cv_pop_front( c_vector* __v ); -void* cv_pop_back( c_vector* __v ); -void cv_insert( c_vector* __v , void* __item , long long __pos ); -void* cv_delete( c_vector* __v , long long __pos ); -void cv_clean( c_vector* __v ); -long long cv_len( c_vector* __v ); - -void cv_print_int( c_vector* __v ); -void cv_print_str( c_vector* __v ); - -#ifdef __cplusplus -} -#endif - -#endif // _CVECTOR_VECTOR_H_ \ No newline at end of file diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 2ca5106..558956f 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -2,8 +2,6 @@ add_subdirectory(ezylog) # build ezylog subproject add_subdirectory(openai_api) # build openai-api lib -add_subdirectory(cvector) -# build cvector subproject add_subdirectory(rwcfg) # build rwcfg lib add_subdirectory(argparse) diff --git a/src/cvector/CMakeLists.txt b/src/cvector/CMakeLists.txt deleted file mode 100644 index 385b9c7..0000000 --- a/src/cvector/CMakeLists.txt +++ /dev/null @@ -1,18 +0,0 @@ -cmake_minimum_required(VERSION 3.10) -project(cvector VERSION 1.0.5) - -# cvector subproject - -# VERSION LIST: -# * v1.0 - first version, achieve all basic function -# * v1.0.5 - add two debug function: 'cv_print_int' and 'cv_print_str' -# they're used to output all objects in vectors - -add_definitions(-DEXPORTING_APIS) - -include_directories(${CMAKE_SOURCE_DIR}/include) - -aux_source_directory(. CVECTOR_SRC) -add_library(cvector SHARED - ${CVECTOR_SRC} -) \ No newline at end of file diff --git a/src/cvector/cvector.c b/src/cvector/cvector.c deleted file mode 100644 index 8cfb075..0000000 --- a/src/cvector/cvector.c +++ /dev/null @@ -1,150 +0,0 @@ -#include"cvector.h" - -void cv_init( c_vector* __v , long long __cap ){ - __v -> cap = __cap; - __v -> len = 0; - __v -> items = calloc( __v -> cap , sizeof( void* ) ); - if ( !__v -> items ) - { - printf( "Memory allocate failed\n" ); - exit( 1 ); - } - return; -} - -void cv_resize( c_vector* __v , long long __cap ){ - __v -> cap = __cap; - __v -> items = realloc( __v -> items , sizeof( void* ) * ( __v -> cap ) ); - if ( !__v -> items ) - { - printf( "Memory allocate failed\n" ); - exit( 1 ); - } - return; -} - -void cv_push_front( c_vector* __v , void* __item ){ - if ( __v -> len == __v -> cap ) - { - cv_resize( __v , __v -> cap * 2 ); - } // Capacity reached the upper limit, resize - memmove( __v -> items + 1 , __v -> items , __v -> len * sizeof( void* ) ); - __v -> items[0] = __item; - __v -> len++; - return; -} - -void cv_push_back( c_vector* __v , void* __item ){ - if ( __v -> len == __v -> cap ) - { - cv_resize( __v , __v -> cap * 2 ); - } // Capacity reached the upper limit, resize - __v -> items[__v->len++] = __item; - return; -} - -void* cv_pop_front( c_vector* __v ){ - if ( __v -> len < 1 ) - { - return NULL; - } // vector already empty - void* ret = __v -> items[0]; - __v -> len--; - memmove( __v -> items , __v -> items + 1 , __v -> len * sizeof( void* ) ); - if ( __v -> len < __v -> cap / 3 ) - { - cv_resize( __v , __v -> cap / 2 ); - } // free excess space - return ret; -} - -void* cv_pop_back( c_vector* __v ){ - if ( __v -> len < 1 ) - { - return NULL; - } - int last = __v -> len - 1; - void* ret = __v -> items[last]; - __v -> items[last] = NULL; - --__v -> len; - if ( __v -> len < __v -> cap / 3 ) - { - cv_resize( __v , __v -> cap / 2 ); - } - return ret; -} - -void cv_insert( c_vector* __v , void* __item , long long __pos ){ - if ( __pos == 0 ) - { - cv_push_front( __v , __item ); - } - else if ( __pos >= __v -> len ) - { - cv_push_back( __v , __item ); - } - else - { - if ( __v -> len == __v -> cap ) - { - cv_resize( __v , __v -> cap * 2 ); - } // Capacity reached the upper limit, resize - memmove( __v -> items + __pos + 1 , __v -> items + __pos , ( __v -> len - __pos ) * sizeof( void* ) ); - __v -> len++; - __v -> items[__pos] = __item; - } - return; -} - -void* cv_delete( c_vector* __v , long long __pos ){ - if ( __v -> len == 0 || __v -> len <= __pos || __pos < 0 ) - { - return NULL; - } - else if ( __v -> len - 1 == __pos ) - { - return cv_pop_back( __v ); - } - else if ( __pos == 0 ) - { - return cv_pop_front( __v ); - } - - void* ret = __v -> items[__pos]; - memmove( __v -> items + __pos , __v -> items + __pos + 1 , ( __v -> len - __pos - 1 ) * sizeof( void* ) ); - __v -> len--; - if ( __v -> len < __v -> cap / 3 ) - { - cv_resize( __v , __v -> cap / 2 ); - } // free excess space - return ret; -} - -void cv_clean( c_vector* __v ){ - free( __v -> items ); - __v -> cap = 1; - __v -> len = 0; - return; -} - -long long cv_len( c_vector* __v ){ - return __v -> len; -} - -// debug functions - -void cv_print_int( c_vector* __v ){ - for ( int i = 0 ; i < __v -> len ; i++ ) - { - printf( "%d\n" , ( int ) __v -> items[i] ); - } - return; -} - -void cv_print_str( c_vector* __v ){ - for ( int i = 0 ; i < __v -> len ; i++ ) - { - printf( "%s\n" , ( char* ) __v -> items[i] ); - } - return; -} \ No newline at end of file From 9ec32b1ce58ef9d259c76bf3f163d9a925a977e3 Mon Sep 17 00:00:00 2001 From: Ace_Radom Date: Wed, 26 Apr 2023 18:06:03 +0200 Subject: [PATCH 03/16] add http 4xx error code match --- include/openai_api.h | 3 +++ src/cli/cli.cpp | 15 ++++++++--- src/openai_api/openai_api.c | 51 +++++++++++++++++++++++++++++-------- 3 files changed, 55 insertions(+), 14 deletions(-) diff --git a/include/openai_api.h b/include/openai_api.h index 980e508..79317c2 100644 --- a/include/openai_api.h +++ b/include/openai_api.h @@ -40,11 +40,14 @@ typedef struct { extern openai_t* openai; extern bool request_working; +extern long HTTP_Response_code; void openai_init(); void openai_send_chatrequest( openai_datatransfer_t* __data ); void openai_free(); +void openai_msg_popback(); + #ifdef __cplusplus } #endif diff --git a/src/cli/cli.cpp b/src/cli/cli.cpp index f93d5d0..53b9a14 100644 --- a/src/cli/cli.cpp +++ b/src/cli/cli.cpp @@ -11,10 +11,10 @@ int start_CLI(){ std::string input; std::cout << "> "; std::getline( std::cin , input ); + if ( input.size() == 0 ) + continue; if ( input == "quit" ) - { break; - } turn_off_echo(); write_ANSI( HIDE_CURSOR ); @@ -38,8 +38,17 @@ int start_CLI(){ // clean wait msg if ( data.response ) { - std::cout << "ChatGPT:" << std::endl << data.response << std::endl; + if ( HTTP_Response_code / 100 != 4 ) + std::cout << "ChatGPT:" << std::endl << data.response << std::endl; + else + { + std::cout << "Request Error: " << data.response << std::endl; + openai_msg_popback(); + } // request error, pop last user's msg } + else + openai_msg_popback(); + // same: request error, pop last user's msg } return 0; } \ No newline at end of file diff --git a/src/openai_api/openai_api.c b/src/openai_api/openai_api.c index dfd2d21..7f4709a 100644 --- a/src/openai_api/openai_api.c +++ b/src/openai_api/openai_api.c @@ -2,6 +2,7 @@ openai_t* openai = NULL; bool request_working = false; +long HTTP_Response_code = 0; typedef struct { char* ptr; @@ -72,7 +73,8 @@ void openai_send_chatrequest( openai_datatransfer_t* __data ){ curl = curl_easy_init(); if ( !curl ) { - fprintf( stderr , "[openai_send_chatrequest] -> init curl failed\n" ); + fprintf( stderr , "[openai_send_chatrequest] -> curl init failed\n" ); + ezylog_logerror( logger , "curl init failed" ); request_working = false; return; } // curl init error @@ -102,32 +104,48 @@ void openai_send_chatrequest( openai_datatransfer_t* __data ){ // make curl request res = curl_easy_perform( curl ); - if ( res != CURLE_OK ) { fprintf( stderr , "send request failed: %s\n" , curl_easy_strerror( res ) ); + ezylog_logerror( logger , "send request failed: %s" , curl_easy_strerror( res ) ); text = NULL; goto request_stop; } + ezylog_logdebug( logger , "Response got, total size: %ld" , response_data.size ); + + curl_easy_getinfo( curl , CURLINFO_RESPONSE_CODE , &HTTP_Response_code ); + // get api response code and parse it later + json_t* root; json_error_t error; root = json_loads( response_data.ptr , 0 , &error ); if ( !root ) { - fprintf( stderr , "[openai_api->write_callback] -> response json error at line %d: %s\n" , error.line , error.text ); + fprintf( stderr , "[openai_send_chatrequest] -> response json error at line %d: %s\n" , error.line , error.text ); text = NULL; goto request_stop; } // json error - - json_t* response_msg = json_object_get( root , "choices" ); - response_msg = json_array_get( response_msg , 0 ); - response_msg = json_object_get( response_msg , "message" ); - json_array_append_new( openai -> messages , response_msg ); - text = json_string_value( json_object_get( response_msg , "content" ) ); - ezylog_loginfo( logger , "ChatGPT: %s" , text ); - ezylog_logdebug( logger , "GPT Response raw: %s" , response_data.ptr ); + + if ( HTTP_Response_code / 100 == 4 ) + { + json_t* response_errormsg = json_object_get( root , "error" ); + text = json_string_value( json_object_get( response_errormsg , "message" ) ); + ezylog_logerror( logger , "OpenAI API responsed Error: Code %ld, Message: \"%s\"" , HTTP_Response_code , text ); + ezylog_logdebug( logger , "GPT Response raw: %s" , json_dumps( root , JSON_COMPACT ) ); + // because of unknown format problem (openai's error response contains \n), using json_dumps here + } // parse response code, match 4xx errros + else + { + json_t* response_msg = json_object_get( root , "choices" ); + response_msg = json_array_get( response_msg , 0 ); + response_msg = json_object_get( response_msg , "message" ); + json_array_append_new( openai -> messages , response_msg ); + text = json_string_value( json_object_get( response_msg , "content" ) ); + ezylog_loginfo( logger , "ChatGPT: %s" , text ); + ezylog_logdebug( logger , "GPT Response raw: %s" , response_data.ptr ); + } // response code 200 OK (most likely) request_stop: curl_easy_cleanup( curl ); @@ -147,4 +165,15 @@ void openai_free(){ free( openai ); curl_global_cleanup(); return; +} + +void openai_msg_popback(){ + size_t msglist_size = json_array_size( openai -> messages ); + if ( msglist_size <= 1 ) + { + return; + } // here: normally msglist size can not be 0 because of the prompt message; + // anyway when there's only prompt message in the list then it can not be pop. + json_array_remove( openai -> messages , msglist_size - 1 ); + return; } \ No newline at end of file From 8c5ef2779f93ec7112b590ad3a01dcbc8088bc42 Mon Sep 17 00:00:00 2001 From: Ace_Radom Date: Wed, 26 Apr 2023 18:14:11 +0200 Subject: [PATCH 04/16] Update README --- README.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 5a17874..7a195a9 100644 --- a/README.md +++ b/README.md @@ -21,14 +21,13 @@ Chat with GPT in Terminal - 自动创建 `config.ini` 配置文件 - 通过命令行对 `config.ini` 内的值进行更改 - 自建log库 支持 `DEBUG` `INFO` `ERROR` `FATAL` 层级 -- 基本的API访问:可以正常与ChatGPT交流 但还没有任何错误特判 +- 基本的API访问:可以正常与ChatGPT交流 - 基本的交互界面 - 在等待GPT回复时显示 `ChatGPT is thinking...` - 在等待回复时锁死终端 不回显并隐藏光标 在回复后解锁 目前正在开发的功能: -- 进一步扩充API交互 增加错误返回解析 - 实现部分底层的斜杠命令:`/timeout` `/model` `/last` `/help` `/exit` - tokens 统计 逐步实现 `/tokens` 命令 - credit 统计 逐步实现 `/usage` 命令 @@ -51,6 +50,11 @@ Chat with GPT in Terminal - 自建段落式Markdown解析库 用于适配流式输出的文本解析【若找到更好的替代方法会取消】 - 流式输出 +### 目前已知的问题 + +- 在Debian 11.6, zsh下测试中发现:中文无法正确退格(以确定为utf8编码) + + 目前不清楚是程序问题还是zsh的问题 但我编写的极其简单的 `cin >> string` 遇到了一模一样的问题 若这个问题普遍存在将是个很头疼的事 但以我的能力可能是没什么办法 `setlocale` 什么的都试过了 ### 项目依赖 From 0307e79718da18e2af9133d57aabda480f695b2d Mon Sep 17 00:00:00 2001 From: Ace_Radom Date: Wed, 26 Apr 2023 20:58:14 +0200 Subject: [PATCH 05/16] add GNU readline require --- .github/workflows/cmake.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/cmake.yml b/.github/workflows/cmake.yml index c8b1673..f017f87 100644 --- a/.github/workflows/cmake.yml +++ b/.github/workflows/cmake.yml @@ -34,6 +34,9 @@ jobs: - name: Install libjansson run: sudo apt install libjansson-dev -y + - name: Install GNU libreadline + run: sudo apt install libreadline-dev -y + - name: Configure CMake # Configure CMake in a 'build' subdirectory. `CMAKE_BUILD_TYPE` is only required if you are using a single-configuration generator such as make. # See https://cmake.org/cmake/help/latest/variable/CMAKE_BUILD_TYPE.html?highlight=cmake_build_type From 5e464b258bac5eb698d8776dacdf30d0b5d07065 Mon Sep 17 00:00:00 2001 From: Ace_Radom Date: Wed, 26 Apr 2023 21:38:32 +0200 Subject: [PATCH 06/16] rewrite CLI in C; link readline lib; fix: chinese char erase problems --- CMakeLists.txt | 5 ++-- include/cli.h | 27 +++++++++++++++++++++ include/cli.hpp | 24 ------------------- include/cli/{toolkit.hpp => toolkit.h} | 22 ++++++++++------- include/openai_api.h | 2 +- main.c | 5 +++- src/cli/CMakeLists.txt | 2 ++ src/cli/{cli.cpp => cli.c} | 33 ++++++++++++++------------ src/cli/{toolkit.cpp => toolkit.c} | 15 +++++++----- src/openai_api/openai_api.c | 7 +++--- 10 files changed, 81 insertions(+), 61 deletions(-) create mode 100644 include/cli.h delete mode 100644 include/cli.hpp rename include/cli/{toolkit.hpp => toolkit.h} (59%) rename src/cli/{cli.cpp => cli.c} (55%) rename src/cli/{toolkit.cpp => toolkit.c} (64%) diff --git a/CMakeLists.txt b/CMakeLists.txt index b6eb457..5f8c9ee 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,9 +2,8 @@ cmake_minimum_required(VERSION 3.10) project(cGPTerm VERSION 1.0) set(CMAKE_C_FLAGS "-Wall -g") -set(CMAKE_CXX_FLAGS "-Wall -g") -set(CMAKE_CXX_STANDARD_REQUIRED ON) -set(CMAKE_CXX_STANDARD 11) +add_definitions(-D _DEFAULT_SOURCE) +add_definitions(-D _GNU_SOURCE) set(LIBRARY_OUTPUT_PATH ${CMAKE_SOURCE_DIR}/bin) set(EXECUTABLE_OUTPUT_PATH ${CMAKE_SOURCE_DIR}/bin) diff --git a/include/cli.h b/include/cli.h new file mode 100644 index 0000000..983db09 --- /dev/null +++ b/include/cli.h @@ -0,0 +1,27 @@ +#ifndef _CLI_H_ +#define _CLI_H_ + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include +#include +#include + +#include +#include +#include + +#include"openai_api.h" +#include"cli/toolkit.h" + +int start_CLI(); +// this is an api for main.c + +#ifdef __cplusplus +} +#endif + +#endif \ No newline at end of file diff --git a/include/cli.hpp b/include/cli.hpp deleted file mode 100644 index 26d421c..0000000 --- a/include/cli.hpp +++ /dev/null @@ -1,24 +0,0 @@ -#ifndef _CLI_HPP_ -#define _CLI_HPP_ - -#ifdef __cplusplus - -#include -#include -#include -#include"cli/toolkit.hpp" - -extern "C" { - -#include"openai_api.h" - -#endif - -int start_CLI(); -// this is an api for main.c - -#ifdef __cplusplus -} -#endif - -#endif \ No newline at end of file diff --git a/include/cli/toolkit.hpp b/include/cli/toolkit.h similarity index 59% rename from include/cli/toolkit.hpp rename to include/cli/toolkit.h index d534f07..01b7f13 100644 --- a/include/cli/toolkit.hpp +++ b/include/cli/toolkit.h @@ -1,13 +1,15 @@ -#ifndef _TOOLKIT_HPP_ -#define _TOOLKIT_HPP_ +#ifndef _TOOLKIT_H_ +#define _TOOLKIT_H_ -#include -#include -#include -#include -#include +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include #include #include +#include #define HIDE_CURSOR "\033[?25l" #define SHOW_CURSOR "\033[?25h" @@ -21,6 +23,10 @@ void turn_off_echo(); void write_ANSI( const char* __ANSI ); -void print_wait_msg( std::string __msg ); +void print_wait_msg( const char* __msg ); + +#ifdef __cplusplus +} +#endif #endif \ No newline at end of file diff --git a/include/openai_api.h b/include/openai_api.h index 79317c2..63fa884 100644 --- a/include/openai_api.h +++ b/include/openai_api.h @@ -43,7 +43,7 @@ extern bool request_working; extern long HTTP_Response_code; void openai_init(); -void openai_send_chatrequest( openai_datatransfer_t* __data ); +void openai_send_chatrequest( void* __data ); void openai_free(); void openai_msg_popback(); diff --git a/main.c b/main.c index 144e68b..73fd19c 100644 --- a/main.c +++ b/main.c @@ -4,15 +4,18 @@ #include #include #include +#include #include"rwcfg.h" #include"argparse.h" #include"utils.h" #include"ezylog.h" #include"openai_api.h" -#include"cli.hpp" +#include"cli.h" int main( int argc , char** argv ){ + setlocale( LC_ALL , "" ); + usrhome = getenv( "HOME" ); // get usr home absolute path cfgdir = ( char* ) malloc( strlen( usrhome ) + strlen( "/.cgpterm" ) + 1 ); diff --git a/src/cli/CMakeLists.txt b/src/cli/CMakeLists.txt index 813b812..5f14bce 100644 --- a/src/cli/CMakeLists.txt +++ b/src/cli/CMakeLists.txt @@ -7,4 +7,6 @@ add_library(cli SHARED ) target_link_libraries(cli openaiapi + pthread + readline ) diff --git a/src/cli/cli.cpp b/src/cli/cli.c similarity index 55% rename from src/cli/cli.cpp rename to src/cli/cli.c index 53b9a14..7e5e8ec 100644 --- a/src/cli/cli.cpp +++ b/src/cli/cli.c @@ -1,4 +1,4 @@ -#include"cli.hpp" +#include"cli.h" /** * @brief start cGPTerm CLI @@ -8,41 +8,44 @@ int start_CLI(){ while ( 1 ) { - std::string input; - std::cout << "> "; - std::getline( std::cin , input ); - if ( input.size() == 0 ) + char* input; + printf( "> " ); + input = readline( NULL ); + if ( !input || *input == '\0' ) continue; - if ( input == "quit" ) + if ( strcmp( input , "quit" ) == 0 ) break; turn_off_echo(); write_ANSI( HIDE_CURSOR ); // turn off echo; hide cursor openai_datatransfer_t data; - data.msg = input.c_str(); + data.msg = ( char* ) malloc( strlen( input ) + 1 ); + strcpy( data.msg , input ); data.response = NULL; // build transfer data - std::thread send_request( openai_send_chatrequest , &data ); - std::this_thread::sleep_for( std::chrono::microseconds( 10 ) ); + pthread_t send_request; + int ptrc = pthread_create( &send_request , NULL , openai_send_chatrequest , ( void* ) &data ); // pthread return code + usleep( 10000 ); // start request; wait 10 ms in order to let openai_send_chatrequest to lock request_working (-> true) while ( request_working ) { print_wait_msg( "ChatGPT is thinking" ); } // until request done: print wait msg - std::cout << "\r \r" << std::flush; - send_request.join(); + printf( "\r \r" ); + fflush( stdout ); + pthread_join( send_request , NULL ); + // clean wait msg, request thread join reset_terattr(); write_ANSI( SHOW_CURSOR ); - // request join; reset attr; show cursor - // clean wait msg + // reset attr; show cursor if ( data.response ) { if ( HTTP_Response_code / 100 != 4 ) - std::cout << "ChatGPT:" << std::endl << data.response << std::endl; + printf( "ChatGPT:\n%s\n" , data.response ); else { - std::cout << "Request Error: " << data.response << std::endl; + printf( "Request Error: %s\n" , data.response ); openai_msg_popback(); } // request error, pop last user's msg } diff --git a/src/cli/toolkit.cpp b/src/cli/toolkit.c similarity index 64% rename from src/cli/toolkit.cpp rename to src/cli/toolkit.c index d00d8ce..7811ed3 100644 --- a/src/cli/toolkit.cpp +++ b/src/cli/toolkit.c @@ -1,8 +1,8 @@ -#include"cli/toolkit.hpp" +#include"cli/toolkit.h" struct termios ori_attr; -const std::string wait_char[] = { ". " , ".. " , "..." , " .." , " ." , " " }; +const char* wait_char[] = { ". " , ".. " , "..." , " .." , " ." , " " }; void get_original_terattr(){ tcgetattr( STDIN_FILENO , &ori_attr ); @@ -28,10 +28,13 @@ void write_ANSI( const char* __ANSI ){ return; } -void print_wait_msg( std::string __msg ){ +void print_wait_msg( const char* __msg ){ static int counter = 0; - std::cout << __msg << wait_char[counter%6] << '\r' << std::flush; - counter++; - std::this_thread::sleep_for( std::chrono::milliseconds( 200 ) ); + printf( "%s%s\r" , __msg , wait_char[counter] ); + fflush( stdout ); + counter == 5 ? counter = 0 + : counter++; + usleep( 100000 ); + // sleep 100 ms return; } \ No newline at end of file diff --git a/src/openai_api/openai_api.c b/src/openai_api/openai_api.c index 7f4709a..53f121f 100644 --- a/src/openai_api/openai_api.c +++ b/src/openai_api/openai_api.c @@ -62,9 +62,10 @@ void openai_init(){ return; } -void openai_send_chatrequest( openai_datatransfer_t* __data ){ +void openai_send_chatrequest( void* __data ){ request_working = true; - const char* __usrmsg = __data -> msg; + openai_datatransfer_t* data = ( openai_datatransfer_t* ) __data; + const char* __usrmsg = data -> msg; // unpack transfer data CURL* curl; @@ -151,7 +152,7 @@ void openai_send_chatrequest( openai_datatransfer_t* __data ){ curl_easy_cleanup( curl ); free( request_data ); free( response_data.ptr ); - __data -> response = text; + data -> response = text; request_working = false; return; } From 5d1c063237608cd8d0abb2bb08fe86ada9f5f02b Mon Sep 17 00:00:00 2001 From: Ace_Radom Date: Thu, 27 Apr 2023 09:28:14 +0200 Subject: [PATCH 07/16] Update README --- README.md | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 7a195a9..e7837b1 100644 --- a/README.md +++ b/README.md @@ -5,12 +5,10 @@ Chat with GPT in Terminal 这是一个将 @xiaoxx970 的使用Python实现的 [GPT-Term](https://github.com/xiaoxx970/chatgpt-in-terminal) 项目以C/C++实现的方式重构的计划 -目标为用C++实现CLI界面交互 C实现后端 尽可能还原原版界面和各项功能 但在遇到还原功能困难的时候会选择用其他的实现方法替代 +目标为用近乎纯C实现CLI界面交互和后端 尽可能还原原版界面和各项功能 但在遇到还原功能困难的时候会选择用其他的实现方法替代 开发工作在Linux环境下进行 由于这个项目更多是出于好玩和C语言练习的目的而发起的 目前不会太考虑可移植性问题 -~~不要问我为什么用C++做前端 问就是尝试用C做前端时候宽字符报错了一晚上~~ - 自现在起 main分支下是最近的功能实现版本 开发分支位于dev ### 项目进度 @@ -32,13 +30,13 @@ Chat with GPT in Terminal - tokens 统计 逐步实现 `/tokens` 命令 - credit 统计 逐步实现 `/usage` 命令 - `/copy` 命令 包括整个回答和代码段的拷贝 +- 绑定Tabs 实现斜杠命令自动补全 +- 在CLI上的斜杠命令匹配提示 +- 在输入未定义的斜杠命令时提示一个用户最有可能想输入的命令 已经计划实现 但在研究实现方法的功能: - 多行模式 -- 绑定Tabs 实现斜杠命令自动补全 -- 在CLI上的斜杠命令匹配提示 -- 在输入未定义的斜杠命令时提示一个用户最有可能想输入的命令 - 守护线程和自动标题生成 - 聊天记录保存和加载 @@ -52,9 +50,7 @@ Chat with GPT in Terminal ### 目前已知的问题 -- 在Debian 11.6, zsh下测试中发现:中文无法正确退格(以确定为utf8编码) - - 目前不清楚是程序问题还是zsh的问题 但我编写的极其简单的 `cin >> string` 遇到了一模一样的问题 若这个问题普遍存在将是个很头疼的事 但以我的能力可能是没什么办法 `setlocale` 什么的都试过了 +- 用户输入数据处理依赖于GNU Readline 之前的中文退格问题已经解决 但在中文退格时仍有可能删除掉行开头的 `>` (非严重问题) ### 项目依赖 @@ -62,4 +58,5 @@ Chat with GPT in Terminal | --- | --- | --- | --- | | `argtable` | 2.13 | 命令行参数解析 | `sudo apt install libargtable2-dev` | | `curl4-openssl` | 7.74.0 | API对接 | `sudo apt install libcurl4-openssl-dev` | -| `jansson` | 2.13.1 | json解析 | `sudo apt install libjansson-dev` | \ No newline at end of file +| `jansson` | 2.13.1 | json解析 | `sudo apt install libjansson-dev` | +| `GNU readline` | 8.1 | 命令行输入 历史记录 Tab补全 | `sudo apt install libreadline-dev` | \ No newline at end of file From e88c9ded14729462d9d02ef3c0e520d8669c072e Mon Sep 17 00:00:00 2001 From: Ace_Radom Date: Thu, 27 Apr 2023 12:13:24 +0200 Subject: [PATCH 08/16] add count tokens lib --- include/openai_api.h | 1 + include/openai_api/tiktokens.h | 6 + src/openai_api/rstiktokens/.gitignore | 1 + src/openai_api/rstiktokens/Cargo.lock | 280 ++++++++++++++++++++++++++ src/openai_api/rstiktokens/Cargo.toml | 13 ++ src/openai_api/rstiktokens/src/lib.rs | 14 ++ src/openai_api/tiktokens.c | 1 + 7 files changed, 316 insertions(+) create mode 100644 include/openai_api/tiktokens.h create mode 100644 src/openai_api/rstiktokens/.gitignore create mode 100644 src/openai_api/rstiktokens/Cargo.lock create mode 100644 src/openai_api/rstiktokens/Cargo.toml create mode 100644 src/openai_api/rstiktokens/src/lib.rs create mode 100644 src/openai_api/tiktokens.c diff --git a/include/openai_api.h b/include/openai_api.h index 63fa884..8063829 100644 --- a/include/openai_api.h +++ b/include/openai_api.h @@ -17,6 +17,7 @@ extern "C" { #include"ezylog.h" #include"utils.h" +#include"openai_api/tiktokens.h" typedef struct { char* endpoint; diff --git a/include/openai_api/tiktokens.h b/include/openai_api/tiktokens.h new file mode 100644 index 0000000..da14e0e --- /dev/null +++ b/include/openai_api/tiktokens.h @@ -0,0 +1,6 @@ +#ifndef _TIKTOKENS_H_ +#define _TIKTOKENS_H_ + + + +#endif \ No newline at end of file diff --git a/src/openai_api/rstiktokens/.gitignore b/src/openai_api/rstiktokens/.gitignore new file mode 100644 index 0000000..c41cc9e --- /dev/null +++ b/src/openai_api/rstiktokens/.gitignore @@ -0,0 +1 @@ +/target \ No newline at end of file diff --git a/src/openai_api/rstiktokens/Cargo.lock b/src/openai_api/rstiktokens/Cargo.lock new file mode 100644 index 0000000..d1d621e --- /dev/null +++ b/src/openai_api/rstiktokens/Cargo.lock @@ -0,0 +1,280 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "aho-corasick" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67fc08ce920c31afb70f013dcce1bfc3a3195de6a228474e45e1f145b36f8d04" +dependencies = [ + "memchr", +] + +[[package]] +name = "anyhow" +version = "1.0.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7de8ce5e0f9f8d88245311066a578d72b7af3e7088f32783804676302df237e4" + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "base64" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a" + +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bstr" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d4260bcc2e8fc9df1eac4919a720effeb63a3f0952f5bf4944adfa18897f09" +dependencies = [ + "memchr", + "once_cell", + "regex-automata", + "serde", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "fancy-regex" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b95f7c0680e4142284cf8b22c14a476e87d61b004a3a0861872b32ef7ead40a2" +dependencies = [ + "bit-set", + "regex", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.142" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a987beff54b60ffa6d51982e1aa1146bc42f19bd26be28b0586f252fccf5317" + +[[package]] +name = "lock_api" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "once_cell" +version = "1.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-sys", +] + +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af83e617f331cc6ae2da5443c602dfa5af81e517212d9d611a5b3ba1777b5370" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" + +[[package]] +name = "regex-syntax" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5996294f19bd3aae0453a862ad728f60e6600695733dd5df01da90c54363a3c" + +[[package]] +name = "rstiktokens" +version = "0.1.0" +dependencies = [ + "tiktoken-rs", +] + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "serde" +version = "1.0.160" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb2f3770c8bce3bcda7e149193a069a0f4365bda1fa5cd88e03bca26afc1216c" + +[[package]] +name = "smallvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" + +[[package]] +name = "tiktoken-rs" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ba161c549e2c0686f35f5d920e63fad5cafba2c28ad2caceaf07e5d9fa6e8c4" +dependencies = [ + "anyhow", + "base64", + "bstr", + "fancy-regex", + "lazy_static", + "parking_lot", + "rustc-hash", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" diff --git a/src/openai_api/rstiktokens/Cargo.toml b/src/openai_api/rstiktokens/Cargo.toml new file mode 100644 index 0000000..4e70528 --- /dev/null +++ b/src/openai_api/rstiktokens/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "rstiktokens" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +tiktoken-rs = "0.4.2" + +[lib] +crate-type = ["staticlib"] +name = "tiktokens" \ No newline at end of file diff --git a/src/openai_api/rstiktokens/src/lib.rs b/src/openai_api/rstiktokens/src/lib.rs new file mode 100644 index 0000000..7d12d9a --- /dev/null +++ b/src/openai_api/rstiktokens/src/lib.rs @@ -0,0 +1,14 @@ +pub fn add(left: usize, right: usize) -> usize { + left + right +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_works() { + let result = add(2, 2); + assert_eq!(result, 4); + } +} diff --git a/src/openai_api/tiktokens.c b/src/openai_api/tiktokens.c new file mode 100644 index 0000000..72760fe --- /dev/null +++ b/src/openai_api/tiktokens.c @@ -0,0 +1 @@ +#include"openai_api/tiktokens.h" \ No newline at end of file From b6de27e603baef16b3ee190c194a828c9442f7af Mon Sep 17 00:00:00 2001 From: Ace_Radom Date: Thu, 27 Apr 2023 14:04:21 +0200 Subject: [PATCH 09/16] finish C <-> Rust FFI, add static lib link in CMakeLists --- src/openai_api/CMakeLists.txt | 19 ++++++++++++++++++- src/openai_api/rstiktokens/Cargo.lock | 1 + src/openai_api/rstiktokens/Cargo.toml | 1 + src/openai_api/rstiktokens/src/lib.rs | 23 ++++++++++++----------- 4 files changed, 32 insertions(+), 12 deletions(-) diff --git a/src/openai_api/CMakeLists.txt b/src/openai_api/CMakeLists.txt index 035f6b8..104d493 100644 --- a/src/openai_api/CMakeLists.txt +++ b/src/openai_api/CMakeLists.txt @@ -3,13 +3,30 @@ project(openai_api VERSION 1.0) # openai_api subproject: requesting information from the openai api using the curl library +if(CMAKE_BUILD_TYPE STREQUAL "Debug") + set(CARGO_ARG "build") + set(TARGET_DIR "debug") +else() + set(CARGO_ARG "build --release") + set(TARGET_DIR "release") +endif() +set(rstiktokens_a ${CMAKE_SOURCE_DIR}/src/openai_api/rstiktokens/target/${TARGET_DIR}/libtiktokens.a) + +add_custom_target(rstiktokens ALL + COMMAND cargo ${CARGO_ARG} + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/src/openai_api/rstiktokens + COMMENT "Building Rust object rstiktokens" +) + aux_source_directory(. OPENAI_API_SRC) add_library(openaiapi SHARED ${OPENAI_API_SRC} ) +add_dependencies(openaiapi rstiktokens) target_link_libraries(openaiapi curl pthread jansson ezylog -) \ No newline at end of file + ${rstiktokens_a} +) diff --git a/src/openai_api/rstiktokens/Cargo.lock b/src/openai_api/rstiktokens/Cargo.lock index d1d621e..17de3ca 100644 --- a/src/openai_api/rstiktokens/Cargo.lock +++ b/src/openai_api/rstiktokens/Cargo.lock @@ -171,6 +171,7 @@ checksum = "a5996294f19bd3aae0453a862ad728f60e6600695733dd5df01da90c54363a3c" name = "rstiktokens" version = "0.1.0" dependencies = [ + "libc", "tiktoken-rs", ] diff --git a/src/openai_api/rstiktokens/Cargo.toml b/src/openai_api/rstiktokens/Cargo.toml index 4e70528..8cd8a43 100644 --- a/src/openai_api/rstiktokens/Cargo.toml +++ b/src/openai_api/rstiktokens/Cargo.toml @@ -6,6 +6,7 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +libc = "*" tiktoken-rs = "0.4.2" [lib] diff --git a/src/openai_api/rstiktokens/src/lib.rs b/src/openai_api/rstiktokens/src/lib.rs index 7d12d9a..6434fcb 100644 --- a/src/openai_api/rstiktokens/src/lib.rs +++ b/src/openai_api/rstiktokens/src/lib.rs @@ -1,14 +1,15 @@ -pub fn add(left: usize, right: usize) -> usize { - left + right -} +extern crate libc; +use libc::c_char; +use std::ffi::CStr; + +use tiktoken_rs::cl100k_base; -#[cfg(test)] -mod tests { - use super::*; +#[no_mangle] +pub extern "C" fn count_tokens( ptr: *const c_char ) -> usize { + let c_str = unsafe { CStr::from_ptr( ptr ) }; + let msg = c_str.to_str().expect( "" ); - #[test] - fn it_works() { - let result = add(2, 2); - assert_eq!(result, 4); - } + let bpe = cl100k_base().unwrap(); + let tokens = bpe.encode_with_special_tokens( msg ); + return tokens.len(); } From a905be4dee19ff8dadfb06fc9673539415debc3e Mon Sep 17 00:00:00 2001 From: Ace_Radom Date: Thu, 27 Apr 2023 15:22:16 +0200 Subject: [PATCH 10/16] count prompt tokens at openai init --- include/openai_api.h | 2 +- include/openai_api/tiktokens.h | 17 +++++++++++++++++ src/openai_api/openai_api.c | 4 ++++ src/openai_api/rstiktokens/src/lib.rs | 12 ++++++++++-- src/openai_api/tiktokens.c | 15 ++++++++++++++- 5 files changed, 46 insertions(+), 4 deletions(-) diff --git a/include/openai_api.h b/include/openai_api.h index 8063829..93f8d91 100644 --- a/include/openai_api.h +++ b/include/openai_api.h @@ -25,7 +25,7 @@ typedef struct { json_t* messages; char* model; int tokens_limit; - long long total_tokens_spent; + long total_tokens_spent; int current_tokens; char* title; diff --git a/include/openai_api/tiktokens.h b/include/openai_api/tiktokens.h index da14e0e..93c9e12 100644 --- a/include/openai_api/tiktokens.h +++ b/include/openai_api/tiktokens.h @@ -1,6 +1,23 @@ #ifndef _TIKTOKENS_H_ #define _TIKTOKENS_H_ +#ifdef __cplusplus +extern "C" { +#endif +#include +#include +#include + +#include + +extern long count_tokens_cl100k_base( const char* ptr ); +// this is a rust FFI used to count tokens of a message with cl100k_base + +long count_tokens_message( const json_t* __msg ); + +#ifdef __cplusplus +} +#endif #endif \ No newline at end of file diff --git a/src/openai_api/openai_api.c b/src/openai_api/openai_api.c index 53f121f..f201232 100644 --- a/src/openai_api/openai_api.c +++ b/src/openai_api/openai_api.c @@ -56,6 +56,10 @@ void openai_init(){ json_array_append_new( openai -> messages , prompt ); // prompt init + openai -> current_tokens += count_tokens_message( prompt ); + ezylog_logdebug( logger , "prompt tokens: %ld" , openai -> current_tokens ); + // count prompt tokens and add to current_tokens + curl_global_init( CURL_GLOBAL_ALL ); // curl init diff --git a/src/openai_api/rstiktokens/src/lib.rs b/src/openai_api/rstiktokens/src/lib.rs index 6434fcb..5fc51a8 100644 --- a/src/openai_api/rstiktokens/src/lib.rs +++ b/src/openai_api/rstiktokens/src/lib.rs @@ -4,10 +4,18 @@ use std::ffi::CStr; use tiktoken_rs::cl100k_base; +/// Count tokens of a message (cl100k_base) #[no_mangle] -pub extern "C" fn count_tokens( ptr: *const c_char ) -> usize { +pub extern "C" fn count_tokens_cl100k_base( ptr: *const c_char ) -> usize { let c_str = unsafe { CStr::from_ptr( ptr ) }; - let msg = c_str.to_str().expect( "" ); + let convert_result = c_str.to_str(); + let msg = match convert_result { + Ok( s ) => s, + Err( _e ) => { + return 0; + } + }; + // change C string into a Rust string let bpe = cl100k_base().unwrap(); let tokens = bpe.encode_with_special_tokens( msg ); diff --git a/src/openai_api/tiktokens.c b/src/openai_api/tiktokens.c index 72760fe..cdbc235 100644 --- a/src/openai_api/tiktokens.c +++ b/src/openai_api/tiktokens.c @@ -1 +1,14 @@ -#include"openai_api/tiktokens.h" \ No newline at end of file +#include"openai_api/tiktokens.h" + +/** + * @note the input should be something like: {"role": "user", "content": prompt} +*/ +long count_tokens_message( const json_t* __msg ){ + char* role = json_string_value( json_object_get( __msg , "role" ) ); + char* content = json_string_value( json_object_get( __msg , "content" ) ); + char* full_msg = ( char* ) malloc( strlen( role ) + strlen( content ) + 32 ); + sprintf( full_msg , "role: %s, content: %s" , role , content ); + long tokens = count_tokens_cl100k_base( full_msg ); + free( full_msg ); + return tokens; +} \ No newline at end of file From c66bf0fa8cceb306bb205791e00402598a28d424 Mon Sep 17 00:00:00 2001 From: Ace_Radom Date: Thu, 27 Apr 2023 16:30:53 +0200 Subject: [PATCH 11/16] add tokens count --- main.c | 7 +++---- src/openai_api/openai_api.c | 3 +++ 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/main.c b/main.c index 73fd19c..e30d8ab 100644 --- a/main.c +++ b/main.c @@ -80,21 +80,20 @@ int main( int argc , char** argv ){ // from here: cGPTerm main service - ezylog_logdebug( logger , "config set not triggered, start launching cGPTerm main service" ); - + ezylog_logdebug( logger , "config set not triggered, start initializing openai service" ); openai_init(); ezylog_logdebug( logger , "openai service initialization complete" ); printf( "Hi, welcome to chat with GPT. Type `/help` to display available commands.\n" ); ezylog_loginfo( logger , "cGPTerm main service launch" ); int CLI_returncode = start_CLI(); - // transfer into CXX - CLI is written in C++ because of unknown error with zh characters + printf( "Total tokens spent: %ld\n" , openai -> total_tokens_spent ); + ezylog_loginfo( logger , "Total tokens spent: %d" , openai -> total_tokens_spent ); openai_free(); stopmain: cconfig(); - printf( "Goodbye\n" ); ezylog_loginfo( logger , "cGPTerm master process shutting down..." ); ezylog_close( logger ); return 0; diff --git a/src/openai_api/openai_api.c b/src/openai_api/openai_api.c index f201232..f136241 100644 --- a/src/openai_api/openai_api.c +++ b/src/openai_api/openai_api.c @@ -150,6 +150,9 @@ void openai_send_chatrequest( void* __data ){ text = json_string_value( json_object_get( response_msg , "content" ) ); ezylog_loginfo( logger , "ChatGPT: %s" , text ); ezylog_logdebug( logger , "GPT Response raw: %s" , response_data.ptr ); + openai -> current_tokens = json_integer_value( json_object_get( json_object_get( root , "usage" ) , "total_tokens" ) ); + openai -> total_tokens_spent += openai -> current_tokens; + // count tokens } // response code 200 OK (most likely) request_stop: From 971f3b98a427e4f7356749445ef7a56b3bb70212 Mon Sep 17 00:00:00 2001 From: Ace_Radom Date: Thu, 27 Apr 2023 16:51:52 +0200 Subject: [PATCH 12/16] add history input record function --- src/cli/cli.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/cli/cli.c b/src/cli/cli.c index 7e5e8ec..8fcc9e6 100644 --- a/src/cli/cli.c +++ b/src/cli/cli.c @@ -9,12 +9,14 @@ int start_CLI(){ while ( 1 ) { char* input; - printf( "> " ); - input = readline( NULL ); + input = readline( "> " ); if ( !input || *input == '\0' ) continue; if ( strcmp( input , "quit" ) == 0 ) break; + + add_history( input ); + // add input to history list turn_off_echo(); write_ANSI( HIDE_CURSOR ); From 126c63ca0a927a156c2bcded9b4cd444ac144085 Mon Sep 17 00:00:00 2001 From: Ace_Radom Date: Thu, 27 Apr 2023 19:34:54 +0200 Subject: [PATCH 13/16] add new slash cmds: /tokens /timeout /help /exit --- include/cli/slashcmd.h | 28 ++++++++++ include/cli/toolkit.h | 5 ++ src/cli/CMakeLists.txt | 2 + src/cli/cli.c | 22 +++++--- src/cli/slashcmd.c | 113 +++++++++++++++++++++++++++++++++++++++++ src/cli/toolkit.c | 20 ++++++++ 6 files changed, 184 insertions(+), 6 deletions(-) create mode 100644 include/cli/slashcmd.h create mode 100644 src/cli/slashcmd.c diff --git a/include/cli/slashcmd.h b/include/cli/slashcmd.h new file mode 100644 index 0000000..0938e59 --- /dev/null +++ b/include/cli/slashcmd.h @@ -0,0 +1,28 @@ +#ifndef _SLASHCMD_H_ +#define _SLASHCMD_H_ + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include +#include + +#include + +#include"openai_api.h" +#include"ezylog.h" +#include"utils.h" + +extern const char* slash_commands[]; + +int handle_slash_command( const char* __slashcmd ); + +void print_slash_command_help(); + +#ifdef __cplusplus +} +#endif + +#endif \ No newline at end of file diff --git a/include/cli/toolkit.h b/include/cli/toolkit.h index 01b7f13..b720a5d 100644 --- a/include/cli/toolkit.h +++ b/include/cli/toolkit.h @@ -11,6 +11,8 @@ extern "C" { #include #include +#include"slashcmd.h" + #define HIDE_CURSOR "\033[?25l" #define SHOW_CURSOR "\033[?25h" @@ -25,6 +27,9 @@ void write_ANSI( const char* __ANSI ); void print_wait_msg( const char* __msg ); + +char* trim( char* __str ); + #ifdef __cplusplus } #endif diff --git a/src/cli/CMakeLists.txt b/src/cli/CMakeLists.txt index 5f14bce..ecef8f0 100644 --- a/src/cli/CMakeLists.txt +++ b/src/cli/CMakeLists.txt @@ -7,6 +7,8 @@ add_library(cli SHARED ) target_link_libraries(cli openaiapi + ezylog + utils pthread readline ) diff --git a/src/cli/cli.c b/src/cli/cli.c index 8fcc9e6..80ed238 100644 --- a/src/cli/cli.c +++ b/src/cli/cli.c @@ -10,20 +10,30 @@ int start_CLI(){ { char* input; input = readline( "> " ); - if ( !input || *input == '\0' ) + char* input_trim = trim( input ); + if ( !input_trim || *input_trim == '\0' ) continue; - if ( strcmp( input , "quit" ) == 0 ) - break; + if ( input_trim[0] == '/' ) + { + int hscrc = handle_slash_command( input_trim ); // handle slash command return code + add_history( input_trim ); + free( input_trim ); + if ( hscrc == -1 ) + break; + // /exit input, break + continue; + } + // input parts - add_history( input ); + add_history( input_trim ); // add input to history list turn_off_echo(); write_ANSI( HIDE_CURSOR ); // turn off echo; hide cursor openai_datatransfer_t data; - data.msg = ( char* ) malloc( strlen( input ) + 1 ); - strcpy( data.msg , input ); + data.msg = ( char* ) malloc( strlen( input_trim ) + 1 ); + strcpy( data.msg , input_trim ); data.response = NULL; // build transfer data pthread_t send_request; diff --git a/src/cli/slashcmd.c b/src/cli/slashcmd.c new file mode 100644 index 0000000..2621da8 --- /dev/null +++ b/src/cli/slashcmd.c @@ -0,0 +1,113 @@ +#include"cli/slashcmd.h" + +const char* slash_commands[] = { + "/tokens", + "/timeout", + "/help", + "/exit" +}; + +/** + * @brief handle slash command + * + * @param __slashcmd the slash command input + * + * @return 0 when slash command successfully handled; 1 when unrecognized slash command occured; -1 when /exit triggered +*/ +int handle_slash_command( const char* __slashcmd ){ + if ( strcmp( __slashcmd , "/tokens" ) == 0 ) + { + ezylog_logdebug( logger , "/tokens command triggered" ); + printf( "Total Tokens Spent: %ld\n" , openai -> total_tokens_spent ); + printf( "Current Tokens: %d/%d\n" , openai -> current_tokens , openai -> tokens_limit ); + return 0; + } // /tokens + + if ( strncmp( __slashcmd , "/timeout" , 8 ) == 0 ) + { + ezylog_logdebug( logger , "/timeout command triggered" ); + double new_timeout; + char* new_timeout_str; + + if ( strlen( __slashcmd ) == 8 ) + goto ask_timeout; + // only input "/timeout", goto ask timeout length + + char* temp = ( char* ) malloc( strlen( __slashcmd ) + 1 ); + strcpy( temp , __slashcmd ); + char* token = strtok( temp , " " ); + token = strtok( NULL , " " ); + // get the second part str + new_timeout = strtod( token , NULL ); + if ( new_timeout != 0 ) + { + if ( new_timeout > 0 ) + { + OPENAI_API_TIMEOUT = new_timeout; + printf( "API timeout set to %.2lfs\n" , OPENAI_API_TIMEOUT ); + ezylog_loginfo( logger , "API timeout set to %lfs" , OPENAI_API_TIMEOUT ); + free( temp ); + return 0; + } + else + { + printf( "API timeout cannot be less than 0s\n" ); + } // illegal input + } + else + { + if ( new_timeout == 0 && token[0] == '0' ) + printf( "API timeout cannot be 0s\n" ); + else + printf( "API timeout must be an integer or a float\n" ); + } // illegal input + free( temp ); + + ask_timeout: + new_timeout_str = readline( "Please input new API timeout: " ); + new_timeout = strtod( new_timeout_str , NULL ); + if ( new_timeout != 0 ) + { + if ( new_timeout > 0 ) + { + OPENAI_API_TIMEOUT = new_timeout; + printf( "API timeout set to %.2lfs\n" , OPENAI_API_TIMEOUT ); + ezylog_loginfo( logger , "API timeout set to %lfs" , OPENAI_API_TIMEOUT ); + free( new_timeout_str ); + return 0; + } + else + { + printf( "API timeout cannot be less than 0s\n" ); + } // illegal input + } + else + { + if ( new_timeout == 0 && new_timeout_str[0] == '0' ) + printf( "API timeout cannot be 0s\n" ); + else + printf( "API timeout must be an integer or a float\n" ); + } // illegal input + goto ask_timeout; + } // /timeout TIMEOUT + + if ( strcmp( __slashcmd , "/help" ) == 0 ) + { + print_slash_command_help(); + return 0; + } // /help + if ( strcmp( __slashcmd , "/exit" ) == 0 ) + { + return -1; + } // /exit, ready to break + return 1; +} + +void print_slash_command_help(){ + printf( "Available commands:\n" ); + printf( " /tokens\t\t\t- Show the total tokens spent and the tokens for the current conversation\n" ); + printf( " /timeout [new_timeout]\t- Modify the api timeout\n" ); + printf( " /help\t\t\t- Show this help message\n" ); + printf( " /exit\t\t\t- Exit the application\n" ); + return; +} \ No newline at end of file diff --git a/src/cli/toolkit.c b/src/cli/toolkit.c index 7811ed3..85034f8 100644 --- a/src/cli/toolkit.c +++ b/src/cli/toolkit.c @@ -37,4 +37,24 @@ void print_wait_msg( const char* __msg ){ usleep( 100000 ); // sleep 100 ms return; +} + +/** + * @brief erase space at begin and end of a str +*/ +char* trim( char* __str ){ + int i = 0; + int j = strlen( __str ) - 1; + while ( __str[i] == ' ' ) + ++i; + while ( __str[j] == ' ' ) + --j; + if ( i > j ) + return NULL; + // here: i > j means there are only spaces in this str + // if continue, it will lead to segmentation fault, therefore return NULL here + char* newstr = ( char* ) malloc( strlen( __str ) ); + strncpy( newstr , __str + i , j - i + 1 ); + newstr[j-i+1] = '\0'; + return newstr; } \ No newline at end of file From 7d9d4f68a7ea05714410fdc246ec426ca0405528 Mon Sep 17 00:00:00 2001 From: Ace_Radom Date: Thu, 27 Apr 2023 20:08:33 +0200 Subject: [PATCH 14/16] finish slash cmds auto-completion --- include/cli/toolkit.h | 13 ++++++++++++- src/cli/cli.c | 2 ++ src/cli/slashcmd.c | 3 ++- src/cli/toolkit.c | 27 +++++++++++++++++++++++++++ 4 files changed, 43 insertions(+), 2 deletions(-) diff --git a/include/cli/toolkit.h b/include/cli/toolkit.h index b720a5d..3b0495f 100644 --- a/include/cli/toolkit.h +++ b/include/cli/toolkit.h @@ -11,6 +11,8 @@ extern "C" { #include #include +#include + #include"slashcmd.h" #define HIDE_CURSOR "\033[?25l" @@ -18,15 +20,24 @@ extern "C" { extern struct termios ori_attr; +// =============================================== +// ================== CLI Tools ================== +// =============================================== + void get_original_terattr(); void reset_terattr(); - void turn_off_echo(); void write_ANSI( const char* __ANSI ); void print_wait_msg( const char* __msg ); +char** rl_attempted_completion_callback( const char* text , int start , int end ); +char* rl_completion_search( const char* text , int state ); + +// =============================================== +// ================== str Tools ================== +// =============================================== char* trim( char* __str ); diff --git a/src/cli/cli.c b/src/cli/cli.c index 80ed238..a2030b1 100644 --- a/src/cli/cli.c +++ b/src/cli/cli.c @@ -6,6 +6,8 @@ int start_CLI(){ get_original_terattr(); + rl_attempted_completion_function = rl_attempted_completion_callback; + while ( 1 ) { char* input; diff --git a/src/cli/slashcmd.c b/src/cli/slashcmd.c index 2621da8..6d1b632 100644 --- a/src/cli/slashcmd.c +++ b/src/cli/slashcmd.c @@ -4,7 +4,8 @@ const char* slash_commands[] = { "/tokens", "/timeout", "/help", - "/exit" + "/exit", + NULL }; /** diff --git a/src/cli/toolkit.c b/src/cli/toolkit.c index 85034f8..978f9ce 100644 --- a/src/cli/toolkit.c +++ b/src/cli/toolkit.c @@ -39,6 +39,33 @@ void print_wait_msg( const char* __msg ){ return; } +char* rl_completion_slash_command_search( const char* text , int state ){ + static int list_index , len; + const char* cmd; + + if ( !state ) + { + list_index = 0; + len = strlen( text ); + } // init + + while ( ( cmd = slash_commands[list_index++] ) ) + { + if ( strncmp( cmd , text , len ) == 0 ) + { + return strdup( cmd ); + } + } + return NULL; +} + +char** rl_attempted_completion_callback( const char* text , int start , int end ){ + char** matches = ( char** ) NULL; + matches = rl_completion_matches( text , rl_completion_slash_command_search ); + return matches; +} + + /** * @brief erase space at begin and end of a str */ From 3a7135522b07151745a8321d8d385705b491a231 Mon Sep 17 00:00:00 2001 From: Ace_Radom Date: Thu, 27 Apr 2023 20:14:08 +0200 Subject: [PATCH 15/16] Update README --- README.md | 53 ++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 38 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index e7837b1..52a4ebe 100644 --- a/README.md +++ b/README.md @@ -3,9 +3,9 @@ Chat with GPT in Terminal ## 简介 -这是一个将 @xiaoxx970 的使用Python实现的 [GPT-Term](https://github.com/xiaoxx970/chatgpt-in-terminal) 项目以C/C++实现的方式重构的计划 +这是一个将 @xiaoxx970 的使用Python实现的 [GPT-Term](https://github.com/xiaoxx970/chatgpt-in-terminal) 项目以近乎纯C实现的方式重构的计划 -目标为用近乎纯C实现CLI界面交互和后端 尽可能还原原版界面和各项功能 但在遇到还原功能困难的时候会选择用其他的实现方法替代 +目标为用近乎纯C实现CLI界面交互和后端 尽可能还原原版界面和各项功能 但在遇到还原功能困难的时候会选择用其他的实现方法替代【比如tokens计算使用了Rust现成的库 也因此编译需要Rust环境】 开发工作在Linux环境下进行 由于这个项目更多是出于好玩和C语言练习的目的而发起的 目前不会太考虑可移植性问题 @@ -19,20 +19,22 @@ Chat with GPT in Terminal - 自动创建 `config.ini` 配置文件 - 通过命令行对 `config.ini` 内的值进行更改 - 自建log库 支持 `DEBUG` `INFO` `ERROR` `FATAL` 层级 -- 基本的API访问:可以正常与ChatGPT交流 +- 基本的API访问:可以正常与ChatGPT交流和解析错误 - 基本的交互界面 - 在等待GPT回复时显示 `ChatGPT is thinking...` - - 在等待回复时锁死终端 不回显并隐藏光标 在回复后解锁 + - 历史记录功能 记录在本次运行中的每一次输入 + - 已经实现的斜杠命令可以按Tab补全【此处的补全逻辑和原版不同 在有多项可能时不会补全而是显示所有可能命令】 +- 基于 [tiktoken-rs](https://github.com/zurawiki/tiktoken-rs) 的tokens计算 +- 部分斜杠命令 (`/tokens` `/timeout` `/help` `/exit`) 目前正在开发的功能: - 实现部分底层的斜杠命令:`/timeout` `/model` `/last` `/help` `/exit` -- tokens 统计 逐步实现 `/tokens` 命令 - credit 统计 逐步实现 `/usage` 命令 - `/copy` 命令 包括整个回答和代码段的拷贝 -- 绑定Tabs 实现斜杠命令自动补全 -- 在CLI上的斜杠命令匹配提示 +- 优化斜杠命令自动补全的界面 - 在输入未定义的斜杠命令时提示一个用户最有可能想输入的命令 +- 自建/封装富文本库 以一种类似于python的rich库的方式输出富文本 已经计划实现 但在研究实现方法的功能: @@ -42,21 +44,42 @@ Chat with GPT in Terminal 目前计划中 但不清楚能力是否允许实现的功能: -- 自建/封装富文本库 以一种类似于python的rich库的方式输出富文本 - - 目的是为了让界面更好看 ~~不然还能因为什么啊喂~~ - 自建段落式Markdown解析库 用于适配流式输出的文本解析【若找到更好的替代方法会取消】 - 流式输出 +- 使用C/C++实现一套tiktoken分词库 替代目前使用的Rust分词库库 + +### 编译 项目依赖 -### 目前已知的问题 +编译需要GCC和Rust环境 -- 用户输入数据处理依赖于GNU Readline 之前的中文退格问题已经解决 但在中文退格时仍有可能删除掉行开头的 `>` (非严重问题) +首先 clone本仓库 -### 项目依赖 +``` +git clone https://github.com/Ace-Radom/cGPTerm +``` -| 依赖库名 | 开发用版本 | 库功能 | 安装命令 +然后 安装所有C的依赖项 + +| 依赖库名 | 开发用版本 | 库功能 | 安装命令 (Debian, Ubuntu) | | --- | --- | --- | --- | | `argtable` | 2.13 | 命令行参数解析 | `sudo apt install libargtable2-dev` | | `curl4-openssl` | 7.74.0 | API对接 | `sudo apt install libcurl4-openssl-dev` | | `jansson` | 2.13.1 | json解析 | `sudo apt install libjansson-dev` | -| `GNU readline` | 8.1 | 命令行输入 历史记录 Tab补全 | `sudo apt install libreadline-dev` | \ No newline at end of file +| `GNU readline` | 8.1 | 命令行输入 历史记录 Tab补全 | `sudo apt install libreadline-dev` | + +随后 依次执行以下命令 + +```shell +mkdir build && cd build +cmake .. +make +``` + +如果一切正常 所有生成的文件都会被存放在项目根文件夹下的bin文件夹内 + +不过此处编译的是Debug版本 若需编译Release版本可以使用 + +``` +cmake -DCMAKE_BUILD_TYPE=Release .. +make +``` \ No newline at end of file From 98c22eab9b486ef3a8527ef27bdeca5962fb941b Mon Sep 17 00:00:00 2001 From: Ace_Radom Date: Thu, 27 Apr 2023 20:28:24 +0200 Subject: [PATCH 16/16] Fixed: Building failed when compiling rust libs under release mode --- src/openai_api/CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/openai_api/CMakeLists.txt b/src/openai_api/CMakeLists.txt index 104d493..3836a4d 100644 --- a/src/openai_api/CMakeLists.txt +++ b/src/openai_api/CMakeLists.txt @@ -4,10 +4,10 @@ project(openai_api VERSION 1.0) # openai_api subproject: requesting information from the openai api using the curl library if(CMAKE_BUILD_TYPE STREQUAL "Debug") - set(CARGO_ARG "build") + set(CARGO_ARG build) set(TARGET_DIR "debug") else() - set(CARGO_ARG "build --release") + set(CARGO_ARG build --release) set(TARGET_DIR "release") endif() set(rstiktokens_a ${CMAKE_SOURCE_DIR}/src/openai_api/rstiktokens/target/${TARGET_DIR}/libtiktokens.a)