Application Development Blog Posts
Learn and share on deeper, cross technology development topics such as integration and connectivity, automation, cloud extensibility, developing at scale, and security.
cancel
Showing results for 
Search instead for 
Did you mean: 
joachim_rese
Product and Topic Expert
Product and Topic Expert

Introduction

IBM watsonx is an all-in-one platform to train, validate, tune and deploy models for generative AI. It comes with a compilation of deployed generative AI models that can be used out-of-the-box. The ABAP SDK for IBM watsonx is a set of ABAP classes and data types that allows generative AI inferencing by pure ABAP means.

It is assumed that you have some basic knowledge in generative AI, in particular prompt engineering. Otherwise get familiar with the most important terms, for example by reading this blog.

Follow the steps in this blog to implement a generative AI test data generator in ABAP. The program will

  1. generate a prompt based on data available in the system
  2. execute that prompt on a model hosted on watsonx.ai
  3. process the generative AI response

Throughout this blog all parts that are related to watsonx are explained in detail. Other parts are only mentioned and not elaborated. However, at the end of this post you find the full code of a working demo program. Use this as a reference.

 

Create a watsonx Project

If you do not already have an IBM watsonx or an IBM Cloud account, "Start your free trail" on https://www.ibm.com/watsonx and proceed through the onboarding process. 

After login to watsonx, you get to the home screen. See the right upper corner for the region where your watsonx instance resides. You need this information later.

Create a new project by clicking the plus sign next to the "Projects" header line.

watsonx_home.png

Enter a name and a description for your project and click "Create".

Inside the watsonx project, click tab "Manage" and copy and save the "Project ID". You will need it later.

watsonx_projectid.png

You must associate a Watson Machine Learning service to your project. Click on "Service & integration", tab "IBM services". Click "Associate service", check service of type "Watson Machine Learning" and click "Associate"

watsonx_wml_service.png

If there is no service of type "Watson Machine Learning" that can be selected, click "New Service" and instantiate a new Waston Machine Learning service.

Next you must generate an apikey. To do so, click on the navigation menu at the left upper corner and select "Administration" → "Access (IAM)". Select "API keys" and click "Create". Enter a name and a description and click "Create". An apikey is created and shown to you. Copy and save the apikey; you will not be able to see it again.

watsonx_apikey.png

You need to identify the URL to the watsonx API. It complies to scheme
https://<region code>.ml.cloud.ibm.com
where <region code> is the region code of the region where your watsonx instance resides (see above). For exmaple eu-de for Europe (Frankfurt) or us-south for Dallas.

Finally, you have the credentials that are needed to access watsonx. (Certainly, the values below are invalid and for demonstration only.)

url:        https://eu-de.ml.cloud.ibm.com
apikey:     t8a0OQpVgs6z9jZNKGt9Ghk498f0biyZvRf-E4eqw11K
project_id: e8db8f7c-36f6-40ca-804a-238c6e557c46

 

Prepare the ABAP Environment

You must import the ABAP SDK for IBM watsonx using abapGIT pull. If your runtime environment is NetWeaver 7.50 (or above) or S/4HANA on premise, follow instructions on https://github.com/IBM/abap-sdk-nwas-x. For the BTP ABAP Environment, follow  instructions on https://github.com/IBM/abap-sdk-btp-x

For this tutorial a set of tables with some contents is required. If such tables are not available or not known to you, import the ABAP Flight Reference Scenario to your environment and generate the demo data.

Select or create a database table that you want to populate with generative AI data. For example, create package ZWATSONX under structure package ZLOCAL and create table ZIBM_CUSTOMER in that package. This table can be a copy of table /DMO/CUSTOMER. Thus, the database table definition may look like below.

 

@EndUserText.label : 'Copy of table /DMO/CUSTOMER'
@AbapCatalog.enhancement.category : #NOT_EXTENSIBLE
@AbapCatalog.tableCategory : #TRANSPARENT
@AbapCatalog.deliveryClass : #A
@AbapCatalog.dataMaintenance : #RESTRICTED
define table zibm_customer {

  key client      : abap.clnt not null;
  key customer_id : /dmo/customer_id not null;
  first_name      : /dmo/first_name;
  last_name       : /dmo/last_name;
  title           : /dmo/title;
  street          : /dmo/street;
  postal_code     : /dmo/postal_code;
  city            : /dmo/city;
  country_code    : land1;
  phone_number    : /dmo/phone_number;
  email_address   : /dmo/email_address;

}

 

Prompt Generation

You must build a prompt that instructs the generative AI model to generate test data. This works best if you use few-shot prompting, which means to include some examples in the prompt. For the sake of convenience and flexibility, do not hard-code those examples. Instead, implement some ABAP code that reads a small portion of the contents and the data dictionary information of some given tables and generates examples for the prompt. A single example can be added to the prompt using this format:

 

