FiveTech Support Forums

FiveWin / Harbour / xBase community
Board index FiveWin para Harbour/xHarbour Clase TGemini
Posts: 470
Joined: Fri Feb 05, 2010 11:30 AM
Clase TGemini
Posted: Thu Jan 29, 2026 01:12 PM

Hola amigos... hace varios meses,
hice unas pruebas con la clase TGemini con las versiones publicadas en el foro.
Con el API KEY correspondiente, logré conectarme con la IA y hacerla funcionar para unas consultas específicas.

Con la nueva versión de FW con la clase ya nativa, suministrada por Antonio del webinar, quise
retomar las pruebas y no logro resultados. Sumado a que ahora se actualizó Gemini a la versión 3.0.
Generé una nueva API KEY, sin embargo sigo sin poder hacerla funcionar. Si bien el mensaje es que se
excedió de la cuota, es un api key nuevo, sin uso, el mensaje de error es el siguiente:

API Error: You exceeded your current quota, please check your plan and billing details. For more information on this error, head to: https://ai.google.dev/gemini-api/docs/rate-limits. To monitor your current usage, head to: https://ai.dev/rate-limit.

  • Quota exceeded for metric: generativelanguage.googleapis.com/generate_content_free_tier_requests, limit: 0, model: gemini-2.0-flash
  • Quota exceeded for metric: generativelanguage.googleapis.com/generate_content_free_tier_requests, limit: 0, model: gemini-2.0-flash
  • Quota exceeded for metric: generativelanguage.googleapis.com/generate_content_free_tier_input_token_count, limit: 0, model: gemini-2.0-flash
    Please retry in 58.507764137s. (Code: 429)[/color]

Alguno la está utilizando positivamente?

Muchas gracias!

Roberto

Univ@c I.S.I.
Desarrolladores de Software
http://www.elcolegioencasa.ar
Posts: 44158
Joined: Thu Oct 06, 2005 05:47 PM
Re: Clase TGemini
Posted: Fri Jan 30, 2026 06:37 PM

Estimado Roberto,

El API ha cambiado. Estamos investigándolo para implementar los cambios necesarios.

Muchas gracias por tu feedback :idea:

regards, saludos

Antonio Linares
www.fivetechsoft.com
Posts: 44158
Joined: Thu Oct 06, 2005 05:47 PM
Re: Clase TGemini
Posted: Sat Jan 31, 2026 07:59 AM

Aqui está la Clase TGemini modificada para funcionar con Gemini 3:

Antes de ejecutar el ejemplo desde el cmd:
set GEMINI_API_KEY=...

tgemini.prg

#include "FiveWin.ch"
#include "hbcurl.ch"

CLASS TGemini

DATA cApiKey
DATA cModel
DATA cUrl
DATA hCurl
DATA nError
DATA cResponse

METHOD New( cApiKey, cModel )
METHOD Send( cPrompt )
METHOD GetValue()
METHOD End()

ENDCLASS

METHOD New( cApiKey, cModel ) CLASS TGemini

DEFAULT cApiKey := GetEnv( "GEMINI_API_KEY" )
DEFAULT cModel  := "gemini-3-flash-preview"

::cApiKey = cApiKey
::cModel  = cModel
::cUrl    = "https://generativelanguage.googleapis.com/v1beta/models/" + ::cModel + ":generateContent"

::hCurl = curl_easy_init()

return Self

METHOD Send( cPrompt ) CLASS TGemini

local cJson, aHeaders

if Empty( ::hCurl )
    return "Error: Curl init failed"
endif

// Construct JSON payload
cJson := '{' + CRLF + ;
    '  "contents": [' + CRLF + ;
    '      {' + CRLF + ;
    '          "parts": [' + CRLF + ;
    '              {"text": "' + cPrompt + '"}' + CRLF + ;
    '          ]' + CRLF + ;
    '      }' + CRLF + ;
    '  ],' + CRLF + ;
    '  "tools": [' + CRLF + ;
    '      {' + CRLF + ;
    '          "url_context": {}' + CRLF + ;
    '      }' + CRLF + ;
    '  ]' + CRLF + ;
    '}'

curl_easy_reset( ::hCurl )
   
curl_easy_setopt( ::hCurl, HB_CURLOPT_POST, .T. )
curl_easy_setopt( ::hCurl, HB_CURLOPT_URL, ::cUrl + "?key=" + ::cApiKey )
   
aHeaders := { "Content-Type: application/json" }
curl_easy_setopt( ::hCurl, HB_CURLOPT_HTTPHEADER, aHeaders )
curl_easy_setopt( ::hCurl, HB_CURLOPT_SSL_VERIFYPEER, .F. )
   
curl_easy_setopt( ::hCurl, HB_CURLOPT_POSTFIELDS, cJson )
   
// Setup buffer to capture response
curl_easy_setopt( ::hCurl, HB_CURLOPT_DL_BUFF_SETUP )

::nError = curl_easy_perform( ::hCurl )

