OpenAI class using hbCurl

Post Reply
User avatar
reinaldocrespo
Posts: 979
Joined: Thu Nov 17, 2005 5:49 pm
Location: Fort Lauderdale, FL

OpenAI class using hbCurl

Post by reinaldocrespo »

Hello Fivewinners;

For what's worth and in case anyone is interested, I share the class I wrote to query OpenAI for ICD10 diagnosis codes. It works really nice.

I think the only external function I use here is LogData() and I will share it if anyone wants it.

How you parse the response for the output you seek may be different.

Happy coding.

Reinaldo.

Code: Select all | Expand


#INCLUDE "hbClass.ch"
#INCLUDE "hbcurl.ch"

#DEFINE CRLF CHR(13)+CHR(10)

CLASS TOpenAI

   DATA hCurl 
   DATA httpcode
   DATA nError             INIT 0 

   DATA Response 
   DATA Prompt 

   DATA model              INIT "text-davinci-003"
   DATA cUrl               INIT "https://api.openai.com/v1/completions" 
   DATA max_tokens         INIT 2048
   DATA temperature        INIT 0 
   DATA top_p              INIT 1 
   DATA frequency_penalty  INIT 0.00
   DATA presence_penalty   INIT 0.00

   DATA cLogFile        INIT "TOpenAI.log"
   DATA nMaxLogSize     INIT 32768
   DATA lDebug          INIT .F. 

   METHOD New()
   METHOD End() 
   METHOD Send() 
   METHOD Reset() 

   //parses response fetching ICD10 codes coded by OpenAI.
   METHOD GetICD10sFromResponse() 

ENDCLASS 


//------------------------------------------------------------------------------------------------
METHOD New() CLASS TOpenAI

   ::hCurl := curl_easy_init()

RETURN Self 


//------------------------------------------------------------------------------------------------
METHOD Send() CLASS TOpenAI
   LOCAL aheaders
   Local httpcode
   Local cJson 
   Local h := { => }

   curl_easy_setopt( ::hCurl, HB_CURLOPT_POST, .T. )
   curl_easy_setopt( ::hCurl, HB_CURLOPT_URL, ::cUrl )

   //curl_easy_setopt( ::hCurl, HB_CURLOPT_PASSWORD, OPENAI_API_KEY )
   //http header could also be an array of headers
   aheaders := { "Content-Type: application/json", ;
                   "Authorization: Bearer " + OPENAI_API_KEY }
   
   curl_easy_setopt( ::hCurl, HB_CURLOPT_HTTPHEADER, aheaders )

   curl_easy_setopt( ::hCurl, HB_CURLOPT_USERNAME, '' )
   curl_easy_setopt( ::hCurl, HB_CURLOPT_DL_BUFF_SETUP )
   curl_easy_setopt( ::hCurl, HB_CURLOPT_SSL_VERIFYPEER, .F. )

   hb_HSet( h, "model", ::model )
   hb_HSet( h, "prompt", ::Prompt )
   hb_HSet( h, "max_tokens", ::max_tokens )
   hb_HSet( h, "temperature", ::temperature )
   hb_HSet( h, "frequency_penalty", ::frequency_penalty )
   hb_HSet( h, "presence_penalty", ::presence_penalty )

   cJson := hb_jsonEncode( h )
   curl_easy_setopt( ::hcurl, HB_CURLOPT_POSTFIELDS, cJson ) //cPostFields )

   ::nError := curl_easy_perform( ::hCurl ) 
   curl_easy_getinfo( ::hCurl, HB_CURLINFO_RESPONSE_CODE, @httpcode )

   ::httpcode := httpcode

   IF ::nError == HB_CURLE_OK

      ::Response = curl_easy_dl_buff_get( ::hCurl )

      IF ::lDebug 
         LogData( ::cLogFile, { "prompt", ::Prompt, "model", ::model }, ::nMaxLogSize )
         LogData( ::cLogFile, ::Response, ::nMaxLogSize )
      ENDIF 
   Else 

      LogData( ::cLogFile, { "TTOpenAI prompt was:", ::Prompt }, ::nMaxLogSize )
      LogData( ::cLogFile, { "Error Num:", ::nError, "Httpcode:", ::httpcode }, ::nMaxLogSize )
      LogData( ::cLogFile, { "Response:", ::Response }, ::nMaxLogSize )
      LogData( ::cLogFile, curl_easy_strerror( ::nError ), ::nMaxLogSize )

   ENDIF 