[example]
records: n
format: <table field names and types>
data: <n table records in JSON format>
[end]

 

The complete prompt looks like this:

 

Generate data in JSON format as shown in examples below. Insert tag [end] after given number of records.
[example]
<example data generated from table #1>
[end]
[example]
<example data generated from table #2>
[end]
[example]
<example data generated from table #3>
[end]
[input]
records: n
format: <table field names and type of target table ZIBM_CUSTOMER>
data:

 

The tables that are used to generate the examples should in total include all data types that appear in the target table. They should be client-dependent if (and only if) that target table is client-dependent.

 

Calling watsonx Generative AI

After the prompt has been generated, you can call watsonx to execute the prompt and thus generate test data. First you must provide watsonx.ai credentials. You can either specify those in your ABAP code or, recommended, configure table ZIBMX_CONFIG accordingly.

 

CONSTANTS c_url        TYPE string VALUE 'https://eu-de.ml.cloud.ibm.com'.
CONSTANTS c_apikey     TYPE string VALUE 't8a0OQpVgs6z9jZNKGt9Ghk498f0biyZvRf-E4eqw11K'.
CONSTANTS c_project_id TYPE string VALUE 'e8db8f7c-36f6-40ca-804a-238c6e557c46'.

 

Next instantiate the wrapper class for watsonx.ai.

 

DATA: lo_watsonx_ai TYPE REF TO zcl_ibmx_watsonx_ai_ml_v1.
zcl_ibmx_service_ext=>get_instance(
    EXPORTING
        i_url     = c_url
        i_apikey  = c_apikey
        i_version = '2023-05-29'
    IMPORTING
        eo_instance = lo_watsonx_ai ).

 

Now you can call method text_generation. It calls watsonx and returns the generative AI inference result.

As parameters, you must specify the prompt that has been generated before, the model id and a set of model parameters like decoding method and the maximal number of generated tokens. watsonx supports several generative AI models, for example IBM's Granite models.

Also, you must provide the id of the watsonx project that you have created before.

 

TRY.
    lo_watsonx_ai->text_generation(
      EXPORTING
        i_textgenrequest = VALUE zcl_ibmx_watsonx_ai_ml_v1=>t_text_gen_request(

          " prompt
          input = <prompt>

          " model parameters
          model_id   = 'ibm/granite-13b-chat-v2'
          parameters = VALUE zcl_ibmx_watsonx_ai_ml_v1=>t_text_gen_parameters(
            decoding_method    = 'greedy'
            max_new_tokens     = 2000
            repetition_penalty = '1.05'
            stop_sequences = VALUE #( ( `[end]` ) )  " stop at [end] tag
          )

          " watsonx project id
          project_id = c_project_id
        )
      IMPORTING
        e_response = DATA(ls_generated_document) ).
  CATCH zcx_ibmx_service_exception INTO DATA(lo_service_exception).
    " handle exception
ENDTRY.

 

The inference result is returned in an internal table that contains a single record which holds the generated data in JSON format as indicated in the prompt. Insert the data into the target database table using a JSON parser.

 

DATA lr_data TYPE REF TO data.
CREATE DATA lr_data TYPE TABLE OF ZIBM_CUSTOMER.
/ui2/cl_json=>deserialize( 
  EXPORTING 
    json = ls_generated_document-results[ 1 ]-generated_text
  CHANGING 
    data = lr_data->*  ).
INSERT ZIBM_CUSTOMER FROM TABLE @LR_data->*.

 

Run the Application

Now you are ready to run your ABAP code. After the program has finished, check the data in your table.

 

CLIENT    CUSTOMER_ID    FIRST_NAME    LAST_NAME    TITLE    STREET             POSTAL_CODE    CITY          COUNTRY_CODE    PHONE_NUMBER        EMAIL_ADDRESS        
100       001001         John          Doe          Mr.      34 South Road      12345          Anytown       US              +1 212-555-1212     johndoe@email.com    
100       001002         Jane          Smith        Ms.      43 East Street     67890          Emeryville    US              +1 415-555-1234     janesmith@email.com  
100       001003         Jim           Johnson      Dr.      56 North Avenue    55111          Seattle       US              +1 206-555-5678     jimjohnson@email.com 
100       001004         Anna          Miller       Mrs.     71 West Lane       33333          London        GB              +44 20-7654-5309    annamiller@email.com 
100       001005         Bob           Williams     Mr.      89 Maple Street    12345          Toronto       CA              +1 416-555-1111     bobwilliams@email.com

 

