Class TOllama with Agents!

Post Reply
User avatar
Antonio Linares
Site Admin
Posts: 42847
Joined: Thu Oct 06, 2005 5:47 pm
Location: Spain
Has thanked: 181 times
Been thanked: 124 times
Contact:

Class TOllama with Agents!

Post by Antonio Linares »

agents.prg

Code: Select all | Expand

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

// Activar depuración (comentar esta línea para desactivarla)
#define DEBUG

FUNCTION Main()
   local oLlama, oAgent
   
   oLlama = TOLlama():New()
   
   // Agente para mostrar la hora
   oAgent = TAgent():New( "time", { {"get_time", {|hParams| GetCurrentTime(hParams)} } } )
   AAdd( oLlama:aAgents, oAgent )
   
   // Agente para filesystem con múltiples tools
   oAgent = TAgent():New( "filesystem", { ;
      {"create_folder", {|hParams| CreateFolder(hParams)} },;
      {"create_file",   {|hParams| CreateFile(hParams)} },;
      {"modify_file",   {|hParams| ModifyFile(hParams)} } } )
   AAdd( oLlama:aAgents, oAgent )
   
   #ifdef DEBUG
      ? "DEBUG: Probando 'What time is it?'"
   #endif
   fw_memoEdit( oLlama:Send( "What time is it?" ) )
   #ifdef DEBUG
      ? "DEBUG: Probando 'Create a folder named test'"
   #endif
   fw_memoEdit( oLlama:Send( "Create a folder named 'test'" ) )
   #ifdef DEBUG
      ? "DEBUG: Probando 'Create a file called test.txt'"
   #endif
   fw_memoEdit( oLlama:Send( "Create a file called 'test.txt'" ) )
   #ifdef DEBUG
      ? "DEBUG: Probando 'Modify the file test.txt with content Hello World'"
   #endif
   fw_memoEdit( oLlama:Send( "Modify the file test.txt with content Hello World" ) )
   
   oLlama:End()
   
return nil

FUNCTION GetCurrentTime( hParams )
   local cTime := Time()
return "The current time is " + cTime  // No necesita parámetros, pero acepta hParams por consistencia

FUNCTION CreateFolder( hParams )
   local cFolder
   if hb_HHasKey( hParams, "folder_name" ) .and. ! Empty( hParams[ "folder_name" ] )
      cFolder = hParams[ "folder_name" ]
      DirMake( cFolder )
      return "Folder '" + cFolder + "' created successfully"
   endif
return "Failed to create folder: no name specified"

FUNCTION CreateFile( hParams )
   local cFile
   if hb_HHasKey( hParams, "filename" ) .and. ! Empty( hParams[ "filename" ] )
      cFile = hParams[ "filename" ]
      hb_MemoWrit( cFile, "" )
      return "File '" + cFile + "' created successfully"
   endif
return "Failed to create file: no name specified"

FUNCTION ModifyFile( hParams )
   local cFile, cContent
   if hb_HHasKey( hParams, "filename" ) .and. ! Empty( hParams[ "filename" ] ) .and. ;
      hb_HHasKey( hParams, "content" ) .and. ! Empty( hParams[ "content" ] )
      cFile = hParams[ "filename" ]
      cContent = hParams[ "content" ]
      hb_MemoWrit( cFile, cContent )
      return "File '" + cFile + "' modified with content: " + cContent
   endif
return "Failed to modify file: missing file name or content"

CLASS TOLlama
   DATA   cModel
   DATA   cPrompt
   DATA   cResponse
   DATA   cUrl
   DATA   hCurl
   DATA   nError INIT 0
   DATA   nHttpCode INIT 0
   DATA   aAgents INIT {}

   METHOD New( cModel )
   METHOD Send( cPrompt, cImageFileName, bWriteFunction )
   METHOD GetPromptCategory( cPrompt )
   METHOD GetToolName( cPrompt, oAgent )
   METHOD End()
   METHOD GetValue()
ENDCLASS

METHOD New( cModel ) CLASS TOLlama
   DEFAULT cModel := "gemma3"
   ::cModel = cModel
   ::cUrl = "http://localhost:11434/api/chat"
   ::hCurl = curl_easy_init()
return Self

