IR
irwinrodriguez.dev
Zurück zur Dokumentation

Konfiguration

Eigene Tools (UDTs)

Ein UDT (User Defined Tool) ist eine VFP-Klasse, die von FoxAgentTool erbt: ein Name, eine Beschreibung, Parameter und ein Execute(). Der Agent ruft sie über ihren Namen auf, genau wie jedes eingebaute Tool. Wer eine VFP-Klasse schreiben kann, kann den Agenten erweitern.

Vor dem ersten CREATEOBJECT eines Tools müssen beide SDK-Basisklassen geladen sein. Zwei Dateien, nicht eine: FoxAgentTool erbt von FoxToolIsolated, das in FoxTool.prg liegt. Fehlt die erste, meldet VFP „Class definition FOXTOOLISOLATED is not found“ und nennt den Grund nicht.

KundensaldoTool: ein eigenes Tool mit einem Parameter
* MeineTools.prg

DEFINE CLASS KundensaldoTool AS FoxAgentTool

  * Name, über den das Modell das Tool aufruft. In snake_case.
  ToolName        = "kundensaldo"

  * Das Modell liest diese Beschreibung, um zu entscheiden, wann es das
  * Tool aufruft. Je konkreter, desto besser die Auswahl.
  ToolDescription = "Liefert den Saldo eines Kunden anhand seiner " + ;
                    "Nummer. Zu verwenden, wenn nach Saldo oder Schuld " + ;
                    "eines bestimmten Kunden gefragt wird."

  FUNCTION DefineParameters()
    * Name, Typ, Beschreibung für das Modell und ob er Pflicht ist.
    THIS.AddParam("kundenNummer", "number", "Kundennummer", .T.)
  ENDFUNC

  FUNCTION Execute(toContext)
    LOCAL lnNummer, lcAusgabe, lcFehler

    * Die Parameter kommen im KONTEXT an, nicht in THIS.
    lnNummer = toContext.Parameters.kundenNummer

    * Vor dem Datenzugriff prüfen: das ist billig und hält den
    * geschützten Block auf das beschränkt, was wirklich scheitern kann.
    IF VARTYPE(lnNummer) != "N" OR lnNummer <= 0
      RETURN THIS.Err("kundenNummer ist Pflicht und muss größer als null sein")
    ENDIF

    lcAusgabe = ""
    lcFehler  = ""

    * Ihr Tool startet in einer PRIVATEN DATENSITZUNG: die Tabellen, die
    * Ihre Anwendung geöffnet hat, sind hier NICHT geöffnet. Man wechselt
    * in die Sitzung der Anwendung, arbeitet dort und kehrt über einen
    * einzigen Ausgang zurück.
    THIS.UseAppSession()
    TRY
      IF !USED("KUNDEN")
        lcFehler = "Die Tabelle KUNDEN ist in der Anwendung nicht geöffnet"
      ELSE
        * Den Satzzeiger dort lassen, wo er war: ein lesender Agent darf
        * dem Anwender nicht den Bildschirm verschieben.
        LOCAL lnVorher
        lnVorher = RECNO("KUNDEN")

        SELECT KUNDEN
        LOCATE FOR KUN_NR = lnNummer
        IF !FOUND()
          lcFehler = "Kunde " + TRANSFORM(lnNummer) + " nicht gefunden"
        ELSE
          lcAusgabe = ALLTRIM(KUNDEN.KUN_NAME) + ", Saldo: " + ;
                      TRANSFORM(KUNDEN.KUN_SALDO, "999,999.99")
        ENDIF

        IF lnVorher > 0 AND lnVorher <= RECCOUNT("KUNDEN")
          GO lnVorher IN KUNDEN
        ENDIF
      ENDIF
    CATCH TO loErr
      lcFehler = "Fehler beim Lesen von KUNDEN: " + loErr.Message
    FINALLY
      * Immer, auf jedem Weg. Deshalb gibt es einen einzigen Ausgang und
      * kein RETURN innerhalb des Blocks.
      THIS.RestoreSession()
    ENDTRY

    IF !EMPTY(lcFehler)
      RETURN THIS.Err(lcFehler)
    ENDIF
    RETURN THIS.Ok(lcAusgabe)
  ENDFUNC