if ::nError == HB_CURLE_OK
    ::cResponse = curl_easy_dl_buff_get( ::hCurl )
else
    ::cResponse = "Error: Curl execution failed: " + Str( ::nError )
endif

return ::cResponse

METHOD GetValue() CLASS TGemini

local hResponse, uValue := ""

if !Empty( ::cResponse )
    hb_jsonDecode( ::cResponse, @hResponse )
endif
   
if ValType( hResponse ) == "H"
    if HHasKey( hResponse, "candidates" ) .and. Len( hResponse[ "candidates" ] ) > 0
        TRY
            uValue = hResponse[ "candidates" ][ 1 ][ "content" ][ "parts" ][ 1 ][ "text" ]
        CATCH
            uValue = "Error: Unexpected response structure"
        END
    elseif HHasKey( hResponse, "error" )
        uValue = "API Error: " + hResponse[ "error" ][ "message" ]
    endif
endif

return uValue

METHOD End() CLASS TGemini

if !Empty( ::hCurl )
    curl_easy_cleanup( ::hCurl )
    ::hCurl = nil
endif

return nil

gemini1.prg

#include "FiveWin.ch"
#include "tgemini.prg"     

function Main()

local oGemini := TGemini():New()

oGemini:Send( "cual es la capital de francia" )
  
MsgInfo( oGemini:GetValue() )
   
oGemini:End()

return nil
regards, saludos

Antonio Linares
www.fivetechsoft.com
Posts: 492
Joined: Wed Nov 16, 2005 12:03 PM
Re: Clase TGemini
Posted: Sun Feb 01, 2026 03:45 AM

Colegas
Saludos
Procedimientos para solicitud del API-KEY sus primeros 4 digitos empieza AIza
https://adaptaproerp.com/implementacion-api-key-gemini/
+584143000518
Mediante CURL deben validarla antes de iniciarla es sus aplicaciones

Posts: 492
Joined: Wed Nov 16, 2005 12:03 PM
Re: Clase TGemini
Posted: Sun Feb 01, 2026 03:50 AM

Colegas
Saludos, validacion del API-KEY antes de iniciaren produccion, depende del uso requiere modelo, si es para Aplica OCR para extraer datos desde documentos deben utilizar Gemini-2.5-pro
curl "https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key=TU_API_KEY" \
-H 'Content-Type: application/json' \
-X POST \
-d '{
"contents": [{
"parts":[{"text": "Hola, responde con la palabra Test."}]
}]
}'

Posts: 470
Joined: Fri Feb 05, 2010 11:30 AM
Re: Clase TGemini
Posted: Wed Mar 18, 2026 07:35 PM

Antonio! Gracias por modificar la clase! Para textos funciona perfecto!

No encuentro la forma de hacerla funcionar para subir imágenes, da unos errores diferentes. Estimo que tendrá que ver con algo que la clase no tenga modificado.

Como puedo modificar la clase para obtener resultado al añadir las imágenes??

Gracias

ROberto

Univ@c I.S.I.
Desarrolladores de Software
http://www.elcolegioencasa.ar
Posts: 44158
Joined: Thu Oct 06, 2005 05:47 PM
Re: Clase TGemini
Posted: Wed Mar 18, 2026 08:26 PM

Estimado Roberto,

Aqui tienes la Clase TGemini modificada y funcionando correctamente con imágenes:

tgemini.prg

#include "FiveWin.ch"
#include "hbcurl.ch"

#ifdef __XHARBOUR__
   #define hb_hHasKey( h, k ) HHasKey( h, k )
#endif   

//----------------------------------------------------------------------------//

CLASS TGemini

   DATA   cKey   INIT ""
   DATA   cModel INIT "gemini-2.0-flash"
   DATA   cResponse
   DATA   cUrl   INIT "https://generativelanguage.googleapis.com/v1beta/models"
   DATA   cUploadUrl INIT "https://generativelanguage.googleapis.com/upload/v1beta/files"
   DATA   hCurl
   DATA   nError INIT 0
   DATA   nHttpCode INIT 0
   DATA   nTemperature INIT 0

   METHOD New( cKey, cModel )
   METHOD Send( uContent, cPrompt, bCallback )
   METHOD End()
   METHOD GetValue()
   METHOD UploadFile( cFileName, lDeleteAfter )
   METHOD GetTokens( cBuffer ) 

ENDCLASS

//----------------------------------------------------------------------------//

METHOD New( cKey, cModel ) CLASS TGemini

   if Empty( cKey )
      ::cKey = GetEnv( "GEMINI_API_KEY" )
   else
      ::cKey = cKey
   endif

   if ! Empty( cModel )
      ::cModel = cModel
   endif

   if Val( SubStr( Curl_Version_Info()[ 1 ], 1, RAt( ".", Curl_Version_Info()[ 1 ] ) - 1 ) ) - 8.10 > 0.2
      MsgAlert( "Please use an updated curl DLL" )
   endif    

   ::hCurl = curl_easy_init()