METHOD GetPromptCategory( cPrompt ) CLASS TOLlama
   local cJson, hRequest := { => }, hMessage := { => }
   local cCategoryResponse, hResponse
   local nError, cCategories, nI, nJ

   cCategories = ""
   if ! Empty( ::aAgents )
      for nI = 1 to Len( ::aAgents )
         cCategories += "'" + ::aAgents[ nI ]:cCategory + "'"
         if nI < Len( ::aAgents )
            cCategories += ", "
         endif
      next
   else
      cCategories = "'general'"
   endif

   curl_easy_reset( ::hCurl )
   curl_easy_setopt( ::hCurl, HB_CURLOPT_POST, .T. )
   curl_easy_setopt( ::hCurl, HB_CURLOPT_URL, ::cUrl )
   curl_easy_setopt( ::hCurl, HB_CURLOPT_HTTPHEADER, { "Content-Type: application/json" } )
   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. )

   hRequest[ "model" ]       = ::cModel
   hMessage[ "role" ]        = "user"
   hMessage[ "content" ]     = "Classify this prompt: '" + cPrompt + "' into one of these categories: " + ;
                              cCategories + ". Respond with only the category name."
   hRequest[ "messages" ]    = { hMessage }
   hRequest[ "stream" ]      = .F.
   hRequest[ "temperature" ] = 0.5

   cJson = hb_jsonEncode( hRequest )
   curl_easy_setopt( ::hCurl, HB_CURLOPT_POSTFIELDS, cJson )
   
   nError = curl_easy_perform( ::hCurl )
   if nError == HB_CURLE_OK
      cCategoryResponse = curl_easy_dl_buff_get( ::hCurl )
      hb_jsonDecode( cCategoryResponse, @hResponse )
      #ifdef DEBUG
         ? "DEBUG: Categoría devuelta por IA:", hResponse[ "message" ][ "content" ]
      #endif
      return hResponse[ "message" ][ "content" ]
   endif
   #ifdef DEBUG
      ? "DEBUG: Error en GetPromptCategory:", nError
   #endif
return nil

METHOD GetToolName( cPrompt, oAgent ) CLASS TOLlama
   local cJson, hRequest := { => }, hMessage := { => }
   local cToolResponse, hResponse, hToolInfo
   local nError, cTools := "", nI

   if ! Empty( oAgent:aTools )
      for nI = 1 to Len( oAgent:aTools )
         cTools += "'" + oAgent:aTools[ nI ][ 1 ] + "'"
         if nI < Len( oAgent:aTools )
            cTools += ", "
         endif
      next
   endif

   curl_easy_reset( ::hCurl )
   curl_easy_setopt( ::hCurl, HB_CURLOPT_POST, .T. )
   curl_easy_setopt( ::hCurl, HB_CURLOPT_URL, ::cUrl )
   curl_easy_setopt( ::hCurl, HB_CURLOPT_HTTPHEADER, { "Content-Type: application/json" } )
   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. )

   hRequest[ "model" ]       = ::cModel
   hMessage[ "role" ]        = "user"
   hMessage[ "content" ]     = "Given this prompt: '" + cPrompt + "' and category '" + oAgent:cCategory + "', " + ;
                              "select the appropriate tool from: " + cTools + " and extract any relevant parameters. " + ;
                              "Respond with a JSON object containing 'tool' (the tool name) and 'params' (a hash of parameters)."
   hRequest[ "messages" ]    = { hMessage }
   hRequest[ "stream" ]      = .F.
   hRequest[ "temperature" ] = 0.5

   cJson = hb_jsonEncode( hRequest )
   curl_easy_setopt( ::hCurl, HB_CURLOPT_POSTFIELDS, cJson )
   
   nError = curl_easy_perform( ::hCurl )
   if nError == HB_CURLE_OK
      cToolResponse = curl_easy_dl_buff_get( ::hCurl )
      hb_jsonDecode( cToolResponse, @hResponse )
      #ifdef DEBUG
         ? "DEBUG: Respuesta cruda de IA:", cToolResponse
         ? "DEBUG: hResponse tras decodificar:", hb_jsonEncode( hResponse )
         ? "DEBUG: Contenido de hResponse[ 'message' ][ 'content' ]:", hResponse[ "message" ][ "content" ]
      #endif
      hResponse[ "message" ][ "content" ] = SubStr( hResponse[ "message" ][ "content" ], 9 )
      hResponse[ "message" ][ "content" ] = SubStr( hResponse[ "message" ][ "content" ], 1, Len( hResponse[ "message" ][ "content" ] ) - 3 )
      hb_jsonDecode( hResponse[ "message" ][ "content" ], @hToolInfo )
      #ifdef DEBUG
         ? "DEBUG: hToolInfo tras procesar:", hb_jsonEncode( hToolInfo )
         ? "DEBUG: Tipo de hToolInfo:", ValType( hToolInfo )
         if ValType( hToolInfo ) == "H"
            ? "DEBUG: Claves en hToolInfo:", hb_HKeys( hToolInfo )
         endif
      #endif
      return hToolInfo
   endif
   #ifdef DEBUG
      ? "DEBUG: Error en GetToolName:", nError
   #endif