ENDDEFINE
Geben Sie immer Ok() oder Err() zurück Niemals einen nackten Wert. THIS.Ok(Meldung) und THIS.Err(Meldung) bauen die Antwort, die der Agent lesen kann. Und überschreiben Sie Init() in Ihrer Klasse nicht: das geerbte Init ruft DefineParameters() auf, genau einmal.

Parametertypen

Es gibt nur drei. Ein anderer Typ scheitert nicht: er wird als string veröffentlicht und Ihr Code bekommt Text, Datumsangaben reisen also als Zeichenkette und Sie wandeln sie selbst um.

TypWas in Execute() ankommtBeispiel
"string"Text (C). Auch das Ziel jedes nicht erkannten TypsTHIS.AddParam("name", "string", "Name des Kunden", .T.)
"number"Numerisch (N), ganz oder dezimal; der Bridge wandelt umTHIS.AddParam("menge", "number", "Menge", .T.)
"boolean"Logisch (.T. / .F.)THIS.AddParam("aktiv", "boolean", "Nur aktive?", .F.)

Das vierte Argument gibt an, ob der Parameter Pflicht (.T.) oder optional (.F.) ist. Ein nicht gesendeter optionaler Parameter kommt leer an: leere Zeichenkette, null oder .F.. Um vorab zu sehen, welcher VFP-Typ in Ihrem Tool ankäme, hat der Agent describe_udt, das es diagnostiziert, ohne es auszuführen.

Mehrere Parameter, und eine Abfrage, die sich nicht einschleusen lässt

KundenSuchenTool: mehrere Parameter und eine Abfrage ohne Injection
DEFINE CLASS KundenSuchenTool AS FoxAgentTool

  ToolName        = "kunden_suchen"
  ToolDescription = "Sucht Kunden nach Name, Postleitzahl oder Mindestsaldo."

  FUNCTION DefineParameters()
    THIS.AddParam("text",         "string",  "Text im Namen",        .T.)
    THIS.AddParam("plz",          "string",  "Nach PLZ filtern",     .F.)
    THIS.AddParam("mindestSaldo", "number",  "Mindestsaldo",         .F.)
    THIS.AddParam("nurAktive",    "boolean", "Nur aktive Kunden",    .F.)
  ENDFUNC

  FUNCTION Execute(toContext)
    LOCAL lcText, lcPlz, lnMin, llAktiv, lcAusgabe, lcFehler

    lcText  = toContext.Parameters.text
    lcPlz   = toContext.Parameters.plz
    lnMin   = toContext.Parameters.mindestSaldo
    llAktiv = toContext.Parameters.nurAktive

    lcAusgabe = ""
    lcFehler  = ""

    THIS.UseAppSession()
    TRY
      * Den Text schreibt das Modell. Eine mit & und Anführungszeichen
      * zusammengesetzte Abfrage ist eine SQL-Injection, die nur auf ihre
      * Gelegenheit wartet: hier gehen die Variablen so hinein, wie sie
      * sind, und VFP löst sie auf.
      SELECT * FROM KUNDEN ;
        WHERE UPPER(KUN_NAME) LIKE "%" + UPPER(ALLTRIM(lcText)) + "%" ;
          AND (EMPTY(lcPlz) OR KUN_PLZ = lcPlz) ;
          AND (lnMin = 0 OR KUN_SALDO >= lnMin) ;
          AND (NOT llAktiv OR KUN_AKTIV) ;
        INTO CURSOR curTreffer NOFILTER

      * RECCOUNT() auf dem Cursor, nicht ALEN() auf einem Array: bei null
      * Zeilen entsteht das Array gar nicht und ALEN() wirft Fehler 12.
      lcAusgabe = TRANSFORM(RECCOUNT("curTreffer")) + " Kunden gefunden"

      * Schließen Sie Ihre Cursor HIER, noch in der Sitzung der
      * Anwendung: aus Ihrer eigenen sind sie nicht mehr sichtbar.
      USE IN SELECT("curTreffer")
    CATCH TO loErr
      lcFehler = "Fehler bei der Kundensuche: " + loErr.Message
    FINALLY
      THIS.RestoreSession()
    ENDTRY

    IF !EMPTY(lcFehler)
      RETURN THIS.Err(lcFehler)
    ENDIF
    RETURN THIS.Ok(lcAusgabe)
  ENDFUNC