return Self

//----------------------------------------------------------------------------//

METHOD End() CLASS TGemini

   curl_easy_cleanup( ::hCurl )
   ::hCurl = nil

return nil

//----------------------------------------------------------------------------//

METHOD GetValue() CLASS TGemini

   local hResponse, uValue

   if ! Empty( ::cResponse )
      hb_jsonDecode( ::cResponse, @hResponse )
   endif

   if hb_isHash( hResponse )
      if hb_hHasKey( hResponse, "error" )
         uValue = "API Error: " + hResponse[ "error" ][ "message" ] + " (Code: " + hb_ntos( hResponse[ "error" ][ "code" ] ) + ")"
      elseif hb_hHasKey( hResponse, "candidates" ) .and. Len( hResponse[ "candidates" ] ) > 0
         TRY
            uValue = hResponse[ "candidates" ][ 1 ][ "content" ][ "parts" ][ 1 ][ "text" ]
         CATCH
            uValue = "Error: Unexpected response structure"
         END
      else
         uValue = "Error: No candidates in response"
      endif
   else
      uValue = "Error: Invalid response format"
   endif

return uValue

//----------------------------------------------------------------------------//

METHOD Send( uContent, cPrompt, bCallback ) CLASS TGemini

   local aHeaders, cJson, hRequest := {=>}, hContents := { => }, hGenerationConfig
   local cMimeType, lIsFile := .F., cUrlEndpoint
   local aFiles, nI, aParts := {}

   if Empty( cPrompt )
      cPrompt = "what is this or solve this"
   endif

   if hb_isArray( uContent ) .and. ! Empty( uContent )
      aFiles = uContent
      for nI = 1 to Len( aFiles )
         if hb_isChar( aFiles[ nI ] ) .and. File( aFiles[ nI ] )
            do case
               case Lower( Right( aFiles[ nI ], 3 ) ) == "png"
                  cMimeType = "image/png"
               case Lower( Right( aFiles[ nI ], 3 ) ) $ "jpg|jpeg"
                  cMimeType = "image/jpeg"
               case Lower( Right( aFiles[ nI ], 3 ) ) == "pdf"
                  cMimeType = "application/pdf"
               case Lower( Right( aFiles[ nI ], 3 ) ) == "txt"
                  cMimeType = "text/plain"
               case Lower( Right( aFiles[ nI ], 3 ) ) == "csv"
                  cMimeType = "text/csv"
               case Lower( Right( aFiles[ nI ], 3 ) ) == "prg"
                  cMimeType = "text/plain"
               case Lower( Right( aFiles[ nI ], 2 ) ) == "ch"
                  cMimeType = "text/plain"
               otherwise
                  return "Unsupported file type: " + aFiles[ nI ]
            endcase
            AAdd( aParts, { "inlineData" => { "data" => hb_base64Encode( hb_MemoRead( aFiles[ nI ] ) ), "mimeType" => cMimeType } } )
         else
            return "Invalid file in array: " + aFiles[ nI ]
         endif
      next
      lIsFile = .T.
   elseif hb_isChar( uContent ) .and. File( uContent )
      lIsFile = .T.
      do case
         case Lower( Right( uContent, 3 ) ) == "png"
            cMimeType = "image/png"
         case Lower( Right( uContent, 3 ) ) $ "jpg|jpeg"
            cMimeType = "image/jpeg"
         case Lower( Right( uContent, 3 ) ) == "pdf"
            cMimeType = "application/pdf"
         case Lower( Right( uContent, 3 ) ) == "txt"
            cMimeType = "text/plain"
         case Lower( Right( uContent, 3 ) ) == "csv"
            cMimeType = "text/csv"
         case Lower( Right( uContent, 3 ) ) == "prg"
            cMimeType = "text/plain"
         case Lower( Right( uContent, 2 ) ) == "ch"
            cMimeType = "text/plain"
         otherwise
            return "Unsupported file type"
      endcase
      AAdd( aParts, { "inlineData" => { "data" => hb_base64Encode( hb_MemoRead( uContent ) ), "mimeType" => cMimeType } } )
   endif

   cUrlEndpoint = iif( hb_isBlock( bCallback ), ":streamGenerateContent", ":generateContent" )
   curl_easy_setopt( ::hCurl, HB_CURLOPT_POST, .T. )
   curl_easy_setopt( ::hCurl, HB_CURLOPT_URL, ::cUrl + "/" + ::cModel + cUrlEndpoint + "?key=" + ::cKey )

   aHeaders := { "Content-Type: application/json" }
   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. )

   hContents[ "role" ] = "user"
   if lIsFile
      if ! Empty( cPrompt )
         AAdd( aParts, { "text" => cPrompt } )
      endif
      hRequest[ "contents" ] = { { "role" => "user", "parts" => aParts } }
   else
      hContents[ "parts" ] = { { "text" => iif( hb_isChar( uContent ), uContent, cPrompt ) } }
      hRequest[ "contents" ] = { hContents }
   endif

   hGenerationConfig = { "temperature" => ::nTemperature,;
                         "topK" => 40, "topP" => 0.95, "maxOutputTokens" => 8192,;
                         "responseMimeType" => "text/plain" }
   hRequest[ "generationConfig" ] = hGenerationConfig

   cJson = hb_jsonEncode( hRequest )
   curl_easy_setopt( ::hCurl, HB_CURLOPT_POSTFIELDS, cJson )

   if hb_isBlock( bCallback )
      curl_easy_setopt( ::hCurl, HB_CURLOPT_WRITEFUNCTION, bCallback )
   endif

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

   if ::nError == HB_CURLE_OK
      ::cResponse = curl_easy_dl_buff_get( ::hCurl )
   else
      ::cResponse = "CURL Error code " + Str( ::nError )
   endif