return nil

METHOD Send( cPrompt, cImageFileName, bWriteFunction ) CLASS TOLlama 
   local aHeaders, cJson, hRequest := { => }, hMessage := { => }
   local cBase64Image
   local oAgent, cToolResult, nI, hToolInfo, cToolName, nTool
   local cCategory

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

   if ! Empty( ::aAgents )
      cCategory = ::GetPromptCategory( cPrompt )
      #ifdef DEBUG
         ? "DEBUG: Categoría obtenida (sin limpiar):", cCategory
      #endif
      cCategory = AllTrim( StrTran( StrTran( cCategory, Chr(13), "" ), Chr(10), "" ) )
      #ifdef DEBUG
         ? "DEBUG: Categoría obtenida (limpia):", cCategory
      #endif
      if ! Empty( cCategory )
         for nI = 1 to Len( ::aAgents )
            oAgent = ::aAgents[ nI ]
            #ifdef DEBUG
               ? "DEBUG: Comparando categoría del agente:", oAgent:cCategory, "con categoría obtenida:", cCategory
               ? "DEBUG: Longitud de oAgent:cCategory:", Len( oAgent:cCategory ), "Longitud de cCategory:", Len( cCategory )
               ? "DEBUG: oAgent:cCategory en hex:", hb_StrToHex( oAgent:cCategory )
               ? "DEBUG: cCategory en hex:", hb_StrToHex( cCategory )
               ? "DEBUG: Lower(oAgent:cCategory):", Lower( oAgent:cCategory ), "Lower(cCategory):", Lower( cCategory )
            #endif
            if Lower( AllTrim( oAgent:cCategory ) ) == Lower( AllTrim( cCategory ) )
               #ifdef DEBUG
                  ? "DEBUG: ¡Coincidencia encontrada para categoría!"
               #endif
               if ! Empty( oAgent:aTools )
                  hToolInfo = ::GetToolName( cPrompt, oAgent )
                  #ifdef DEBUG
                     ? "DEBUG: hToolInfo recibido:", hb_jsonEncode( hToolInfo )
                  #endif
                  if ValType( hToolInfo ) == "H" .and. hb_HHasKey( hToolInfo, "tool" )
                     cToolName = AllTrim( StrTran( StrTran( hToolInfo[ "tool" ], Chr(13), "" ), Chr(10), "" ) )
                     #ifdef DEBUG
                        ? "DEBUG: Tool obtenida (limpia):", cToolName
                        ? "DEBUG: Parámetros extraídos:", hb_jsonEncode( hToolInfo[ "params" ] )
                     #endif
                     if ! Empty( cToolName )
                        nTool = 0
                        for nJ = 1 to Len( oAgent:aTools )
                           #ifdef DEBUG
                              ? "DEBUG: Comparando tool del agente:", oAgent:aTools[ nJ ][ 1 ], "con tool obtenida:", cToolName
                              ? "DEBUG: Longitud de oAgent:aTools[", nJ, "][1]:", Len( oAgent:aTools[ nJ ][ 1 ] ), "Longitud de cToolName:", Len( cToolName )
                              ? "DEBUG: oAgent:aTools[", nI, "][1] en hex:", hb_StrToHex( oAgent:aTools[ nJ ][ 1 ] )
                              ? "DEBUG: cToolName en hex:", hb_StrToHex( cToolName )
                              ? "DEBUG: Lower(oAgent:aTools[", nJ, "][1]):", Lower( oAgent:aTools[ nJ ][ 1 ] ), "Lower(cToolName):", Lower( cToolName )
                           #endif
                           if Lower( AllTrim( oAgent:aTools[ nJ ][ 1 ] ) ) == Lower( AllTrim( cToolName ) )
                              nTool = nJ
                              #ifdef DEBUG
                                 ? "DEBUG: ¡Coincidencia encontrada para tool!"
                              #endif
                              exit
                           endif
                        next
                        if nTool > 0
                           cToolResult = Eval( oAgent:aTools[ nTool ][ 2 ], hToolInfo[ "params" ] )
                           #ifdef DEBUG
                              ? "DEBUG: Resultado de la tool:", cToolResult
                           #endif
                           ::cResponse = hb_jsonEncode( { "message" => { "content" => cToolResult }, "done" => .T. } )
                           return ::cResponse
                        else
                           #ifdef DEBUG
                              ? "DEBUG: Tool '" + cToolName + "' no encontrada en el agente '" + oAgent:cCategory + "'"
                           #endif
                           ::cResponse = hb_jsonEncode( { "message" => { "content" => "Tool not found" }, "done" => .T. } )
                           return ::cResponse
                        endif
                     else
                        #ifdef DEBUG
                           ? "DEBUG: No se obtuvo nombre de tool válido"
                        #endif
                        ::cResponse = hb_jsonEncode( { "message" => { "content" => "No tool selected" }, "done" => .T. } )
                        return ::cResponse
                     endif
                  else
                     #ifdef DEBUG
                        ? "DEBUG: Respuesta de GetToolName no válida o sin 'tool'. Tipo:", ValType( hToolInfo )
                        if ValType( hToolInfo ) == "H"
                           ? "DEBUG: Claves en hToolInfo:", hb_HKeys( hToolInfo )
                        endif
                     #endif
                     ::cResponse = hb_jsonEncode( { "message" => { "content" => "Invalid tool response" }, "done" => .T. } )
                     return ::cResponse
                  endif
               else
                  #ifdef DEBUG
                     ? "DEBUG: El agente '" + oAgent:cCategory + "' no tiene tools"
                  #endif
                  ::cResponse = hb_jsonEncode( { "message" => { "content" => "No tools available" }, "done" => .T. } )
                  return ::cResponse
               endif
            else
               #ifdef DEBUG
                  ? "DEBUG: No hay coincidencia entre '" + Lower( AllTrim( oAgent:cCategory ) ) + "' y '" + Lower( AllTrim( cCategory ) ) + "'"
               #endif
            endif
         next
         #ifdef DEBUG
            ? "DEBUG: No se encontró agente para la categoría '" + cCategory + "'"
         #endif
         ::cResponse = hb_jsonEncode( { "message" => { "content" => "Agent not found" }, "done" => .T. } )
         return ::cResponse
      else
         #ifdef DEBUG
            ? "DEBUG: No se obtuvo categoría válida"
         #endif
      endif
   else
      #ifdef DEBUG
         ? "DEBUG: No hay agentes definidos"
      #endif
   endif

   #ifdef DEBUG
      ? "DEBUG: Llamando a la API de Ollama"
   #endif
   curl_easy_reset( ::hCurl )
   curl_easy_setopt( ::hCurl, HB_CURLOPT_POST, .T. )
   curl_easy_setopt( ::hCurl, HB_CURLOPT_URL, ::cUrl )
   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_SSL_VERIFYPEER, .F. )

   hRequest[ "model" ]       = ::cModel
   hMessage[ "role" ]        = "user"
   hMessage[ "content" ]     = ::cPrompt
   hRequest[ "messages" ]    = { hMessage }
   hRequest[ "temperature" ] = 0.5

   if ! Empty( cImageFileName )
      if File( cImageFileName )
         cBase64Image = hb_base64Encode( memoRead( cImageFileName ) )
         hMessage[ "images" ] = { cBase64Image }
      else
         MsgAlert( "Image " + cImageFileName + " not found" )
         return nil
      endif
   endif

   if bWriteFunction != nil
      hRequest[ "stream" ] = .T.
      curl_easy_setopt( ::hCurl, HB_CURLOPT_WRITEFUNCTION, bWriteFunction )
   else
      hRequest[ "stream" ] = .F.
      curl_easy_setopt( ::hCurl, HB_CURLOPT_DL_BUFF_SETUP )
   endif

   cJson = hb_jsonEncode( hRequest )
   curl_easy_setopt( ::hCurl, HB_CURLOPT_POSTFIELDS, cJson )
   
   ::nError = curl_easy_perform( ::hCurl )
   curl_easy_getinfo( ::hCurl, HB_CURLINFO_RESPONSE_CODE, @::nHttpCode )

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

