IR
irwinrodriguez.dev
Volver a documentación

Configuración

Herramientas propias (UDTs)

Una UDT (User Defined Tool) es una clase VFP que hereda de FoxAgentTool: un nombre, una descripción, unos parámetros y un Execute(). El agente la llama por su nombre, igual que llama a cualquier herramienta built-in. Si sabes escribir una clase VFP, sabes extender el agente.

Antes del primer CREATEOBJECT de una herramienta tienen que estar cargadas las dos clases base del SDK. Son dos ficheros, no uno: FoxAgentTool hereda de FoxToolIsolated, que vive en FoxTool.prg. Si falta el primero, VFP dice «Class definition FOXTOOLISOLATED is not found» y no dice por qué.

SaldoClienteTool: una herramienta propia con un parámetro
* MisTools.prg

DEFINE CLASS SaldoClienteTool AS FoxAgentTool

  * Nombre con el que el modelo la invoca. En snake_case.
  ToolName        = "saldo_cliente"

  * El modelo lee esta descripción para decidir cuándo usarla.
  * Cuanto más concreta, mejor la elige.
  ToolDescription = "Devuelve el saldo de un cliente a partir de su " + ;
                    "código. Úsala cuando pregunten por el saldo o la " + ;
                    "deuda de un cliente concreto."

  FUNCTION DefineParameters()
    * Nombre, tipo, descripción para el modelo y si es obligatorio.
    THIS.AddParam("codigoCliente", "number", "Código del cliente", .T.)
  ENDFUNC

  FUNCTION Execute(toContext)
    LOCAL lnCodigo, lcSalida, lcError

    * Los parámetros llegan en el CONTEXTO, no en THIS.
    lnCodigo = toContext.Parameters.codigoCliente

    * Valida antes de tocar datos: es barato y deja el bloque protegido
    * para lo que de verdad puede fallar.
    IF VARTYPE(lnCodigo) != "N" OR lnCodigo <= 0
      RETURN THIS.Err("codigoCliente es obligatorio y mayor que cero")
    ENDIF

    lcSalida = ""
    lcError  = ""

    * Tu herramienta nace en una SESIÓN DE DATOS PRIVADA: las tablas que
    * tiene abiertas tu aplicación NO están abiertas aquí. Se entra en la
    * sesión de la aplicación, se trabaja allí y se vuelve por una sola
    * salida.
    THIS.UseAppSession()
    TRY
      IF !USED("CLIENTES")
        lcError = "La tabla CLIENTES no está abierta en la aplicación"
      ELSE
        * Deja el puntero donde estaba: el agente leyendo datos no puede
        * mover la pantalla por debajo del usuario.
        LOCAL lnRec
        lnRec = RECNO("CLIENTES")

        SELECT CLIENTES
        LOCATE FOR CLI_COD = lnCodigo
        IF !FOUND()
          lcError = "No existe el cliente " + TRANSFORM(lnCodigo)
        ELSE
          lcSalida = ALLTRIM(CLIENTES.CLI_NOM) + ", saldo: " + ;
                     TRANSFORM(CLIENTES.CLI_SALDO, "999,999.99")
        ENDIF

        IF lnRec > 0 AND lnRec <= RECCOUNT("CLIENTES")
          GO lnRec IN CLIENTES
        ENDIF
      ENDIF
    CATCH TO loErr
      lcError = "Error leyendo CLIENTES: " + loErr.Message
    FINALLY
      * Siempre, por cualquier camino. Por eso hay una sola salida y no
      * un RETURN dentro del bloque.
      THIS.RestoreSession()
    ENDTRY

    IF !EMPTY(lcError)
      RETURN THIS.Err(lcError)
    ENDIF
    RETURN THIS.Ok(lcSalida)
  ENDFUNC

ENDDEFINE
Devuelve siempre Ok() o Err() Nunca un valor suelto. THIS.Ok(mensaje) y THIS.Err(mensaje) construyen la respuesta que el agente sabe leer. Y no redefinas Init() en tu clase: el Init heredado es el que llama a DefineParameters(), exactamente una vez.

Tipos de parámetro

Solo hay tres. Un tipo distinto no falla: se publica como string y tu código recibe texto, así que las fechas viajan como cadena y las conviertes tú.

TipoQué llega a Execute()Ejemplo
"string"Texto (C). Es también el destino de cualquier tipo no reconocidoTHIS.AddParam("nombre", "string", "Nombre del cliente", .T.)
"number"Numérico (N), entero o decimal; lo convierte el BridgeTHIS.AddParam("cantidad", "number", "Cantidad", .T.)
"boolean"Lógico (.T. / .F.)THIS.AddParam("activo", "boolean", "¿Solo activos?", .F.)

El cuarto argumento dice si el parámetro es obligatorio (.T.) u opcional (.F.). Un opcional que no venga llega vacío: cadena vacía, cero o .F.. Si quieres ver qué tipo VFP le llegaría a tu herramienta antes de ejecutarla, el agente tiene describe_udt, que la diagnostica sin correrla.