Conclusion

You learnt how to generate a prompt, execute that prompt on a generative AI model deployed on watsonx and process the response. All this is done with ABAP code only while the complexity of the API calls is hidden by the ABAP SDK for IBM watsonx.

The same technique can be used to include generative AI in any business process that is implemented in ABAP.

 

Addendum

Here is the full code of a demo program. It was implemented on BTP ABAP Environment and runs as a console application. To run it on a NetWeaver system or on S/4HANA on premise, remove or replace all references to interface if_oo_adt_classrun, including the out->write statements. In all cases you have to adjust watsonx credentials before running the application.

 

CLASS zwatsonx_genai_data_generator DEFINITION
  PUBLIC
  FINAL
  CREATE PUBLIC .

  PUBLIC SECTION.
    INTERFACES if_oo_adt_classrun .
  PROTECTED SECTION.
  PRIVATE SECTION.
    METHODS field_data
      IMPORTING
        i_tabname     TYPE string
      RETURNING
        VALUE(e_text) TYPE string.
    METHODS content_data
      IMPORTING
        i_tabname     TYPE string
        i_count       TYPE i
      RETURNING
        VALUE(e_text) TYPE string.
    METHODS prompt_section
      IMPORTING
        i_kind        TYPE string
        i_tabname     TYPE string
        i_count       TYPE i
      RETURNING
        VALUE(e_text) TYPE string.
    METHODS insert_json
      IMPORTING
        i_json         TYPE string
        i_tabname      TYPE string
      RAISING
        cx_sy_open_sql_db.
ENDCLASS.