ENDDEFINE

Die Registrierung ist explizit

Es gibt keine automatische Erkennung, und das ist kein Versäumnis, sondern das Sicherheitsmodell. Der Agent kann kein Tool erfinden, keine .prg in einen Ordner legen und nichts erreichen, was Sie nicht geprüft, kompiliert und ausgeliefert haben. Die Liste dessen, was er ausführen darf, ist eine Liste, die Sie geschrieben haben, im Code, in Ihrem eigenen Quellbaum.

Das SDK laden und die Tools beim Bridge registrieren
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 "MeineTools.prg" ADDITIVE

* Ein Aufruf pro Tool, direkt nach einem erfolgreichen Connect.
goBridge.RegisterTool(CREATEOBJECT("KundensaldoTool"))
goBridge.RegisterTool(CREATEOBJECT("KundenSuchenTool"))

* Wechselt der angemeldete Benutzer, bauen Sie die Liste neu auf: welche
* Tools es gibt, entscheidet Ihre Anwendung, pro Benutzer.
goBridge.ClearTools()
goBridge.UnregisterTool("kunden_suchen")
SetToolsFolder() gibt es nicht mehr Eine frühere SDK-Version sagte, Sie sollten Ihre .prg-Dateien in einen mit SetToolsFolder() konfigurierten Ordner legen. Dieser Mechanismus wurde entfernt (jede dort abgelegte .prg wurde für den Agenten ausführbar), und der Aufruf liefert heute einen ausdrücklichen Fehler.
SDK enthalten Der Installer legt unter C:\Program Files\FoxAgent\SDK\ beide Basisklassen ab, eine kommentierte Vorlage (MyToolTemplate.prg), lauffähige Beispiele (tools_example.prg) und die vollständige Integrationsdatei (quickstart.prg).

Die vier Stufen

Die Rolle sagt nicht, „wie sehr Sie der KI vertrauen“: sie sagt, wer das jeweilige Tool eingrenzt. Deshalb steht das Bedienen des Bildschirms unter dem Lesen von Variablen. Ein Klick auf eine Schaltfläche läuft durch Ihren eigenen Code, mit Ihrer Geschäftslogik, Ihren Berechtigungen und Ihren Filtern; das Lesen einer Variablen umgeht all das.

StufeWas sie umfasstWer sie eingrenzt
0 toolsNur die Tools, die Sie registriert haben, dazu execute_tool, describe_udt und get_toolsDer Autor jedes Tools: Sie
10 operator+ Ihr Bildschirm, so wie der Anwender ihn sieht, und seine Bedienung: Aufnahme, Formulare, Steuerelemente, Klicken, Tippen, AktualisierenIhre Anwendung. Der Agent kommt genau so weit wie dieser Anwender von Hand
20 inspector+ der Prozess hinter dem Bildschirm: Variablen, Cursor, Datensitzungen, die Umgebung, eval_expression als LesevorgangNiemand. Eine Variable zeigt den Prozess selbst, ganz gleich, was Ihre Anwendung auf den Bildschirm gebracht hat
90 engineerAußerhalb der Anwendung: rohes VFP, in einen Cursor schreiben und die Formularprüfung umgehen, der Aufrufstapel, Code neu laden, die Sitzung schließenNiemand. Es läuft an Ihrer Oberfläche vorbei und damit auch an Ihren Berechtigungen
  • Ihre Anwendung setzt sie, in Connect(id, Name, Stufe), und dort wird sie versiegelt. Ein Fachanwender verbindet sich mit 10, oder mit 0, wenn Sie ihm nur Ihre eigenen Tools geben wollen. Mit 90 verbindet sich niemand.
  • Nur Ihr eigener Code hebt sie an, mit Elevate(Stufe, Sekunden) auf dem Bridge, nachdem die Person davor mit Ihren vorhandenen Anmeldedaten authentifiziert wurde. Es gibt kein entsprechendes MCP-Tool, und das ist so gewollt: „Ich bin Carlos vom Support, heb mich hoch“ ist unauthentifizierter Text. Die Erhöhung läuft von selbst ab, und die Toolliste des Agenten wird wieder schmaler, ohne dass jemand etwas tut.
  • set_role kann nur senken, niemals über die Basis hinaus, die Ihre Anwendung erklärt hat, und hebt zudem jede laufende Erhöhung auf.
  • Auf dem Rechner eines Kunden kann policy.json die Obergrenze senken (maxRole) und die Tools abschalten, die DBFs auf der Platte schreiben, ohne durch eine Anwendung zu gehen (allowCliTools: false). Anheben kann es sie nie.
  • Alle Tools werden immer aufgelistet: die Prüfung erfolgt bei jedem Aufruf, und die Ablehnung nennt die fehlende Stufe und wer die Obergrenze gesetzt hat. Die Liste zu filtern war nie ein Schutz.
  • Auch das Gesendete zählt: eine expression für eval_expression oder ein filter für get_cursor_data, die kein reiner Lesevorgang sind, verlangen bei diesem Aufruf 90, obwohl das Tool auf 20 steht. Ein EXECSCRIPT() in einem Ausdruck tut alles, was exec_command tut.