return ::Response            

//------------------------------------------------------------------------------------------------
METHOD Reset() CLASS TOpenAI
      
   curl_easy_reset( ::hCurl )
   ::nError := HB_CURLE_OK 
   ::Response := Nil 

return NIL 



//------------------------------------------------------------------------------------------------
METHOD End() CLASS TOpenAI

   curl_easy_cleanup( ::hCurl )
   ::hCurl := Nil  

return NIL 


/*------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------*/
METHOD GetICD10sFromResponse() CLASS TOpenAI 
   LOCAL aICD10s := {}
   LOCAL aParsed, iter, acodelines, code 
   LOCAL a := {}
   LOCAL aKeys 
   LOCAL hResponse := hb_jsonDecode( ::Response )

   if ::lDebug

      LogData( ::cLogFile, { "hb_isHash( hResponse )", hb_isHash( hResponse ), ;
                           "hb_HHasKey( hResponse, 'choices' )", hb_HHasKey( hResponse, "choices" ) } )
      aKeys := HGetKeys( hResponse )
      LogData( ::cLogFile, aKeys, ::nMaxLogSize )

   ENDIF


   IF hb_isHash( hResponse ) .AND. ;
      hb_HHasKey( hResponse, "choices" ) 

      if HB_ISARRAY( hResponse[ "choices" ] )
         a := hResponse[ "choices" ]
      endif 

      FOR iter := 1 TO LEN( a )

         IF hb_HHasKey( a[ iter ], "text" )
         
            acodelines := hb_ATokens( a[ iter ][ "text" ], CRLF )

            FOR EACH Code IN acodelines

               aParsed := hb_ATokens( Code, "-" )
               AADD( aIcd10s, aParsed ) 

            NEXT 

         ENDIF

      NEXT iter

   ENDIF 

RETURN aICD10s 
 
User avatar
Antonio Linares
Site Admin
Posts: 42270
Joined: Thu Oct 06, 2005 5:47 pm
Location: Spain
Contact:

Re: OpenAI class using hbCurl

Post by Antonio Linares »

Dear Reinaldo,

Congratulations for porting it to use CURL and many thanks for sharing it.

Just curiosity, would you mind to show an example about how you query it ?
regards, saludos

Antonio Linares
www.fivetechsoft.com
User avatar
reinaldocrespo
Posts: 979
Joined: Thu Nov 17, 2005 5:49 pm
Location: Fort Lauderdale, FL

Re: OpenAI class using hbCurl

Post by reinaldocrespo »

Yes, it makes sense.

Here is sample code on how I use it. I'm pasting text that the program reads from dictations so you see how to feed OpenAI:

Code: Select all | Expand

      TEXT INTO cText
         ENDOVAGINAL SONOGRAM:
         Compared to previous study dated 08/24/2022 reporting tiny fibroid and endometrial polyp.
         Normal size, retroflexed uterus measuring 7 x 4.3 x 5 cm. Focal area of endometrial thickening measuring 9 mm suggesting an endometrial polyp, stable since previous study. Intramural fibroid in posterior fundal wall measuring 9 mm, also stable. Multiple cervical Nabothian cysts. Normal endocervical stripe. 
         Right ovary: 3.3 x 2.1 x 2.3 cm, with dominant follicle measuring 1.1 cm. 
         Left ovary: 3.7 x 2.3 x 2.2 cm, with small corpus luteum cyst measuring 8 mm. 
         No adnexal masses, or fluid collections.
         
         IMPRESSION:         
         SMALL ENDOMETRIAL POLYP AND INTRAMURAL FIBROID, WITHOUT SIGNIFICANT CHANGE SINCE PREVIOUS STUDY.
      ENDTEXT 

      oAI := TOpenAI():New() 
      oAI:lDebug := .T. 
      //Open AI models allowed for completions are:
      //text-davinci-003, text-davinci-002, text-curie-001, text-babbage-001, text-ada-001
      //text-ada is the fastest and least expensive.
      oAI:model  := "text-ada-001" //"text-davinci-003"
      oAI:Prompt := "Code with ICD10 diagnosis codes the following radiological imaging transcription: " + CRLF + cText 
      oAI:Send() 

      IF !Empty( oAI:Response ) 
          MsgInfo( oAI:Response ) 
      ENDIF 

 