return ::cResponse

//----------------------------------------------------------------------------//

METHOD UploadFile( cFileName, lDeleteAfter ) CLASS TGemini

   local pCurl, aPost := {}, hHash

   if hb_isPointer( pCurl := curl_easy_init() )
      curl_easy_setopt( pCurl, HB_CURLOPT_CUSTOMREQUEST, "POST" )
      curl_easy_setopt( pCurl, HB_CURLOPT_URL, ::cUploadUrl + "?key=" + ::cKey )
      curl_easy_setopt( pCurl, HB_CURLOPT_FOLLOWLOCATION, .T. )
      curl_easy_setopt( pCurl, HB_CURLOPT_DL_BUFF_SETUP )
      curl_easy_setopt( pCurl, HB_CURLOPT_SSL_VERIFYPEER, .F. )

  AAdd( aPost, { "file", hb_jsonEncode( { "display_name" => cFileName } ) } )
  AAdd( aPost, { "", cFileName } )

  curl_easy_setopt( pCurl, HB_CURLOPT_MIMEPOST, aPost )

  if ( ::nError := curl_easy_perform( pCurl ) ) == HB_CURLE_OK
     hb_jsonDecode( ::cResponse := curl_easy_dl_buff_get( pCurl ), @hHash )
  else
     MsgAlert( "curl error: " + AllTrim( Str( ::nError ) ) )
  endif

  curl_easy_cleanup( pCurl )
   endif

   if hb_isHash( hHash )
      if hb_hHasKey( hHash, "file" ) .and. hb_hHasKey( hHash[ "file" ], "uri" )
         return hHash[ "file" ][ "uri" ]
      endif
   endif

   if lDeleteAfter .and. File( cFileName )
      hb_FileDelete( cFileName )
   endif

return ""

//----------------------------------------------------------------------------//

METHOD GetTokens( cBuffer ) CLASS TGemini

   local hResponse, cValue := ""

   if Left( cBuffer, 1 ) == ","
      cBuffer = SubStr( cBuffer, 2 )
   endif

   hb_jsonDecode( cBuffer, @hResponse )

   if ! Empty( hResponse )
      if ValType( hResponse ) == "A"  // Streaming response (array of chunks)
         if hb_hHasKey( hResponse[ 1 ], "error" )
            cValue = "API Error: " + hResponse[ 1 ][ "error" ][ "message" ] + " (Code: " + hb_ntos( hResponse[ 1 ][ "error" ][ "code" ] ) + ")"
         elseif hb_hHasKey( hResponse[ 1 ], "candidates" ) .and. Len( hResponse[ 1 ][ "candidates" ] ) > 0
            TRY
               cValue = hResponse[ 1 ][ "candidates" ][ 1 ][ "content" ][ "parts" ][ 1 ][ "text" ]
            CATCH
               cValue = "Error: Unexpected streaming response structure"
            END
         else
            cValue = "Error: No candidates in streaming response"
         endif
      elseif hb_isHash( hResponse )  // Non-streaming response
         if hb_hHasKey( hResponse, "error" )
            cValue = "API Error: " + hResponse[ "error" ][ "message" ] + " (Code: " + hb_ntos( hResponse[ "error" ][ "code" ] ) + ")"
         elseif hb_hHasKey( hResponse, "candidates" ) .and. Len( hResponse[ "candidates" ] ) > 0
            TRY
               cValue = hResponse[ "candidates" ][ 1 ][ "content" ][ "parts" ][ 1 ][ "text" ]
            CATCH
               cValue = "Error: Unexpected response structure"
            END
         else
            cValue = "Error: No candidates in response"
         endif
      else
         cValue = "Error: Invalid response format in streaming buffer"
      endif
   endif

return cValue

//----------------------------------------------------------------------------//

#ifdef __XHARBOUR__
   