CLASS zwatsonx_genai_data_generator IMPLEMENTATION.

  METHOD if_oo_adt_classrun~main.

    CONSTANTS c_url        TYPE string VALUE 'https://eu-de.ml.cloud.ibm.com'.                " <-- ADJUST
    CONSTANTS c_apikey     TYPE string VALUE 't8a0OQpVgs6z9jZNKGt9Ghk498f0biyZvRf-E4eqw11K'.  " <-- ADJUST
    CONSTANTS c_project_id TYPE string VALUE 'e8db8f7c-36f6-40ca-804a-238c6e557c46'.          " <-- ADJUST
    CONSTANTS c_tabname    TYPE string VALUE 'ZIBM_CUSTOMER'.
    CONSTANTS c_count      TYPE i VALUE 5.

    " compile prompt (featuring 3-shot prompting)
    DATA(lv_prompt) =
      " instruction
      `Generate data in JSON format as shown in examples below. Insert tag [end] after given number of records.` && cl_abap_char_utilities=>newline &&
      " examples
      prompt_section( i_kind = 'example' i_tabname = '/DMO/AGENCY' i_count = 2 ) &&
      prompt_section( i_kind = 'example' i_tabname = '/DMO/AIRPORT' i_count = 4 ) &&
      prompt_section( i_kind = 'example' i_tabname = '/DMO/FLIGHT' i_count = 3 ) &&
      " input
      prompt_section( i_kind = 'input' i_tabname = c_tabname i_count = c_count ).

    " instantiate wrapper class for watsonx.ai API
    DATA: lo_watsonx_ai TYPE REF TO zcl_ibmx_watsonx_ai_ml_v1.
    zcl_ibmx_service_ext=>get_instance(
        EXPORTING
          i_url     = c_url
          i_apikey  = c_apikey
          i_version = '2023-05-29'
        IMPORTING
          eo_instance = lo_watsonx_ai ).

    " run text generation
    TRY.
        lo_watsonx_ai->text_generation(
          EXPORTING
            i_textgenrequest = VALUE zcl_ibmx_watsonx_ai_ml_v1=>t_text_gen_request(

              " prompt
              input = lv_prompt

              " model parameters
              model_id   = 'ibm/granite-13b-chat-v2'
              parameters = VALUE zcl_ibmx_watsonx_ai_ml_v1=>t_text_gen_parameters(
                decoding_method    = 'greedy'
                max_new_tokens     = 2000
                repetition_penalty = '1.05'
                stop_sequences = VALUE #( ( `[end]` ) )  " stop at [end] tag
              )

              " watsonx project id
              project_id = c_project_id
            )
          IMPORTING
            e_response = DATA(ls_generated_document) ).
      CATCH zcx_ibmx_service_exception INTO DATA(lo_service_exception).
        out->write( `ERROR when generating text: ` && lo_service_exception->get_longtext(  ) ). EXIT.
    ENDTRY.

    " collect generated texts (single record in ls_generated_document-results is expected)
    DATA(lv_json) = ``.
    LOOP AT ls_generated_document-results INTO DATA(ls_result).
      lv_json = lv_json && ls_result-generated_text.
    ENDLOOP.

    " if LLM has generated [end] tag, remove it
    REPLACE ALL OCCURRENCES OF '[end]' IN lv_json WITH ''.

    " insert JSON data into table
    TRY.
        insert_json( i_json = lv_json i_tabname = c_tabname ).
      CATCH cx_sy_open_sql_db INTO DATA(lo_osql_exception).
        out->write( `ERROR when inserting data: ` && lo_osql_exception->get_longtext(  ) ). EXIT.
    ENDTRY.

    " success message
    out->write( `Data in table ` && c_tabname && ` that has been generated by watsonx generative AI: ` ).
    DATA lr_data TYPE REF TO data.
    CREATE DATA lr_data TYPE TABLE OF (c_tabname).
    SELECT * FROM (c_tabname) INTO TABLE @LR_data->*.
    out->write( lr_data->* ).

  ENDMETHOD.


  METHOD field_data.
    " generates field type information
    " example: table has CHAR field of length 8, INT field and STRING field
    "   e_text = FIELDNAME1,C(8) | FIELDNAME2,I | FIELDNAME3,g |
    DATA ref_descr TYPE REF TO cl_abap_structdescr.
    DATA lt_comp   TYPE abap_compdescr_tab.

    " initialize return value
    e_text = ``.

    " get field type information
    ref_descr ?= cl_abap_typedescr=>describe_by_name( i_tabname ).
    lt_comp[] = ref_descr->components[].

    " compile fields type list
    LOOP AT lt_comp INTO DATA(ls_comp).

      " get type length info
      DATA(lv_length) = ``.
      CASE ls_comp-type_kind.
        WHEN 'C' OR 'N'.
          lv_length = condense( CONV string( ls_comp-length DIV 2 ) ).   " UTF-16 length to number of chars
        WHEN 'P'.
          lv_length = condense( CONV string( ls_comp-length ) ) && `,` && condense( CONV string( ls_comp-decimals ) ).
      ENDCASE.
      IF NOT lv_length EQ ''. lv_length = `(` && lv_length && `)`. ENDIF.

      " add field data to return value
      e_text = e_text && ls_comp-name && `,` && ls_comp-type_kind && lv_length && ` | `.
    ENDLOOP.
  ENDMETHOD.


  METHOD content_data.
    " returns first rows of a table in JSON notation
    DATA lr_data TYPE REF TO data.

    e_text = `[`.
    DATA(lv_separator) = ``.

    " read table content
    CREATE DATA lr_data TYPE TABLE OF (i_tabname).
    SELECT *
      FROM (i_tabname)
      INTO TABLE @LR_data->*
      UP TO @i_count ROWS.

    " serialize record-wise and add newlines
    LOOP AT lr_data->* ASSIGNING FIELD-SYMBOL(<ls_reocord>).
      e_text = e_text && lv_separator && /ui2/cl_json=>serialize( data = <ls_reocord> ).
      lv_separator = `,` && cl_abap_char_utilities=>newline.
    ENDLOOP.
    e_text = e_text && `]`.
  ENDMETHOD.


  METHOD prompt_section.
    " returns example table data to be added to prompt, i.e.
    " [example]
    " records: 2
    " format: PET,C(8) | AGE,I | COMMENT,g |
    " data:
    " [{"PET": "cat", "AGE": 4, "COMMENT": "my pet"},
    " {"PET": "dog", "AGE": 3, "COMMENT": "your pet"}]
    " [end]
    e_text =
      `[` && i_kind && `]` && cl_abap_char_utilities=>newline &&
      `records: ` && condense( CONV string( i_count ) ) && cl_abap_char_utilities=>newline &&
      `format: ` && field_data( i_tabname = i_tabname ) && cl_abap_char_utilities=>newline &&
      `data:` && cl_abap_char_utilities=>newline.
    IF i_kind EQ 'example'.
      e_text = e_text && content_data( i_tabname = i_tabname  i_count = i_count ) && cl_abap_char_utilities=>newline &&
        `[end]` && cl_abap_char_utilities=>newline && cl_abap_char_utilities=>newline.
    ENDIF.
  ENDMETHOD.


  METHOD insert_json.
    " inserts JSON data into database table
    DATA lr_data TYPE REF TO data.
    CREATE DATA lr_data TYPE TABLE OF (i_tabname).

    /ui2/cl_json=>deserialize( EXPORTING json = i_json CHANGING data = lr_data->*  ).

    DELETE from (i_tabname).
    INSERT (i_tabname) FROM TABLE @LR_data->*.
    COMMIT WORK.

  ENDMETHOD.

ENDCLASS.

 

Labels in this area