Response in this case will be aJSon structure with ICD10 codes and descriptions. I parse the result and move to an xbrowse where coders work with it. After formatting the response it looks like this:
ICD10 codes:

Tiny fibroid: D25.0
Endometrial polyp: N84.0
Retroflexed uterus: N89.3
Endometrial thickening: N85.00
Intramural fibroid: D25.1
Nabothian cysts: N88.0
Dominant follicle: N83.0
Corpus luteum cyst: N83.1
No adnexal masses or fluid collections: R93.8
They no longer need to lookup codes on books or websites.
User avatar
reinaldocrespo
Posts: 979
Joined: Thu Nov 17, 2005 5:49 pm
Location: Fort Lauderdale, FL

Re: OpenAI class using hbCurl

Post by reinaldocrespo »

Here is another example:

Code: Select all | Expand

      TEXT INTO cText
         CHEST FOR RIBS:
         There is a non displaced fracture of the anterolateral aspect of the right 7th rib. 
         There are wire sutures in the sternum. There is cardiomegaly and there is elongation and tortuosity of the aorta.
      ENDTEXT 

      oAI := TOpenAI():New() 
      oAI:lDebug := .T. 
      //Open AI models allowed for completions are:
      //text-davinci-003, text-davinci-002, text-curie-001, text-babbage-001, text-ada-001
      //text-ada is the fastest and least expensive.
      oAI:model  := "text-ada-001" //"text-davinci-003"
      oAI:Prompt := "Code with ICD10 diagnosis codes the following radiological imaging transcription: " + CRLF + cText 
      oAI:Send() 

      IF !Empty( oAI:Response ) 
         ...do whatever with oAI:Response
     ENDIF 
 
After parsing the Json data received on Response, this is what I get:
ICD10 codes:
- Non-displaced fracture of the rib(s): S22.4-
- Wire sutures in the sternum: Z98.1
- Cardiomegaly: I51.7
- Elongation and tortuosity of the aorta: Q27.8
Hope it helps.
User avatar
Antonio Linares
Site Admin
Posts: 42270
Joined: Thu Oct 06, 2005 5:47 pm
Location: Spain
Contact:

Re: OpenAI class using hbCurl

Post by Antonio Linares »

Amazing... :-)
regards, saludos

Antonio Linares
www.fivetechsoft.com
User avatar
Otto
Posts: 6380
Joined: Fri Oct 07, 2005 7:07 pm
Contact:

Re: OpenAI class using hbCurl

Post by Otto »

Dear Antonio, dear Reinaldo,

Since I'm unsure about the costs, I'm currently using an offline version. For example, in a reception with multiple users making requests all day, hundreds of requests could be generated, and using the API could become expensive.
At the moment, users seem to expect information to be free, but that won't remain the case.

@ Reinado, we just had a great event. Here's a link:

https://pustertal-online.com/index.prg?recnr=781
:-) Powered by mod harbour

If there's interest in a new edition of a Fivewin/Harbour meeting, we'd be happy to organize it.


Image

Image

Image
********************************************************************
mod harbour - Vamos a la conquista de la Web
modharbour.org
https://www.facebook.com/groups/modharbour.club
********************************************************************
User avatar
reinaldocrespo
Posts: 979
Joined: Thu Nov 17, 2005 5:49 pm
Location: Fort Lauderdale, FL

Re: OpenAI class using hbCurl

Post by reinaldocrespo »

Dear Otto;

If you organize that meeting again, count me in. I will be the first the attend. Perhaps you can start a thread to count who else is interested. I bear witness that your last meeting was an event to be remembered forever.

@fivewinners--
On the subject of OpenAI, I have made some changes to the class. I have learned that there are differences on the models and depending on your needs, you would have to change the models and URL.

Here is the new class now:

Code: Select all | Expand


#INCLUDE "hbClass.ch"
#INCLUDE "hbcurl.ch"

#DEFINE CRLF CHR(13)+CHR(10)