METHOD End() CLASS TOLlama
   curl_easy_cleanup( ::hCurl )
   ::hCurl = nil
return nil

METHOD GetValue() CLASS TOLlama
   local hResponse, uValue  
   hb_jsonDecode( ::cResponse, @hResponse )
   TRY 
      uValue = hResponse[ "message" ][ "content" ]
   CATCH
      uValue = hResponse[ "error" ][ "message" ]
   END   
return uValue

CLASS TAgent 
   DATA cCategory
   DATA aTools
   METHOD New( cCategory, aTools )
ENDCLASS      

METHOD New( cCategory, aTools ) CLASS TAgent
   ::cCategory = cCategory
   ::aTools = aTools
return Self
regards, saludos

Antonio Linares
www.fivetechsoft.com
chiaiese
Posts: 89
Joined: Wed Feb 08, 2006 10:32 pm
Location: Roma, Italia
Been thanked: 2 times
Contact:

Re: Class TOllama with Agents!

Post by chiaiese »

Hi Antonio,
I tested this class and to make it work for me I had to rename some parameter names as this:

Code: Select all | Expand

FUNCTION CreateFolder( hParams )
   local cFolder
   if hb_HHasKey( hParams, "folder_name" ) .and. ! Empty( hParams[ "folder_name" ] )
      cFolder = hParams[ "folder_name" ]
      DirMake( cFolder )
      return "Folder '" + cFolder + "' created successfully"
   endif