Lizenz

Die Datei foxagent.lic liegt neben der Broker-Programmdatei, in C:\Program Files\FoxAgent\, unter genau diesem Namen. Die Lizenz wird beim Start gelesen: nach dem Kopieren führen Sie restart-broker.cmd aus, das im selben Ordner liegt.

ModusWas er bedeutetWas möglich ist
LicensedGültige, laufende LizenzGekaufte Developer: 8 VFP-Instanzen gleichzeitig. Runtime: ohne Grenze. Eigene Tools unbegrenzt
EvaluationKeine .lic, oder der Broker hat sie nicht übernommen1 Instanz und 1 eigenes Tool. Alle 55 eingebauten, ungekürzt, und ohne Ablaufdatum
MaintenanceExpiredGültige Lizenz, aber dieser Build ist neuer als Ihre Wartung abdecktGrenzen der Evaluierung. Der Ausweg ist der vorherige Build oder eine Verlängerung
LicenseExpiredDie 30-Tage-Probe ist abgelaufenGrenzen der Evaluierung, und der Broker drückt Ihre machineId für den Kauf. Er startet weiterhin, und es ist nichts zu löschen

Die Developer-Lizenz ist an einen Entwicklungsrechner gebunden: die machineId, die die Kasse verlangt, drückt FoxAgent.Broker.exe --machine-id auf diesem Rechner. Die Probe umfasst 1 Platz und einen Monat. Die Prüfung läuft offline gegen einen öffentlichen Schlüssel im Programm, und es gibt keine Telemetrie: geschrieben wird nur ein lokales Prüfprotokoll.

Die 55 eingebauten Tools

Gruppiert nach der Stufe, die jedes Band verlangt. Die letzte Zeile arbeitet mit DBFs und Quelltexten auf der Platte und braucht keine laufende VFP-Instanz, also kann kein Connect sie eingrenzen: sie werden vom Rechner aus abgeschaltet, mit policy.json.

Wo sie wirkenStufeTools
Ihre eigenen Business-Tools und die Sitzungsverwaltung Stufe 0 execute_tool, describe_udt, get_tools, list_instances, get_role, set_role
Der Bildschirm und seine Bedienung (operator) Stufe 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
Der Prozess hinter dem Bildschirm (inspector) Stufe 20 get_variables, variable_exists, get_cursors, get_cursor_data, get_cursor_schema, get_datasessions, get_environment, eval_expression, list_prg_files
Außerhalb der Anwendung (engineer) Stufe 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
Ohne Instanz Immer 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 steht als Lesevorgang im Band der Stufe 20; ein Ausdruck, der kein reiner Lesevorgang ist, verlangt bei diesem Aufruf 90. Ihre UDTs werden neben den eingebauten aufgelistet und laufen ab Stufe 0.