CLASS TOpenAI

   DATA hCurl 
   DATA httpcode
   DATA nError             INIT 0 

   DATA Response 
   DATA Prompt 

   DATA model              INIT "text-davinci-003"
   DATA cUrl               INIT "https://api.openai.com/v1/completions"
   DATA max_tokens         INIT 2048
   DATA temperature        INIT 0 
   DATA top_p              INIT 1 
   DATA frequency_penalty  INIT 0.00
   DATA presence_penalty   INIT 0.00

   DATA cLogFile        INIT "TOpenAI.log"
   DATA nMaxLogSize     INIT 32768
   DATA lDebug          INIT .F. 

   METHOD New()
   METHOD End() 
   METHOD Send() 
   METHOD Reset() 

ENDCLASS 


//------------------------------------------------------------------------------------------------
METHOD New() CLASS TOpenAI

   ::hCurl := curl_easy_init()

RETURN Self 


//------------------------------------------------------------------------------------------------
METHOD Send() CLASS TOpenAI
   LOCAL aheaders
   Local httpcode
   Local cJson 
   Local h := { => }

   curl_easy_setopt( ::hCurl, HB_CURLOPT_POST, .T. )
   curl_easy_setopt( ::hCurl, HB_CURLOPT_URL, ::cUrl )

   //curl_easy_setopt( ::hCurl, HB_CURLOPT_PASSWORD, OPENAI_API_KEY )
   //http header could also be an array of headers
   aheaders := { "Content-Type: application/json", ;
                   "Authorization: Bearer " + OPENAI_API_KEY }
   
   curl_easy_setopt( ::hCurl, HB_CURLOPT_HTTPHEADER, aheaders )

   curl_easy_setopt( ::hCurl, HB_CURLOPT_USERNAME, '' )
   curl_easy_setopt( ::hCurl, HB_CURLOPT_DL_BUFF_SETUP )
   curl_easy_setopt( ::hCurl, HB_CURLOPT_SSL_VERIFYPEER, .F. )

   hb_HSet( h, "model", ::model )
   hb_HSet( h, "prompt", ::Prompt )
   hb_HSet( h, "max_tokens", ::max_tokens )
   hb_HSet( h, "temperature", ::temperature )
   hb_HSet( h, "frequency_penalty", ::frequency_penalty )
   hb_HSet( h, "presence_penalty", ::presence_penalty )

   cJson := hb_jsonEncode( h )
   curl_easy_setopt( ::hcurl, HB_CURLOPT_POSTFIELDS, cJson ) //cPostFields )

   ::nError := curl_easy_perform( ::hCurl ) 
   curl_easy_getinfo( ::hCurl, HB_CURLINFO_RESPONSE_CODE, @httpcode )

   ::httpcode := httpcode

   IF ::nError == HB_CURLE_OK

      ::Response = curl_easy_dl_buff_get( ::hCurl )

      IF ::lDebug 
         LogData( ::cLogFile, { "prompt", ::Prompt, "model", ::model }, ::nMaxLogSize )
         LogData( ::cLogFile, ::Response, ::nMaxLogSize )
      ENDIF 
   Else 

      LogData( ::cLogFile, { "TTOpenAI prompt was:", ::Prompt }, ::nMaxLogSize )
      LogData( ::cLogFile, { "Error Num:", ::nError, "Httpcode:", ::httpcode }, ::nMaxLogSize )
      LogData( ::cLogFile, { "Response:", ::Response }, ::nMaxLogSize )
      LogData( ::cLogFile, curl_easy_strerror( ::nError ), ::nMaxLogSize )

   ENDIF 

return ::Response            

//------------------------------------------------------------------------------------------------
METHOD Reset() CLASS TOpenAI
      
   curl_easy_reset( ::hCurl )
   ::nError := HB_CURLE_OK 
   ::Response := Nil 

return NIL 



//------------------------------------------------------------------------------------------------
METHOD End() CLASS TOpenAI

   curl_easy_cleanup( ::hCurl )
   ::hCurl := Nil  

return NIL 