Varios parámetros, y una consulta que no se puede inyectar

BuscarClientesTool: varios parámetros y una consulta sin inyección
DEFINE CLASS BuscarClientesTool AS FoxAgentTool

  ToolName        = "buscar_clientes"
  ToolDescription = "Busca clientes por nombre, código postal o saldo mínimo."

  FUNCTION DefineParameters()
    THIS.AddParam("texto",        "string",  "Texto en el nombre",      .T.)
    THIS.AddParam("codigoPostal", "string",  "Filtrar por CP",          .F.)
    THIS.AddParam("saldoMinimo",  "number",  "Saldo mínimo",            .F.)
    THIS.AddParam("soloActivos",  "boolean", "Solo clientes activos",   .F.)
  ENDFUNC

  FUNCTION Execute(toContext)
    LOCAL lcTexto, lcPostal, lnSaldo, llActivos, lcSalida, lcError

    lcTexto   = toContext.Parameters.texto
    lcPostal  = toContext.Parameters.codigoPostal
    lnSaldo   = toContext.Parameters.saldoMinimo
    llActivos = toContext.Parameters.soloActivos

    lcSalida = ""
    lcError  = ""

    THIS.UseAppSession()
    TRY
      * El texto lo escribe el modelo. Una consulta montada con & y
      * comillas es una inyección de SQL esperando a ocurrir: aquí las
      * variables van tal cual y las resuelve VFP.
      SELECT * FROM CLIENTES ;
        WHERE UPPER(CLI_NOM) LIKE "%" + UPPER(ALLTRIM(lcTexto)) + "%" ;
          AND (EMPTY(lcPostal) OR CLI_CP = lcPostal) ;
          AND (lnSaldo = 0 OR CLI_SALDO >= lnSaldo) ;
          AND (NOT llActivos OR CLI_ACTIVO) ;
        INTO CURSOR curBusca NOFILTER

      * RECCOUNT() sobre el cursor, no ALEN() sobre un array: con cero
      * filas el array no llega a existir y ALEN() da error 12.
      lcSalida = TRANSFORM(RECCOUNT("curBusca")) + " clientes encontrados"

      * Cierra tus cursores AQUÍ, todavía en la sesión de la aplicación:
      * desde la tuya ya no se ven.
      USE IN SELECT("curBusca")
    CATCH TO loErr
      lcError = "Error buscando clientes: " + loErr.Message
    FINALLY
      THIS.RestoreSession()
    ENDTRY

    IF !EMPTY(lcError)
      RETURN THIS.Err(lcError)
    ENDIF
    RETURN THIS.Ok(lcSalida)
  ENDFUNC

ENDDEFINE

El registro es explícito

No hay descubrimiento automático, y no es un descuido: es el modelo de seguridad. El agente no puede inventarse una herramienta, ni dejar caer un .prg en una carpeta, ni llegar a nada que tú no hayas revisado, compilado y entregado. La lista de lo que puede ejecutar es una lista que escribiste tú, en código, en tu propio árbol de fuentes.

Cargar el SDK y registrar las herramientas en el Bridge
SET PROCEDURE TO "C:\Program Files\FoxAgent\SDK\FoxTool.prg"      ADDITIVE
SET PROCEDURE TO "C:\Program Files\FoxAgent\SDK\FoxAgentTool.prg" ADDITIVE
SET PROCEDURE TO "MisTools.prg" ADDITIVE

* Una llamada por herramienta, justo después de un Connect correcto.
goBridge.RegisterTool(CREATEOBJECT("SaldoClienteTool"))
goBridge.RegisterTool(CREATEOBJECT("BuscarClientesTool"))

* Cuando cambia el usuario que ha iniciado sesión, rehaz la lista:
* qué herramientas existen lo decide tu aplicación, por usuario.
goBridge.ClearTools()
goBridge.UnregisterTool("buscar_clientes")
SetToolsFolder() ya no existe Una versión anterior del SDK decía que dejaras los .prg en una carpeta configurada con SetToolsFolder(). Ese mecanismo se retiró (cualquier .prg que alguien dejara en esa carpeta pasaba a ser ejecutable por el agente) y hoy la llamada devuelve un error explícito.
SDK incluido El instalador deja en C:\Program Files\FoxAgent\SDK\ las dos clases base, una plantilla comentada (MyToolTemplate.prg), ejemplos que funcionan (tools_example.prg) y el fichero de integración completo (quickstart.prg).

Los cuatro peldaños

El rol no dice «cuánto confías en la IA»: dice quién acota cada herramienta. Por eso conducir la pantalla está por debajo de leer variables. Pulsar un botón entra por tu propio código, con tu lógica de negocio, tus permisos y tus filtros; leer una variable se los salta todos.