return "Failed to create folder: no name specified"

FUNCTION CreateFile( hParams )
   local cFile
   if hb_HHasKey( hParams, "filename" ) .and. ! Empty( hParams[ "filename" ] )
      cFile = hParams[ "filename" ]
      hb_MemoWrit( cFile, "" )
      return "File '" + cFile + "' created successfully"
   endif
return "Failed to create file: no name specified"

FUNCTION ModifyFile( hParams )
   local cFile, cContent
   if hb_HHasKey( hParams, "filename" ) .and. ! Empty( hParams[ "filename" ] ) .and. ;
      hb_HHasKey( hParams, "content" ) .and. ! Empty( hParams[ "content" ] )
      cFile = hParams[ "filename" ]
      cContent = hParams[ "content" ]
      hb_MemoWrit( cFile, cContent )
      return "File '" + cFile + "' modified with content: " + cContent
   endif
return "Failed to modify file: missing file name or content"
does it mean that parameter names the AI generates could be different from one machine to another?
Roberto
Roberto Chiaiese
R&C Informatica S.n.c.
https://www.recinformatica.it
[email protected]
User avatar
Antonio Linares
Site Admin
Posts: 42847
Joined: Thu Oct 06, 2005 5:47 pm
Location: Spain
Has thanked: 181 times
Been thanked: 124 times
Contact:

Re: Class TOllama with Agents!

Post by Antonio Linares »

Dear Roberto,

many thanks!

Are you using Gemma3 too ?

We should use a good quality LLM and also low temperature
regards, saludos

Antonio Linares
www.fivetechsoft.com
chiaiese
Posts: 89
Joined: Wed Feb 08, 2006 10:32 pm
Location: Roma, Italia
Been thanked: 2 times
Contact:

Re: Class TOllama with Agents!

Post by chiaiese »

Antonio,
yes, I'm using "gemma3" model with Ollama version 0.6.3

maybe we have to instruct Ollama on JSON parameter names too... :-)
Roberto Chiaiese
R&C Informatica S.n.c.
https://www.recinformatica.it
[email protected]
chiaiese
Posts: 89
Joined: Wed Feb 08, 2006 10:32 pm
Location: Roma, Italia
Been thanked: 2 times
Contact:

Re: Class TOllama with Agents!

Post by chiaiese »

I slightly modified the Prompt:

Code: Select all | Expand

   hMessage[ "content" ]     = "Given this prompt: '" + cPrompt + "' and category '" + oAgent:cCategory + "', " + ;
                              "select the appropriate tool from: " + cTools + " and extract any relevant parameters. " + ;
                              "Respond with a JSON object containing 'tool' (the tool name) and 'params' (a hash of parameters)."+;
			      "Name the parameters as 'Folder_Name' for folders, as 'File_Name' for file names and as 'Content' for file content."
and Ollama answered correctly respecting also the capitalization.
Of course the Prompt should be standardized to respect the fields that we expect to be returned.
Roberto Chiaiese
R&C Informatica S.n.c.
https://www.recinformatica.it
[email protected]
User avatar
Antonio Linares
Site Admin
Posts: 42847
Joined: Thu Oct 06, 2005 5:47 pm
Location: Spain
Has thanked: 181 times
Been thanked: 124 times
Contact:

Re: Class TOllama with Agents!

Post by Antonio Linares »

We may also set the temperature used in the code to 0.1 or 0, so we tell the LLM to be more conservative when answering
regards, saludos

Antonio Linares
www.fivetechsoft.com
User avatar
Antonio Linares
Site Admin
Posts: 42847
Joined: Thu Oct 06, 2005 5:47 pm
Location: Spain
Has thanked: 181 times
Been thanked: 124 times
Contact:

Re: Class TOllama with Agents!

Post by Antonio Linares »

regards, saludos

Antonio Linares
www.fivetechsoft.com
Post Reply