/*------------------------------------------------------------------------------------------------
typeical response:
{
    "id": "cmpl-7EOhp7sTGKvQv0hsntGbg41BdadX9",
    "object": "text_completion",
    "created": 1683665917,
    "model": "text-davinci-003",
    "choices": [
        {
            "text": "\r\nICD-10 Diagnosis Codes: \r\n1. S52.521A - Fracture of lower end of right radius, initial encounter for closed fracture \r\n2. S52.522A - Fracture of lower end of right ulna, initial encounter for closed fracture \r\n3. S52.531A - Fracture of shaft of right radius, initial encounter for closed fracture \r\n4. S52.532A - Fracture of shaft of right ulna, initial encounter for closed fracture",
            "index": 0,
            "logprobs": null,
            "finish_reason": "stop"
        }
    ],
    "usage": {
        "prompt_tokens": 417,
        "completion_tokens": 113,
        "total_tokens": 530
    }
}
-------------------------------------------------------------------------------------------------*/
 
If you wish to emulate chatting, which is the most powerful, you'd change the URL and model like this:

:cURL := https://api.openai.com/v1/chat/completions
:Model := "gpt-3.5-turbo"

and then make the call using the :send() method after you have loaded the prompt.

I suspect I will be making many more changes to the class so that it works with any URL and Model combination and be able to manage any response. Keep in mind that the Json response will be different depending on what model you are using.

For my current use, which is to parse response for ICD10 codes, here is what I'm doing:

Code: Select all | Expand

CLASS TCodeICD10sWOpenAI FROM TOpenAI
   METHOD GetICD10sFromResponse()
END CLASS 

//#DEFINE _ONLY_ICD10S "\b[A-TV-Z]\d{2}\.\d{1,4}[A-Z]?\b\s*-\s*.+"
#DEFINE _ONLY_ICD10S "\s*[A-TV-Z]\d{2}\.\d{1,4}[A-Z]*\b\s*-\s.+"
METHOD GetICD10sFromResponse() CLASS TCodeICD10sWOpenAI 
   LOCAL aICD10s := {}
   LOCAL aParsed, iter, acodelines, CodeLine 
   LOCAL a := {}
   LOCAL aKeys, aCleanLn
   LOCAL hResponse := hb_jsonDecode( ::Response )

   if ::lDebug

      LogData( ::cLogFile, { "hb_isHash( hResponse )", hb_isHash( hResponse ), ;
                           "hb_HHasKey( hResponse, 'choices' )", hb_HHasKey( hResponse, "choices" ) } )
      aKeys := HGetKeys( hResponse )
      LogData( ::cLogFile, aKeys, ::nMaxLogSize )

   ENDIF


   IF hb_isHash( hResponse ) .AND. ;
      hb_HHasKey( hResponse, "choices" ) 

      if HB_ISARRAY( hResponse[ "choices" ] )
         a := hResponse[ "choices" ]
      endif 

      FOR iter := 1 TO LEN( a )

         IF hb_HHasKey( a[ iter ], "text" )

            acodelines := hb_ATokens( a[ iter ]["text"], CRLF )

            FOR EACH CodeLine IN acodelines
               aCleanLn := hb_RegEx( _ONLY_ICD10S, CodeLine )

               IF Len( aCleanLn ) > 0 

                  aParsed := hb_ATokens( aCleanLn[ 1 ], "-" )
                  AADD( aIcd10s, aParsed ) 

               ENDIF 

            NEXT 

         ENDIF 

      NEXT iter

   ENDIF 

RETURN aICD10s 
 
Exactly how you can benefit from OpenAI, takes imagination. For me, being able to have the system automatically code medical procedures using CPT codes and diagnosis conditions using International coding diagnosis version 10 codes places my software ahead of everyone else. I know that won't last long. Others will see it and will easily imitate it but I know my users will be very very happy with this new feature.

Reinaldo.
User avatar
Antonio Linares
Site Admin
Posts: 42270
Joined: Thu Oct 06, 2005 5:47 pm
Location: Spain
Contact:

Re: OpenAI class using hbCurl

Post by Antonio Linares »

Dear Reinaldo,

many thanks!

very good :-)
regards, saludos

Antonio Linares
www.fivetechsoft.com
User avatar
Antonio Linares
Site Admin
Posts: 42270
Joined: Thu Oct 06, 2005 5:47 pm
Location: Spain
Contact:

Re: OpenAI class using hbCurl

Post by Antonio Linares »

Dear Reinaldo,

Are you aware that chatGPT many thanks fails (it invents) on its answers ?

This may be specially important when dealing with medical issues...
regards, saludos

Antonio Linares
www.fivetechsoft.com
Post Reply