function HB_FNAMEDIR( cFileName )
   local nLastSlash := Max( RAt( "\", cFileName ), RAt( "/", cFileName ) )
   if nLastSlash > 0
      return Left( cFileName, nLastSlash )
   endif
return ""

function HB_FNAMENAME( cFileName )
   local cName := cFileName
   local nLastSlash := Max( RAt( "\", cFileName ), RAt( "/", cFileName ) )
   local nLastDot

   if nLastSlash > 0
      cName = SubStr( cFileName, nLastSlash + 1 )
   endif

   nLastDot = RAt( ".", cName )
   if nLastDot > 0
      cName = Left( cName, nLastDot - 1 )
   endif

return cName

function HB_FCOPY( cSource, cDest )

   local hSource, hDest, nRead, aBuffer := {}

   if hb_isPointer( hSource := FOpen( cSource, "rb" ) )
      if hb_isPointer( hDest := FOpen( cDest, "wb" ) )
         while ! hb_feof( hSource )
            nRead := FRead( aBuffer, 1, 1024, hSource )
            FWrite( aBuffer, 1, nRead, hDest )
         end
         FClose( hDest )
      endif
      FClose( hSource )
   endif

return nil   

function HB_FILEDELETE( cFileName )

   if File( cFileName )
      return FErase( cFileName )
   endif

return nil

#endif

gemini2.prg

// Get your API key from https://aistudio.google.com
// from cmd: set GEMINI_API_KEY=your_api_key

#include "FiveWin.ch"

//----------------------------------------------------------------------------//

function Main()

   local oChat := TGemini():New()
   local cFile, cPrompt := Space( 254 )

   cFile := cGetFile( "Please select a file| *.png| ", "Please select an image PNG" )
   if empty( cFile )
      msgStop( "No image selected", "Aborted" )
      return nil
   endif

   msgGet( "Warning", "Enter your question about the image:", @cPrompt )
   if empty( cPrompt )
      msgStop( "No Question", "Aborted" )
      return nil
   endif

   oChat:Send( cFile, cPrompt )
   ? oChat:GetValue()

   oChat:End()

return nil

//----------------------------------------------------------------------------//

Te agradezco si la pruebas y verificas su funcionamiento, Gracias! :)

regards, saludos

Antonio Linares
www.fivetechsoft.com
Posts: 470
Joined: Fri Feb 05, 2010 11:30 AM
Re: Clase TGemini
Posted: Wed Mar 18, 2026 08:27 PM

GRACIAS MAESTROOOOOOOOOOOOO!!

Univ@c I.S.I.
Desarrolladores de Software
http://www.elcolegioencasa.ar
Posts: 410
Joined: Sun Jan 31, 2010 03:30 PM
Re: Clase TGemini
Posted: Thu Mar 19, 2026 12:18 PM
#include "FiveWin.ch"
#include "hbcurl.ch"

PROCEDURE Main()
   LOCAL oGemini := GeminiAPI():New( "SU API KEY" )
   LOCAL cRespuesta := ""

   ? "Enviando pregunta a Gemini..."
   cRespuesta := oGemini:Preguntar( "Hola, ¿VALOR DOLAR HOY EN PESOS COLOMBIANOS ?" )

   ? " Respuesta de la IA. Ver en : "+"RESPONSE.JSON"

   HB_MEMOWRIT("RESPONSE.JSON",cRespuesta)

memoedit(cRespuesta)


RETURN

// --- Clase para gestionar la API ---
CREATE CLASS GeminiAPI
   VAR cApiKey


VAR cModel INIT "gemini-2.5-flash"
VAR cUrl   INIT "https://generativelanguage.googleapis.com/v1/models/"



   METHOD New( cKey )
   METHOD Preguntar( cPrompt )
   METHOD ListarModelos()

ENDCLASS

METHOD New( cKey ) CLASS GeminiAPI
   ::cApiKey := cKey
RETURN Self

METHOD ListarModelos() CLASS GeminiAPI
   LOCAL hCurl, cFullUrl

   // Esta URL te devolverá la lista real de modelos que puedes usar
   cFullUrl := "https://generativelanguage.googleapis.com/v1/models?key=" + ::cApiKey

   hCurl := curl_easy_init()
   curl_easy_setopt( hCurl, HB_CURLOPT_SSL_VERIFYPEER, .F. )
   curl_easy_setopt( hCurl, HB_CURLOPT_SSL_VERIFYHOST, 0 )
   curl_easy_setopt( hCurl, HB_CURLOPT_URL, cFullUrl )
   curl_easy_setopt( hCurl, HB_CURLOPT_HTTPGET, .T. )
   curl_easy_setopt( hCurl, HB_CURLOPT_DL_BUFF_SETUP )

   IF curl_easy_perform( hCurl ) == 0
      ? "REVISE : Modelos disponibles para ti : "+"MODELOS.JSON"
      //? curl_easy_dl_buff_get( hCurl )
      HB_MEMOWRIT("MODELOS.JSON",curl_easy_dl_buff_get( hCurl ))
   ENDIF
   curl_easy_cleanup( hCurl )
RETURN NIL

METHOD Preguntar( cPrompt ) CLASS GeminiAPI
   LOCAL hCurl
   LOCAL nCode
   LOCAL cResponse := ""
   LOCAL cJson     := ""
   LOCAL cFullUrl  := ::cUrl + ::cModel + ":generateContent?key=" + ::cApiKey


   ::ListarModelos()

   // 1. Construir el JSON (Estructura requerida por Google)
   cJson := '{' + ;
               '"contents": [{' + ;
                  '"parts": [{' + ;
                     '"text": "' + cPrompt + '"' + ;
                  '}]' + ;
               '}]' + ;
            '}'

   HB_MEMOWRIT("RESPONSE_ERROR.JSON",cJson)
   // 2. Configurar hCurl
   hCurl := curl_easy_init()
   IF !Empty( hCurl )
      curl_easy_setopt( hCurl, HB_CURLOPT_URL, cFullUrl )
      curl_easy_setopt( hCurl, HB_CURLOPT_POST, .T. )
      curl_easy_setopt( hCurl, HB_CURLOPT_POSTFIELDS, cJson )
      curl_easy_setopt( hCurl, HB_CURLOPT_HTTPHEADER, { "Content-Type: application/json" } )
      curl_easy_setopt( hCurl, HB_CURLOPT_DL_BUFF_SETUP )
      curl_easy_setopt( hCurl, HB_CURLOPT_SSL_VERIFYPEER, .F. ) // Para evitar líos con certificados SSL

  nCode := curl_easy_perform( hCurl )

  IF nCode == 0
     cResponse := curl_easy_dl_buff_get( hCurl )
  ELSE
     cResponse := "Error en Curl: " + hb_ntos( nCode )
  ENDIF


  curl_easy_cleanup( hCurl )
   ENDIF

RETURN cResponse
Posts: 410
Joined: Sun Jan 31, 2010 03:30 PM
Re: Clase TGemini
Posted: Thu Mar 19, 2026 02:03 PM
#include "FiveWin.ch"
#include "hbcurl.ch"

#ifdef __XHARBOUR__
   #define hb_hHasKey( h, k ) HHasKey( h, k )
   #define hb_jsonDecode( c, h ) ap_JSONDecode( c, @h ) // Depende de tu versión de xHarbour
#endif

//----------------------------------------------------------------------------//

function Main()

   local oChat := TGemini():New()
   local cFile, cPrompt := Space( 254 )

   cFile := cGetFile( "Please select a file| *.png| ", "Please select an image PNG" )
   if empty( cFile )
      msgStop( "No image selected", "Aborted" )
      return nil
   endif

   msgGet( "Warning", "Enter your question about the image:", @cPrompt )
   if empty( cPrompt )
      msgStop( "No Question", "Aborted" )
      return nil
   endif

   oChat:Send( cFile, cPrompt )
   ? oChat:GetValue()

   oChat:End()

return nil

//----------------------------------------------------------------------------//

CLASS TGemini

   DATA   cKey          INIT ""
   DATA   cModel        INIT "gemini-2.5-flash"  // Usando el modelo que confirmamos disponible
   DATA   cResponse     INIT ""
   DATA   cUrl          INIT "https://generativelanguage.googleapis.com/v1/models/"

   DATA   hCurl
   DATA   nError        INIT 0
   DATA   nHttpCode     INIT 0
   DATA   nTemperature  INIT 1

   METHOD New( cKey, cModel )
   METHOD Send( uContent, cPrompt )
   METHOD GetValue()
   METHOD End()

ENDCLASS

//----------------------------------------------------------------------------//

METHOD New( cKey, cModel ) CLASS TGemini
   if !Empty( cKey )
      ::cKey = cKey
   else
      ::cKey = GetEnv( "GEMINI_API_KEY" )
   endif

   if !Empty( cModel )
      ::cModel = cModel
   endif

   ::hCurl = curl_easy_init()
return Self

//----------------------------------------------------------------------------//

METHOD Send( uContent, cPrompt ) CLASS TGemini
   local cJson, hRequest := {=>}, aParts := {}
   local cMimeType := "image/png"
   local cBinary, cBase64
   local cFullUrl := ::cUrl + ::cModel + ":generateContent?key=" + ::cKey

   if Empty( ::hCurl ) ; return "Error: CURL no inicializado" ; endif

   // 1. Manejo de Imagen/Archivo
   if hb_isChar( uContent ) .and. File( uContent )
      do case
         case Lower( Right( uContent, 3 ) ) == "png" ; cMimeType = "image/png"
         case Lower( Right( uContent, 3 ) ) $ "jpg|jpeg" ; cMimeType = "image/jpeg"
         case Lower( Right( uContent, 3 ) ) == "pdf" ; cMimeType = "application/pdf"
      endcase

  cBinary := hb_MemoRead( uContent )
  cBase64 := hb_base64Encode( cBinary )

  // CRÍTICO: Google rechaza Base64 con saltos de línea. Limpiamos CRLF.
  cBase64 := StrTran( cBase64, hb_Eol(), "" )
  cBase64 := StrTran( cBase64, Chr(10), "" )
  cBase64 := StrTran( cBase64, Chr(13), "" )

  // Añadir imagen al array de partes
  AAdd( aParts, { "inlineData" => { "mimeType" => cMimeType, "data" => cBase64 } } )

  // Añadir texto al array de partes
  if !Empty( cPrompt )
     AAdd( aParts, { "text" => cPrompt } )
  endif
   else
      // Solo texto
      AAdd( aParts, { "text" => iif( Empty(uContent), cPrompt, uContent ) } )
   endif

   // 2. Construcción Manual del JSON (Para evitar errores de hb_jsonEncode con hashes complejos)
   // Google espera: {"contents": [{"parts": [{"inlineData":...}, {"text":...}]}]}
   hRequest["contents"] := { { "parts" => aParts } }
   cJson := hb_jsonEncode( hRequest )

   // 3. Configuración de CURL
   curl_easy_setopt( ::hCurl, HB_CURLOPT_URL, cFullUrl )
   curl_easy_setopt( ::hCurl, HB_CURLOPT_POST, .T. )
   curl_easy_setopt( ::hCurl, HB_CURLOPT_POSTFIELDS, cJson )
   curl_easy_setopt( ::hCurl, HB_CURLOPT_HTTPHEADER, { "Content-Type: application/json" } )

   // Ignorar SSL (Error 60)
   curl_easy_setopt( ::hCurl, HB_CURLOPT_SSL_VERIFYPEER, .F. )
   curl_easy_setopt( ::hCurl, HB_CURLOPT_SSL_VERIFYHOST, 0 )

   curl_easy_setopt( ::hCurl, HB_CURLOPT_DL_BUFF_SETUP )
   curl_easy_setopt( ::hCurl, HB_CURLOPT_TIMEOUT, 60 ) // Más tiempo para subir imágenes

   // 4. Ejecución y Captura de Respuesta
   ::nError = curl_easy_perform( ::hCurl )

   if ::nError == 0
      ::cResponse = curl_easy_dl_buff_get( ::hCurl )

  //MsgInfo( ::cResponse )
   else
      ::cResponse = '{"error": {"message": "CURL Connection Error: ' + AllTrim(Str(::nError)) + '"}}'
   endif

return ::cResponse
//----------------------------------------------------------------------------//

METHOD GetValue() CLASS TGemini
   local hResponse := {=>}
   local uValue := ""

   if Empty( ::cResponse )
      return "Error: No hay respuesta del servidor."
   endif

   // DEBUG: Descomenta la siguiente línea para ver el JSON real en caso de error
   // MemoWrit( "debug_gemini.json", ::cResponse )

   hb_jsonDecode( ::cResponse, @hResponse )

   if hb_isHash( hResponse )
      if hb_hHasKey( hResponse, "error" )
         uValue = "API Error: " + hResponse["error"]["message"]
      elseif hb_hHasKey( hResponse, "candidates" ) .and. Len( hResponse["candidates"] ) > 0
         uValue = hResponse["candidates"][1]["content"]["parts"][1]["text"]
      else
         uValue = "Respuesta inesperada. JSON: " + ::cResponse
      endif
   else
      uValue = "Error crítico: El servidor no devolvió un JSON válido. Verifique su API Key."
   endif

return uValue

//----------------------------------------------------------------------------//

METHOD End() CLASS TGemini
   if !Empty( ::hCurl )
      curl_easy_cleanup( ::hCurl )
   endif
return nil
Posts: 410
Joined: Sun Jan 31, 2010 03:30 PM
Re: Clase TGemini
Posted: Thu Mar 19, 2026 02:12 PM

Para que Gemini "lea" tus tablas y te ayude con reportes, consultas o análisis, tenemos que "darle ojos" a través de metadatos y muestras de datos.

Aquí tienes la estrategia ganadora para lograrlo en Harbour:

  1. El Concepto: "RAG" (Generación Aumentada por Recuperación)
    No le envías toda la base de datos (sería muy lento y costoso). Le envías:

  2. La Estructura (Nombres de campos y tipos).

  3. Una Muestra de los datos (las primeras 5 o 10 filas).

  4. La Pregunta del usuario.

Función Harbour para "Explicar" la DBF a Gemini
Crea una función que convierta el DbStruct() y los registros en un texto que Gemini entienda:

Fragmento de código
FUNCTION GetDbfContext( cAlias )
LOCAL aStruct := (cAlias)->( DbStruct() )
LOCAL cContext := "Estructura de la tabla " + cAlias + ":" + hb_Eol()
LOCAL nI, nJ

// 1. Enviamos la definición de campos
FOR nI := 1 TO Len( aStruct )
cContext += "- Campo: " + aStruct[nI][1] + " (Tipo: " + aStruct[nI][2] + ")" + hb_Eol()
NEXT

cContext += hb_Eol() + "Muestra de datos (primeros 3 registros):" + hb_Eol()

// 2. Enviamos una pequeña muestra de datos
(cAlias)->( DbGoTop() )
FOR nI := 1 TO 3
cContext += "Registro " + Str(nI) + ": "
FOR nJ := 1 TO Len( aStruct )
cContext += aStruct[nJ][1] + "=" + cValToChar( (cAlias)->(FieldGet(nJ)) ) + ", "
NEXT
cContext += hb_Eol()
(cAlias)->( DbSkip() )
NEXT

RETURN cContext

//**************************************************************************

Cómo llamar a Gemini con tus Datos
Ahora, en tu método Send, concatenas el contexto de la DBF con la pregunta del usuario:

Fragmento de código
// Ejemplo de uso
USE Clientes SHARED NEW ALIAS CLI
cContextoDBF := GetDbfContext( "CLI" )

cPregunta := "Basado en estos datos, ¿cuál es el cliente con mayor saldo y qué sugerencia me das para cobrarle?"

// El prompt final que recibe Gemini es: "Datos de mi DBF... + Mi Pregunta"
oChat:Send( cContextoDBF + hb_Eol() + "PREGUNTA: " + cPregunta )

MsgInfo( oChat:GetValue(), "Análisis de mi DBF" )

Posts: 410
Joined: Sun Jan 31, 2010 03:30 PM
Re: Clase TGemini
Posted: Thu Mar 19, 2026 02:53 PM

ejemplo funcional :

PROCEDURE TestAI()
   LOCAL cCondicionUsuario := "clientes que deban más de 5000 y se llamen Sergio"
   LOCAL cCodigoHarbour

   USE "e:\zerus\00\bases\CLIENTES.dat" SHARED NEW ALIAS CLIENTES

   ? "Preguntando a la IA por el filtro..."
   cCodigoHarbour := AskForFilter( "CLIENTES", cCondicionUsuario )

   ? "Filtro generado por Gemini: " + cCodigoHarbour
   // Resultado esperado: SALDO > 5000 .AND. "SERGIO" $ UPPER(NOMBRE)

   // APLICAR EL FILTRO DINÁMICAMENTE
   TRY

   MsgInfo(cCodigoHarbour)
  CLIENTES->( DbSetFilter( {|| &cCodigoHarbour }, cCodigoHarbour ) )
  CLIENTES->( DbGoTop() )

  IF CLIENTES->( EOF() )
     MsgInfo( "No hay registros que cumplan esa condición." )
  ELSE
     Browse() // Muestra los resultados filtrados
  ENDIF
   CATCH
      MsgStop( "El filtro generado no es válido: " + cCodigoHarbour )
   END

RETURN

FUNCTION GetDbfSchema( cAlias )
   LOCAL aStruct := (cAlias)->( DbStruct() )
   LOCAL cSchema := "Tabla: " + cAlias + hb_Eol()
   LOCAL nI

   cSchema += "Campos disponibles: para el saldo debes usar el campo nsalinicli" + hb_Eol()
   FOR nI := 1 TO Len( aStruct )
      // Ejemplo: NOMBRE (C), SALDO (N), FECHA (D)
      cSchema += "- " + aStruct[nI][1] + " (" + aStruct[nI][2] + ")" + hb_Eol()
   NEXT
RETURN cSchema


FUNCTION AskForFilter( cAlias, cUserRequest )
   LOCAL oChat := TGemini():New()
   LOCAL cSchema := GetDbfSchema( cAlias )
   LOCAL cPrompt, cFilterCode

   cPrompt := "Actúa como un programador de Clipper/Harbour experto." + hb_Eol()
   cPrompt += "Basado en esta estructura de base de datos:" + hb_Eol()
   cPrompt += cSchema + hb_Eol()
   cPrompt += "Genera UNICAMENTE el código para un SET FILTER TO que cumpla esta condición: " + cUserRequest + hb_Eol()
   cPrompt += "REGLAS:" + hb_Eol()
   cPrompt += "1. Solo devuelve la expresión del filtro, nada de texto adicional." + hb_Eol()
   cPrompt += "2. Usa sintaxis estándar de Harbour (ej. .AND., .OR., Upper(), DTOS())." + hb_Eol()
   cPrompt += "3. Si el usuario pide fechas, asume que debe compararse con fechas o usar CTOD()."

   oChat:Send( cPrompt )
   cFilterCode := oChat:GetValue()

   // Limpieza de seguridad por si Gemini devuelve comillas invertidas (markdown)
   cFilterCode := StrTran( cFilterCode, "`", "" )
   cFilterCode := AllTrim( cFilterCode )

RETURN cFilterCode
Posts: 410
Joined: Sun Jan 31, 2010 03:30 PM
Re: Clase TGemini
Posted: Thu Mar 19, 2026 03:20 PM

TABLA DE COSTOS :

Concepto,Cantidad,Tarifa (por 1M),Subtotal
Tokens Nuevos (Input),475,$0.10,$0.0000475
Tokens en Caché (Input),"1,109",$0.025 (25%),$0.0000277
Salida + Pensamiento,282 (26+256),$0.40,$0.0001128
TOTAL ESTIMADO,,,$0.000188

Continue the discussion