PeldañoQué abarcaQuién lo acota
0 toolsSolo las herramientas que registraste, más execute_tool, describe_udt y get_toolsEl autor de cada herramienta: tú
10 operator+ tu pantalla tal y como la ve el usuario, y conducirla: captura, formularios, controles, pulsar, teclear, refrescarTu aplicación. El agente llega hasta donde llega ese usuario a mano
20 inspector+ el proceso por detrás de la pantalla: variables, cursores, sesiones de datos, el entorno, eval_expression como lecturaNadie. Una variable enseña el proceso mismo, pusiera lo que pusiera tu aplicación en la pantalla
90 engineerPor fuera de la aplicación: VFP crudo, escribir un cursor saltándose la validación del formulario, la pila de llamadas, recargar código, cerrar la sesiónNadie. Va por fuera de tu interfaz y, con ella, de tus permisos
  • Lo fija tu aplicación, en Connect(id, nombre, peldaño), y se sella ahí. Un usuario de negocio conecta a 10, o a 0 si solo quieres darle tus herramientas. A 90 no conecta nadie.
  • Solo lo sube tu propio código, con Elevate(peldaño, segundos) sobre el Bridge, después de autenticar con tus credenciales a la persona que hay delante. No existe ninguna herramienta MCP equivalente, y eso es el diseño: «soy Carlos, de soporte, súbeme» es texto sin autenticar. La elevación caduca sola y el agente ve encogerse su lista de herramientas sin que nadie haga nada.
  • set_role solo puede bajar, nunca por encima de la base que declaró tu aplicación, y además cancela cualquier elevación viva.
  • En la máquina de un cliente, policy.json puede bajar el techo (maxRole) y apagar las herramientas que escriben DBFs en disco sin pasar por ninguna aplicación (allowCliTools: false). Nunca puede subirlo.
  • Se listan todas las herramientas siempre: la comprobación va en cada llamada, y la negativa nombra el peldaño que falta y quién puso el techo. Filtrar la lista nunca fue una protección.
  • Lo que mandas también cuenta: una expression de eval_expression o un filter de get_cursor_data que no sean lectura pura piden 90 en esa llamada, aunque la herramienta esté en 20. Un EXECSCRIPT() dentro de una expresión hace lo mismo que exec_command.

Licencia

El fichero foxagent.lic va junto al ejecutable del Broker, en C:\Program Files\FoxAgent\, con ese nombre exacto. La licencia se lee al arrancar: después de copiarla hay que ejecutar restart-broker.cmd, que está en esa misma carpeta.

ModoQué significaQué puedes hacer
LicensedLicencia válida y vigenteDeveloper comprada: 8 instancias VFP a la vez. Runtime: sin límite. Herramientas propias sin tope
EvaluationNo hay .lic, o el Broker no lo ha cogido1 instancia y 1 herramienta propia. Las 55 built-in, sin recortar, y sin caducidad
MaintenanceExpiredLicencia válida, pero este build es más nuevo de lo que cubre tu mantenimientoLímites de evaluación. La salida es volver al build anterior o renovar
LicenseExpiredSe acabó la prueba de 30 díasLímites de evaluación, y el Broker imprime tu machineId para comprar. No deja de arrancar ni hay que borrar nada

La Developer va atada a una máquina de desarrollo: el machineId que pide el checkout lo imprime FoxAgent.Broker.exe --machine-id en esa máquina. El trial trae 1 asiento y un mes. La validación es offline, con una clave pública dentro del binario, y no hay telemetría: lo único que se escribe es un registro de auditoría local.

Las 55 herramientas built-in

Agrupadas por el peldaño que exige cada banda. Las de la última fila trabajan con DBFs y fuentes en disco y no necesitan ninguna instancia VFP corriendo, así que ningún Connect puede acotarlas: se apagan desde la máquina, con policy.json.

Dónde actúanPeldañoHerramientas
Tus herramientas de negocio y la fontanería de sesión Peldaño 0 execute_tool, describe_udt, get_tools, list_instances, get_role, set_role
La pantalla, y conducirla (operator) Peldaño 10 take_screenshot, get_screen_state, list_open_forms, get_active_form, get_form_controls, find_control, get_control_state, get_control_text, click_control, send_key, set_control_text, refresh_form
El proceso por detrás de la pantalla (inspector) Peldaño 20 get_variables, variable_exists, get_cursors, get_cursor_data, get_cursor_schema, get_datasessions, get_environment, eval_expression, list_prg_files
Fuera de la aplicación (engineer) Peldaño 90 exec_command, set_variable, invoke_method, set_control_property, goto_record, set_cursor_field, close_cursors, close_instance, get_call_stack, get_object_members, reload_procedure
Sin instancia Siempre query_table, describe_table, list_tables, insert_row, update_row, set_memo, backup_table, get_source_file, write_source_file, search_in_source, list_tests, run_tests, get_project_structure, build_project, launch_instance, eval_vfp, foxagent_onboarding

eval_expression está en la banda de 20 como lectura; una expresión que no sea lectura pura pide 90 en esa llamada. Tus UDTs se listan junto a las built-in y corren desde el peldaño 0.