diff --git a/tutorials/dev-garage-aem-publish-subscribe/assets/ex0-0.png b/tutorials/dev-garage-aem-publish-subscribe/assets/ex0-0.png new file mode 100644 index 0000000..6cf6ec3 Binary files /dev/null and b/tutorials/dev-garage-aem-publish-subscribe/assets/ex0-0.png differ diff --git a/tutorials/dev-garage-aem-publish-subscribe/assets/ex1-0.png b/tutorials/dev-garage-aem-publish-subscribe/assets/ex1-0.png new file mode 100644 index 0000000..a07b954 Binary files /dev/null and b/tutorials/dev-garage-aem-publish-subscribe/assets/ex1-0.png differ diff --git a/tutorials/dev-garage-aem-publish-subscribe/assets/ex1-1.png b/tutorials/dev-garage-aem-publish-subscribe/assets/ex1-1.png new file mode 100644 index 0000000..b602718 Binary files /dev/null and b/tutorials/dev-garage-aem-publish-subscribe/assets/ex1-1.png differ diff --git a/tutorials/dev-garage-aem-publish-subscribe/assets/ex1-10.png b/tutorials/dev-garage-aem-publish-subscribe/assets/ex1-10.png new file mode 100644 index 0000000..53904a0 Binary files /dev/null and b/tutorials/dev-garage-aem-publish-subscribe/assets/ex1-10.png differ diff --git a/tutorials/dev-garage-aem-publish-subscribe/assets/ex1-11.png b/tutorials/dev-garage-aem-publish-subscribe/assets/ex1-11.png new file mode 100644 index 0000000..da4be10 Binary files /dev/null and b/tutorials/dev-garage-aem-publish-subscribe/assets/ex1-11.png differ diff --git a/tutorials/dev-garage-aem-publish-subscribe/assets/ex1-12.png b/tutorials/dev-garage-aem-publish-subscribe/assets/ex1-12.png new file mode 100644 index 0000000..f250054 Binary files /dev/null and b/tutorials/dev-garage-aem-publish-subscribe/assets/ex1-12.png differ diff --git a/tutorials/dev-garage-aem-publish-subscribe/assets/ex1-13.png b/tutorials/dev-garage-aem-publish-subscribe/assets/ex1-13.png new file mode 100644 index 0000000..32920e9 Binary files /dev/null and b/tutorials/dev-garage-aem-publish-subscribe/assets/ex1-13.png differ diff --git a/tutorials/dev-garage-aem-publish-subscribe/assets/ex1-14.png b/tutorials/dev-garage-aem-publish-subscribe/assets/ex1-14.png new file mode 100644 index 0000000..afb0e0a Binary files /dev/null and b/tutorials/dev-garage-aem-publish-subscribe/assets/ex1-14.png differ diff --git a/tutorials/dev-garage-aem-publish-subscribe/assets/ex1-15.png b/tutorials/dev-garage-aem-publish-subscribe/assets/ex1-15.png new file mode 100644 index 0000000..3a3d84f Binary files /dev/null and b/tutorials/dev-garage-aem-publish-subscribe/assets/ex1-15.png differ diff --git a/tutorials/dev-garage-aem-publish-subscribe/assets/ex1-16.png b/tutorials/dev-garage-aem-publish-subscribe/assets/ex1-16.png new file mode 100644 index 0000000..5dda840 Binary files /dev/null and b/tutorials/dev-garage-aem-publish-subscribe/assets/ex1-16.png differ diff --git a/tutorials/dev-garage-aem-publish-subscribe/assets/ex1-17.png b/tutorials/dev-garage-aem-publish-subscribe/assets/ex1-17.png new file mode 100644 index 0000000..671cac1 Binary files /dev/null and b/tutorials/dev-garage-aem-publish-subscribe/assets/ex1-17.png differ diff --git a/tutorials/dev-garage-aem-publish-subscribe/assets/ex1-18.png b/tutorials/dev-garage-aem-publish-subscribe/assets/ex1-18.png new file mode 100644 index 0000000..159a5e4 Binary files /dev/null and b/tutorials/dev-garage-aem-publish-subscribe/assets/ex1-18.png differ diff --git a/tutorials/dev-garage-aem-publish-subscribe/assets/ex1-19.png b/tutorials/dev-garage-aem-publish-subscribe/assets/ex1-19.png new file mode 100644 index 0000000..4e67194 Binary files /dev/null and b/tutorials/dev-garage-aem-publish-subscribe/assets/ex1-19.png differ diff --git a/tutorials/dev-garage-aem-publish-subscribe/assets/ex1-2.png b/tutorials/dev-garage-aem-publish-subscribe/assets/ex1-2.png new file mode 100644 index 0000000..a82ba25 Binary files /dev/null and b/tutorials/dev-garage-aem-publish-subscribe/assets/ex1-2.png differ diff --git a/tutorials/dev-garage-aem-publish-subscribe/assets/ex1-20.png b/tutorials/dev-garage-aem-publish-subscribe/assets/ex1-20.png new file mode 100644 index 0000000..0f5adf8 Binary files /dev/null and b/tutorials/dev-garage-aem-publish-subscribe/assets/ex1-20.png differ diff --git a/tutorials/dev-garage-aem-publish-subscribe/assets/ex1-21.png b/tutorials/dev-garage-aem-publish-subscribe/assets/ex1-21.png new file mode 100644 index 0000000..8fc2c7c Binary files /dev/null and b/tutorials/dev-garage-aem-publish-subscribe/assets/ex1-21.png differ diff --git a/tutorials/dev-garage-aem-publish-subscribe/assets/ex1-22.png b/tutorials/dev-garage-aem-publish-subscribe/assets/ex1-22.png new file mode 100644 index 0000000..48f927b Binary files /dev/null and b/tutorials/dev-garage-aem-publish-subscribe/assets/ex1-22.png differ diff --git a/tutorials/dev-garage-aem-publish-subscribe/assets/ex1-3.png b/tutorials/dev-garage-aem-publish-subscribe/assets/ex1-3.png new file mode 100644 index 0000000..b81b482 Binary files /dev/null and b/tutorials/dev-garage-aem-publish-subscribe/assets/ex1-3.png differ diff --git a/tutorials/dev-garage-aem-publish-subscribe/assets/ex1-4.png b/tutorials/dev-garage-aem-publish-subscribe/assets/ex1-4.png new file mode 100644 index 0000000..7827302 Binary files /dev/null and b/tutorials/dev-garage-aem-publish-subscribe/assets/ex1-4.png differ diff --git a/tutorials/dev-garage-aem-publish-subscribe/assets/ex1-5.png b/tutorials/dev-garage-aem-publish-subscribe/assets/ex1-5.png new file mode 100644 index 0000000..ec469f8 Binary files /dev/null and b/tutorials/dev-garage-aem-publish-subscribe/assets/ex1-5.png differ diff --git a/tutorials/dev-garage-aem-publish-subscribe/assets/ex1-6.png b/tutorials/dev-garage-aem-publish-subscribe/assets/ex1-6.png new file mode 100644 index 0000000..bc83a3e Binary files /dev/null and b/tutorials/dev-garage-aem-publish-subscribe/assets/ex1-6.png differ diff --git a/tutorials/dev-garage-aem-publish-subscribe/assets/ex1-7.png b/tutorials/dev-garage-aem-publish-subscribe/assets/ex1-7.png new file mode 100644 index 0000000..bd5acab Binary files /dev/null and b/tutorials/dev-garage-aem-publish-subscribe/assets/ex1-7.png differ diff --git a/tutorials/dev-garage-aem-publish-subscribe/assets/ex1-8.png b/tutorials/dev-garage-aem-publish-subscribe/assets/ex1-8.png new file mode 100644 index 0000000..5c1a712 Binary files /dev/null and b/tutorials/dev-garage-aem-publish-subscribe/assets/ex1-8.png differ diff --git a/tutorials/dev-garage-aem-publish-subscribe/assets/ex1-9.png b/tutorials/dev-garage-aem-publish-subscribe/assets/ex1-9.png new file mode 100644 index 0000000..d78b78e Binary files /dev/null and b/tutorials/dev-garage-aem-publish-subscribe/assets/ex1-9.png differ diff --git a/tutorials/dev-garage-aem-publish-subscribe/assets/ex2-0.png b/tutorials/dev-garage-aem-publish-subscribe/assets/ex2-0.png new file mode 100644 index 0000000..1e9368c Binary files /dev/null and b/tutorials/dev-garage-aem-publish-subscribe/assets/ex2-0.png differ diff --git a/tutorials/dev-garage-aem-publish-subscribe/assets/ex2-1.png b/tutorials/dev-garage-aem-publish-subscribe/assets/ex2-1.png new file mode 100644 index 0000000..5859041 Binary files /dev/null and b/tutorials/dev-garage-aem-publish-subscribe/assets/ex2-1.png differ diff --git a/tutorials/dev-garage-aem-publish-subscribe/assets/ex2-10.png b/tutorials/dev-garage-aem-publish-subscribe/assets/ex2-10.png new file mode 100644 index 0000000..bbcf40c Binary files /dev/null and b/tutorials/dev-garage-aem-publish-subscribe/assets/ex2-10.png differ diff --git a/tutorials/dev-garage-aem-publish-subscribe/assets/ex2-2.png b/tutorials/dev-garage-aem-publish-subscribe/assets/ex2-2.png new file mode 100644 index 0000000..ae8997a Binary files /dev/null and b/tutorials/dev-garage-aem-publish-subscribe/assets/ex2-2.png differ diff --git a/tutorials/dev-garage-aem-publish-subscribe/assets/ex2-3.png b/tutorials/dev-garage-aem-publish-subscribe/assets/ex2-3.png new file mode 100644 index 0000000..569b21c Binary files /dev/null and b/tutorials/dev-garage-aem-publish-subscribe/assets/ex2-3.png differ diff --git a/tutorials/dev-garage-aem-publish-subscribe/assets/ex2-4.png b/tutorials/dev-garage-aem-publish-subscribe/assets/ex2-4.png new file mode 100644 index 0000000..cd61048 Binary files /dev/null and b/tutorials/dev-garage-aem-publish-subscribe/assets/ex2-4.png differ diff --git a/tutorials/dev-garage-aem-publish-subscribe/assets/ex2-5.png b/tutorials/dev-garage-aem-publish-subscribe/assets/ex2-5.png new file mode 100644 index 0000000..343631a Binary files /dev/null and b/tutorials/dev-garage-aem-publish-subscribe/assets/ex2-5.png differ diff --git a/tutorials/dev-garage-aem-publish-subscribe/assets/ex2-6.png b/tutorials/dev-garage-aem-publish-subscribe/assets/ex2-6.png new file mode 100644 index 0000000..e1e9817 Binary files /dev/null and b/tutorials/dev-garage-aem-publish-subscribe/assets/ex2-6.png differ diff --git a/tutorials/dev-garage-aem-publish-subscribe/assets/ex2-7.png b/tutorials/dev-garage-aem-publish-subscribe/assets/ex2-7.png new file mode 100644 index 0000000..ccdb04e Binary files /dev/null and b/tutorials/dev-garage-aem-publish-subscribe/assets/ex2-7.png differ diff --git a/tutorials/dev-garage-aem-publish-subscribe/assets/ex2-8.png b/tutorials/dev-garage-aem-publish-subscribe/assets/ex2-8.png new file mode 100644 index 0000000..0d30517 Binary files /dev/null and b/tutorials/dev-garage-aem-publish-subscribe/assets/ex2-8.png differ diff --git a/tutorials/dev-garage-aem-publish-subscribe/assets/ex2-9.png b/tutorials/dev-garage-aem-publish-subscribe/assets/ex2-9.png new file mode 100644 index 0000000..310e14a Binary files /dev/null and b/tutorials/dev-garage-aem-publish-subscribe/assets/ex2-9.png differ diff --git a/tutorials/dev-garage-aem-publish-subscribe/assets/ex3-0.png b/tutorials/dev-garage-aem-publish-subscribe/assets/ex3-0.png new file mode 100644 index 0000000..1028aea Binary files /dev/null and b/tutorials/dev-garage-aem-publish-subscribe/assets/ex3-0.png differ diff --git a/tutorials/dev-garage-aem-publish-subscribe/assets/ex3-1.png b/tutorials/dev-garage-aem-publish-subscribe/assets/ex3-1.png new file mode 100644 index 0000000..a83cd1e Binary files /dev/null and b/tutorials/dev-garage-aem-publish-subscribe/assets/ex3-1.png differ diff --git a/tutorials/dev-garage-aem-publish-subscribe/assets/ex3-2.png b/tutorials/dev-garage-aem-publish-subscribe/assets/ex3-2.png new file mode 100644 index 0000000..d0c8555 Binary files /dev/null and b/tutorials/dev-garage-aem-publish-subscribe/assets/ex3-2.png differ diff --git a/tutorials/dev-garage-aem-publish-subscribe/assets/ex3-3.png b/tutorials/dev-garage-aem-publish-subscribe/assets/ex3-3.png new file mode 100644 index 0000000..513f6d3 Binary files /dev/null and b/tutorials/dev-garage-aem-publish-subscribe/assets/ex3-3.png differ diff --git a/tutorials/dev-garage-aem-publish-subscribe/assets/ex3-4.png b/tutorials/dev-garage-aem-publish-subscribe/assets/ex3-4.png new file mode 100644 index 0000000..3602e39 Binary files /dev/null and b/tutorials/dev-garage-aem-publish-subscribe/assets/ex3-4.png differ diff --git a/tutorials/dev-garage-aem-publish-subscribe/assets/ex3-5.png b/tutorials/dev-garage-aem-publish-subscribe/assets/ex3-5.png new file mode 100644 index 0000000..857f4f4 Binary files /dev/null and b/tutorials/dev-garage-aem-publish-subscribe/assets/ex3-5.png differ diff --git a/tutorials/dev-garage-aem-publish-subscribe/assets/ex3-6.png b/tutorials/dev-garage-aem-publish-subscribe/assets/ex3-6.png new file mode 100644 index 0000000..fc1a864 Binary files /dev/null and b/tutorials/dev-garage-aem-publish-subscribe/assets/ex3-6.png differ diff --git a/tutorials/dev-garage-aem-publish-subscribe/assets/ex3-7.png b/tutorials/dev-garage-aem-publish-subscribe/assets/ex3-7.png new file mode 100644 index 0000000..93f2910 Binary files /dev/null and b/tutorials/dev-garage-aem-publish-subscribe/assets/ex3-7.png differ diff --git a/tutorials/dev-garage-aem-publish-subscribe/assets/ex4-0.png b/tutorials/dev-garage-aem-publish-subscribe/assets/ex4-0.png new file mode 100644 index 0000000..fc0a992 Binary files /dev/null and b/tutorials/dev-garage-aem-publish-subscribe/assets/ex4-0.png differ diff --git a/tutorials/dev-garage-aem-publish-subscribe/assets/ex4-1.png b/tutorials/dev-garage-aem-publish-subscribe/assets/ex4-1.png new file mode 100644 index 0000000..b8dd5ff Binary files /dev/null and b/tutorials/dev-garage-aem-publish-subscribe/assets/ex4-1.png differ diff --git a/tutorials/dev-garage-aem-publish-subscribe/dev-garage-aem-publish-subscribe.md b/tutorials/dev-garage-aem-publish-subscribe/dev-garage-aem-publish-subscribe.md new file mode 100644 index 0000000..23b3620 --- /dev/null +++ b/tutorials/dev-garage-aem-publish-subscribe/dev-garage-aem-publish-subscribe.md @@ -0,0 +1,420 @@ +--- +parser: v2 +author_name: t-griebe +author_profile: https://github.com/t-griebe +keywords: tutorial +auto_validation: true +time: 30 +tags: [ tutorial>beginner, software-product>sap-integration-suite, software-product>sap-business-technology-platform ] +primary_tag: software-product>sap-integration-suite +--- + +# SAP TechEd Developer Garage: Publish and Subscribe to Events in SAP Integration Suite, Advanced Event Mesh + + Get started with your first message exchange using the Try Me! UI in SAP Integration Suite, Advanced Event Mesh (AEM). Explore topics, queues, topic hierarchies, wildcards, and the difference between direct and persistent message delivery modes. + +## Overview + +SAP Integration Suite, advanced event mesh is SAP's flagship event broker — a fully managed event streaming and management service that enables enterprise-wide, enterprise-grade event-driven architecture. + +In this tutorial you will explore AEM hands-on using its built-in **Try Me!** tool. You will create a queue, subscribe it to a topic, publish messages, experiment with topic hierarchies and wildcards, and experience the difference between direct (non-persistent) and guaranteed (persistent) message delivery. + +## Prerequisites + +- Access to an SAP Integration Suite, Advanced Event Mesh tenant + +## You will learn + +- How to access AEM and take your first steps +- What topics are and how they work in AEM +- How to send and receive messages via the **Try Me!** page in the event broker service +- The difference between direct and persistent message delivery modes + +--- + +### Introduction — What is SAP Integration Suite, Advanced Event Mesh + +![Advanced Event Mesh overview](assets/ex0-0.png) + +SAP Integration Suite, advanced event mesh is a fully managed event streaming and management service that enables enterprise-wide and enterprise-grade event-driven architecture. It is a full-blown, general-purpose event mesh offering enterprise-grade performance, reliability, security, and governance. It scales to very large use cases. + +Use the link and credentials below to log in: + +| Resource | Value | +| ---------- | ------- | +| AEM console | https://url.sap/y3fiys | +| Login user | **DevGarageAEM** | +| Password | Provided by your session moderator | + +Key characteristics: + +- Distributed mesh of event brokers deployable across cloud and on-premise environments +- Full-purpose set of eventing services covering all relevant use cases +- Supports event streaming, event management, and event monitoring +- Brokers scale as required and come in different sizes to fit different needs + +SAP Integration Suite, advanced event mesh: + +- Is a general-purpose, multi-site EDA platform and portal +- Allows SAP-to-SAP, SAP-to-everything, and everything-to-everything scenarios +- Supports distributed networks of event brokers deployed in different clouds and on-premises +- Can be deployed in your cloud of choice or in your on-premises Kubernetes environment +- Provides sophisticated authentication and security (Kerberos, OAuth, TLS) +- Offers fine-grained filtering options +- Enables real-time monitoring, capacity insights, and distributed tracing +- Supports all relevant protocols: JMS, REST, AMQP, MQTT, and SMF +- Scales from small to billions of events per day + +Features you will touch in this tutorial include: + +- AEM queues +- AEM Cluster Manager +- AEM Mesh Manager +- AEM Event Designer and Event Management +- AEM **Try Me!** tool +- AEM Event Portal + +--- + +### Log into Advanced Event Mesh and explore the cockpit + +![AEM cockpit overview](assets/ex1-0.png) + +1. Log into Advanced Event Mesh using the credentials from the Prerequisites section. + + ![AEM login screen](assets/ex1-1.png) + +2. Explore the **Advanced Event Mesh cockpit**. The different areas represent the main categories of services AEM offers: + + - **Mission Control** — Deploy event brokers, create event meshes, and monitor health and performance. + - **Cluster Manager** — Event broker services are available here. Each service consists of event brokers configured in a High-Availability (HA) setup. + - **Mesh Manager** — Connect multiple event broker services across data centers to create an event mesh. An event mesh is an architectural layer that allows events from one application to be dynamically routed to any other application regardless of where they are deployed. + - **Event Portal** — Event management services providing tools to create, design, share, and manage aspects of an EDA based on event brokers or other streaming technologies such as Kafka. + - **Event Monitoring and Insights** — Curated dashboards, visualizations based on historical and real-time metrics, and notifications about your event broker services. + +3. Click on **Cluster Manager**. + + ![Cluster Manager](assets/ex1-2.png) + +4. Select the **Developer Garage** broker by clicking on it. + +5. Click **Open Broker Manager**. + + > The Broker Manager opens in a new browser tab. Keep both tabs open — you will need both. + + ![Open Broker Manager](assets/ex1-3.png) + +6. Explore the **Broker Manager**. + + ![Broker Manager overview](assets/ex1-4.png) + + On the left side you will find the main navigation sections: + + - **Message VPN** — VPN-level stats and configuration. One AEM broker can host multiple Message VPNs, each with different authorization schemes and topic spaces. + - **Clients** — Information about connected and configured client applications. + - **Queues** — Used for guaranteed/persistent messaging. + - **Connectors** — Wizards to connect to a variety of web services. + - **Access Control** — Create client usernames, ACL profiles, and client profiles. + - **Replay** — Enable replay to allow the broker to re-send previous messages. + - **Try Me!** — Connect two WebSocket test applications to publish and subscribe to messages. + +### Create a queue in Advanced Event Mesh + +After completing this step you will have created a queue in Advanced Event Mesh. + +1. Go back to the original browser tab and click on **Cluster Manager** on the left. + +2. In the **All Services** screen, click on the **Developer Garage** tile. + +![Cluster Manager](assets/ex1-2.png) + +3. Press **Manage**. + +![Manage broker](assets/ex1-5.png) + +4. Under **Broker Manager Quick Settings**, click the **Queues** link. A new window opens. + +![Quick Settings Queues link](assets/ex1-6.png) + +5. Click the **+ Queue** button in the top right. + +![Add Queue button](assets/ex1-7.png) + +6. In the pop-up, enter a **queue name** — use something unique like your initials or a nickname. + +7. Click **Create**. + +![Create queue dialog](assets/ex1-8.png) + +8. On the next screen, click **Apply**. + +![Apply queue settings](assets/ex1-9.png) + +9. Verify that your queue appears in the list. + +![Queue list](assets/ex1-10.png) + +### Create a queue subscription in Advanced Event Mesh + +After completing this step you will have subscribed your queue to a topic. + +1. Click on your queue. + +2. Click on **Subscriptions**. + +3. Click the **+ Subscription** button. + +![Add subscription](assets/ex1-11.png) + +4. Enter a topic into the field. Use something unique like your initials or a nickname as ``. + +![Enter topic subscription](assets/ex1-12.png) + +> **Note:** This topic is intentionally simple to facilitate the exercise. In real-world scenarios you can work with topic hierarchies that allow for filtering, and with dynamic topics based on event data. All UTF-8 characters are generally allowed in topic names. Topic strings are strictly case-sensitive (`a/b/c` differs from `A/B/C`). Topics can have up to 128 levels and a maximum length of 250 bytes. + +5. Click **Create**. + +6. Verify that your queue subscription was created. + +![Subscription created](assets/ex1-13.png) + +### Send a message using the Try Me! tool + +After completing this step you will have sent your first message to a topic and confirmed it was delivered to your queue. + +1. Switch to the **Try Me!** section in the left-hand menu. + +2. Press the **expand** icon to show the connection details. + +> The connection properties must be adjusted before you can connect — you will fill them in from the connection details screen. + +![Try Me! expand](assets/ex1-14.png) + +3. Keep this tab open and **go back to the other/main tab**. Click **Connect** and select **View by "Protocol"**. + +![Connect Protocol view](assets/ex1-15.png) + +4. Open the **Solace Web Messaging** entry and select **Solace JavaScript API**. A pop-up on the right shows the connection details. + +5. Copy the **Password** and **Host URI** into the corresponding properties in the **Try Me!** tool. Overwrite the existing placeholder values. + +![Connection details pop-up](assets/ex1-16.png) + +6. Click **Connect** in the **Publisher** section. + +> **Important:** If your browser asks you to select a certificate for authentication, press **Cancel** — selecting a certificate will cause the connection to fail. If you accidentally selected a certificate, restart your browser to see the dialog again. + +![Publisher Connect](assets/ex1-17.png) + +7. Enter your unique topic (the same one you used in step 4) into the topic field. + +![Enter publish topic](assets/ex1-18.png) + +8. Click **Publish** to send your message (default payload: *Hello World!*) to your topic. + +9. Verify that **1 message published** appears in the **Publish Status**. + +![Publish status](assets/ex1-19.png) + +10. Switch back to the **Queues** view. + +![Queues view](assets/ex1-20.png) + +11. Find the queue you created earlier. You should see **1 message queued**. + +![Message in queue](assets/ex1-21.png) + +You have published a message to a topic, and via the subscription that message has been stored in your queue. + +12. Optional: Return to the **Try Me!** tool and click **Publish** several more times. Switch back to the **Queues** tab and refresh — observe the message count increasing. Clicking on your queue shows a more detailed summary. + +![Queue message count](assets/ex1-22.png) + +### Understand topic hierarchies and wildcards + +After completing this step you will have learned about **topic hierarchies and wildcards** and practised using them. + +![Topic hierarchies](assets/ex2-0.png) + +#### What are topic hierarchies? + +Advanced Event Mesh supports a hierarchical topic structure, which means you can be very descriptive when defining your topic. Use it to describe the **contents and intent of your message payload data**. + +Each message can be published to a unique topic depending on the event metadata. Examples of valid AEM topics: + +- `hello/world/aem` +- `acme/taxi/rider/hail` +- `city/train/1234/alert/stopped` +- `mfg/plant42/inv/update/p12345667` +- `payme/debit/qr/bank01/f89a09-2b9c065a3/ok` + +Topics for events originating from SAP S/4HANA follow a standard structure: + +- `ce/sap/s4/beh/salesorder/v1/SalesOrder/Changed/v1` +- `ce/sap/s4/beh/businesspartner/v1/BusinessPartner/Created/v1` +- `ce/sap/s4/beh/workcenter/v1/WorkCenter/Created/v1` + +#### Subscription wildcards + +Because published topics can be so variable and dynamic, subscribers can use wildcards to match a single subscription to multiple published topics. AEM supports two wildcard types: + +- **Single-level wildcard** (`*`) — matches 0 or more characters up to the next level `/`. Can be used with a prefix (e.g. `abc*`) but not a suffix. +- **Multi-level wildcard** (`>`) — matches one or more levels. Must appear at the end of the topic subscription. + +Examples of subscriptions and the topics they match: + +![Wildcard examples](assets/ex2-1.png) + +#### Practise topic hierarchies with the Try Me! tool — Berlin edition + +Now that you understand topic hierarchies, use the **Try Me!** tool to experiment with them. You will use Berlin-themed topics. + +![Berlin topics overview](assets/ex2-2.png) + +1. Go to the **Try Me!** tool used in the previous step. + +2. Connect both the **Publisher** and **Subscriber** by clicking their **Connect** buttons. You may still be connected from the previous step. + +3. Clear any existing subscriptions in the Subscriber so you start fresh. + +![Clear subscriptions](assets/ex2-3.png) + +4. Review the hierarchical categorisation of Berlin-related topics below. + +![Berlin topic hierarchy](assets/ex2-4.png) + +> **Note:** If you publish to a shared topic, other participants will receive your event too — that is exactly how topics work! If you want to experiment privately, prefix your topics with your participant number (e.g. `42/city/berlin/food/modern/currywurst/ready`). Use the same prefix when subscribing. + +5. Subscribe the consumer to `city/berlin/food/modern/currywurst/*` to listen for all currywurst events. + +![Subscribe to currywurst wildcard](assets/ex2-5.png) + +6. In the **Publisher**, set the topic to `city/berlin/food/modern/currywurst/ready`, add a payload (e.g. *medium spicy currywurst for Scott*), and click **Publish**. + +![Publish currywurst event](assets/ex2-6.png) + +7. Verify that you receive the message in the **Subscriber**. + +![Message received](assets/ex2-7.png) + +8. Now experiment with sights. Remove your current subscriptions. + +![Remove subscriptions](assets/ex2-8.png) + +9. Subscribe to `city/berlin/sights/*/*/open`. + +![Subscribe to sights wildcard](assets/ex2-9.png) + +10. Publish an event with topic `city/berlin/sights/monuments/reichstag/open`. + +![Publish reichstag event](assets/ex2-10.png) + +11. Try additional combinations on your own using the topic list below: + +``` +city/berlin/sights/monuments/reichstag/open +city/berlin/sights/monuments/reichstag/closed +city/berlin/sights/monuments/wall/open +city/berlin/sights/monuments/wall/closed +city/berlin/sights/nature/wannsee/open +city/berlin/sights/nature/wannsee/closed +city/berlin/sights/nature/tiergarten/open +city/berlin/sights/nature/tiergarten/closed +city/berlin/food/traditional/buletten/ready +city/berlin/food/traditional/buletten/soldout +city/berlin/food/traditional/klopse/ready +city/berlin/food/traditional/klopse/soldout +city/berlin/food/modern/doener/ready +city/berlin/food/modern/doener/soldout +city/berlin/food/modern/currywurst/ready +city/berlin/food/modern/currywurst/soldout +``` + +### Understand persistent and non-persistent message delivery + +After completing this step you will have learned about delivery modes, persistency, and quality of service, and seen the practical difference in the **Try Me!** tool. + +![Delivery modes](assets/ex3-0.png) + +#### Delivery modes and quality of service + +Advanced Event Mesh supports two delivery modes: + +**Persistent (Guaranteed)** +- Messages are placed onto non-volatile storage after arriving at the broker. +- The broker sends an acknowledgment back to the producer after successfully storing the message. +- If a subscriber wants guaranteed delivery, they must set up a queue (or topic endpoint) and add their subscription there — this is called **topic-to-queue mapping**. The broker stores all matching messages and delivers them losslessly. The consumer acknowledges each message, after which the broker removes it from the queue. +- Also known as: *Guaranteed Messaging*, *QoS1/QoS2*, *at-least-once delivery*. + +**Non-Persistent (Direct)** +- Messages are only stored if subscribed by a queue or durable topic endpoint. +- Publisher and subscriber must both be connected at the same time for delivery to succeed — messages published while the subscriber is offline are lost. +- Fastest mode, but not lossless. +- Also known as: *Direct Messaging*, *QoS0*, *at-most-once delivery*. + +#### Experiment with persistency + +1. Remove all subscriptions from your Subscriber and clear the messages. + +![Clear subscriber](assets/ex3-1.png) + +2. In the Publisher, enter your unique topic from the earlier exercises and publish **one message** using direct (non-persistent) mode. + +![Publish direct message](assets/ex3-2.png) + +> Did you receive anything? No — your Subscriber is not yet subscribed to the topic. + +3. Subscribe the Subscriber to your unique topic and click **Subscribe**. + +![Subscribe to topic](assets/ex3-3.png) + +> Did you receive the message now? Still no. Your subscriber is using direct/non-persistent mode. There is no persistent endpoint, so messages published while the subscriber was not listening are gone. + +4. Click **Publish** one more time. + +![Receive direct message](assets/ex3-4.png) + +> Now you receive a message — because publisher and subscriber are both connected at the same time. This is **non-persistent messaging**: publishers and subscribers must be connected simultaneously. It is the fastest mode but offers no delivery guarantee. + +5. Publish a few more messages, then clear the Subscriber's messages and remove the topic subscription. + +![Clear and unsubscribe](assets/ex3-5.png) + +6. Click **Bind to an endpoint to receive guaranteed messages**, then enter the unique queue name you created in the **Create a queue** step. + +![Bind to endpoint](assets/ex3-6.png) + +7. Click **Start Consume**. + +![Start consume from queue](assets/ex3-7.png) + +> You should now receive multiple messages at once. How? Your queue has been subscribed to your topic the entire time. Every message you published to that topic was attracted to the queue by its subscription and stored there. When you started consuming from the queue, the broker delivered all stored messages immediately. +> +> This is **persistent messaging**: publishers and consumers do not need to be online at the same time or process messages at the same speed. The broker securely stores all messages matching the queue's subscription and delivers them when the consumer is ready. + +### Clean up + +![Clean up](assets/ex4-0.png) + +Please delete the artifacts you created during this tutorial to keep the shared environment clean for all participants. + +1. Click on **Queues**. +2. Search for your queue. +3. Select your queue from the list. +4. Click the **Action** button. +5. Select **Delete** to delete your queue and all currently stored messages. +6. Repeat for any other artifacts you may have created. + +![Delete queue](assets/ex4-1.png) + +## Summary + +In this tutorial you have: + +1. Explored the SAP Integration Suite, Advanced Event Mesh cockpit, Cluster Manager, and Broker Manager. +2. Created a queue and subscribed it to a topic. +3. Published messages using the **Try Me!** tool and confirmed delivery to your queue. +4. Learned how topic hierarchies and wildcards enable flexible, descriptive event routing. +5. Experienced the practical difference between direct (non-persistent) and guaranteed (persistent) message delivery. + +To dive deeper, join the TechEd 2026 hands-on session: [IN163: Build your own event-driven scenario with advanced event mesh](https://www.sap.com/events/teched/berlin/flow/sap/te26/catalog/page/catalog/session/1786029069284001qBM4). diff --git a/tutorials/dev-garage-aws-pipeline-revenue-management/assets/aws-s3-step3-read-sales-connection.png b/tutorials/dev-garage-aws-pipeline-revenue-management/assets/aws-s3-step3-read-sales-connection.png new file mode 100644 index 0000000..472dd3f Binary files /dev/null and b/tutorials/dev-garage-aws-pipeline-revenue-management/assets/aws-s3-step3-read-sales-connection.png differ diff --git a/tutorials/dev-garage-aws-pipeline-revenue-management/assets/aws-s3-step3-read-sales-processing.png b/tutorials/dev-garage-aws-pipeline-revenue-management/assets/aws-s3-step3-read-sales-processing.png new file mode 100644 index 0000000..a36c4cc Binary files /dev/null and b/tutorials/dev-garage-aws-pipeline-revenue-management/assets/aws-s3-step3-read-sales-processing.png differ diff --git a/tutorials/dev-garage-aws-pipeline-revenue-management/assets/aws-s3-step3-sender-connection.png b/tutorials/dev-garage-aws-pipeline-revenue-management/assets/aws-s3-step3-sender-connection.png deleted file mode 100644 index c554813..0000000 Binary files a/tutorials/dev-garage-aws-pipeline-revenue-management/assets/aws-s3-step3-sender-connection.png and /dev/null differ diff --git a/tutorials/dev-garage-aws-pipeline-revenue-management/assets/aws-s3-step3-sender-processing.png b/tutorials/dev-garage-aws-pipeline-revenue-management/assets/aws-s3-step3-sender-processing.png deleted file mode 100644 index d0b6381..0000000 Binary files a/tutorials/dev-garage-aws-pipeline-revenue-management/assets/aws-s3-step3-sender-processing.png and /dev/null differ diff --git a/tutorials/dev-garage-aws-pipeline-revenue-management/assets/aws-s3-step3-timer-start.png b/tutorials/dev-garage-aws-pipeline-revenue-management/assets/aws-s3-step3-timer-start.png new file mode 100644 index 0000000..df8b292 Binary files /dev/null and b/tutorials/dev-garage-aws-pipeline-revenue-management/assets/aws-s3-step3-timer-start.png differ diff --git a/tutorials/dev-garage-aws-pipeline-revenue-management/assets/aws-s3-step4-csv-to-xml.png b/tutorials/dev-garage-aws-pipeline-revenue-management/assets/aws-s3-step4-csv-to-xml.png index ed25d86..dd94343 100644 Binary files a/tutorials/dev-garage-aws-pipeline-revenue-management/assets/aws-s3-step4-csv-to-xml.png and b/tutorials/dev-garage-aws-pipeline-revenue-management/assets/aws-s3-step4-csv-to-xml.png differ diff --git a/tutorials/dev-garage-aws-pipeline-revenue-management/assets/aws-s3-step4-xml-to-json.png b/tutorials/dev-garage-aws-pipeline-revenue-management/assets/aws-s3-step4-xml-to-json.png index 2a5e57b..8a3bbd7 100644 Binary files a/tutorials/dev-garage-aws-pipeline-revenue-management/assets/aws-s3-step4-xml-to-json.png and b/tutorials/dev-garage-aws-pipeline-revenue-management/assets/aws-s3-step4-xml-to-json.png differ diff --git a/tutorials/dev-garage-aws-pipeline-revenue-management/assets/aws-s3-step5-prepare-for-lookup.png b/tutorials/dev-garage-aws-pipeline-revenue-management/assets/aws-s3-step5-prepare-for-lookup.png index 7038091..463288a 100644 Binary files a/tutorials/dev-garage-aws-pipeline-revenue-management/assets/aws-s3-step5-prepare-for-lookup.png and b/tutorials/dev-garage-aws-pipeline-revenue-management/assets/aws-s3-step5-prepare-for-lookup.png differ diff --git a/tutorials/dev-garage-aws-pipeline-revenue-management/assets/aws-s3-step6-cost-lookup.png b/tutorials/dev-garage-aws-pipeline-revenue-management/assets/aws-s3-step6-cost-lookup.png index bc7ca6e..24d7acc 100644 Binary files a/tutorials/dev-garage-aws-pipeline-revenue-management/assets/aws-s3-step6-cost-lookup.png and b/tutorials/dev-garage-aws-pipeline-revenue-management/assets/aws-s3-step6-cost-lookup.png differ diff --git a/tutorials/dev-garage-aws-pipeline-revenue-management/assets/aws-s3-step8-finished-iflow.png b/tutorials/dev-garage-aws-pipeline-revenue-management/assets/aws-s3-step8-finished-iflow.png index 53128b5..71cdc46 100644 Binary files a/tutorials/dev-garage-aws-pipeline-revenue-management/assets/aws-s3-step8-finished-iflow.png and b/tutorials/dev-garage-aws-pipeline-revenue-management/assets/aws-s3-step8-finished-iflow.png differ diff --git a/tutorials/dev-garage-aws-pipeline-revenue-management/assets/aws-s3-step8-write-connection.png b/tutorials/dev-garage-aws-pipeline-revenue-management/assets/aws-s3-step8-write-connection.png new file mode 100644 index 0000000..df08b81 Binary files /dev/null and b/tutorials/dev-garage-aws-pipeline-revenue-management/assets/aws-s3-step8-write-connection.png differ diff --git a/tutorials/dev-garage-aws-pipeline-revenue-management/assets/aws-s3-step8-write-curated.png b/tutorials/dev-garage-aws-pipeline-revenue-management/assets/aws-s3-step8-write-curated.png index 81bbb86..6b8fe2b 100644 Binary files a/tutorials/dev-garage-aws-pipeline-revenue-management/assets/aws-s3-step8-write-curated.png and b/tutorials/dev-garage-aws-pipeline-revenue-management/assets/aws-s3-step8-write-curated.png differ diff --git a/tutorials/dev-garage-aws-pipeline-revenue-management/dev-garage-aws-pipeline-revenue-management.md b/tutorials/dev-garage-aws-pipeline-revenue-management/dev-garage-aws-pipeline-revenue-management.md index dadee5a..b4ba6c5 100644 --- a/tutorials/dev-garage-aws-pipeline-revenue-management/dev-garage-aws-pipeline-revenue-management.md +++ b/tutorials/dev-garage-aws-pipeline-revenue-management/dev-garage-aws-pipeline-revenue-management.md @@ -11,25 +11,32 @@ primary_tag: software-product>sap-integration-suite # SAP TechEd Developer Garage: AWS Pipeline for Revenue Management - Build an integration flow in SAP Integration Suite that polls a sales file from Amazon S3, looks up unit costs in a reference file in S3, calculates profit margin, and writes the enriched data to a second S3 bucket in another region. - -## Prerequisites - -- An SAP BTP account or trial account with **SAP Integration Suite** and the **Cloud Integration** capability activated -- [Assign roles to your user for Cloud Integration](https://help.sap.com/docs/integration-suite/sap-integration-suite/configuring-user-access?locale=en-US&state=PRODUCTION&version=CLOUD#loio2c6214a3228e4b4cba207f49fda92ed4__section_xbf_glz_l2c), including the `Integration_Developer` role collection -- The file `From the Instructor.txt`, which your instructor shares with you through a secure medium. It is not part of this repository. It contains the **AWS Access Key ID** and **Secret Access Key** for the S3 buckets, plus the bucket, region, and folder names -- Familiarity with the SAP Integration Suite interface helps. No Groovy experience is required; the script is provided. - -You don't need an AWS account. Your instructor prepares the S3 buckets, the reference file, and the access keys, and places one sales file in the shared `daily/` folder. The file stays there, so your flow reprocesses it on every poll. + Build an integration flow in SAP Integration Suite that reads a sales file from Amazon S3, looks up unit costs in a reference file in S3, calculates profit margin, and writes the enriched data to a second S3 bucket in another region. ## You will learn - How to build an integration flow from scratch in SAP Integration Suite -- How to store AWS credentials as Security Material -- How the AmazonWebServices adapter polls, reads, and writes files in Amazon S3 +- How the AWS adapters use credentials that have been provisioned for you +- How the AmazonWebServices adapter reads and writes files in Amazon S3 - How to enrich messages with a reference-data lookup and calculate profit margin in Groovy - How to deploy an integration flow and follow a message step by step in the Monitor +## Prerequisites + +- Contact a Developer Advocate in the Developer Garage to share the necessary credentials for accessing the provisioned systems. +- Various systems have been provisioned for you to complete this tutorial. + +### System access + +As part of this tutorial, you will need to access the provisioned systems using the credentials shared with you. In the table below, you will find the necessary information to access each system. + +> There are two SAP Integration Suite environments available for use in this tutorial. Use the one corresponding to your user range. + +| System | URL | +| --- | --- | +| SAP Integration Suite - Dev Garage 1 (User01 - User99) | | +| SAP Integration Suite - Dev Garage 2 (User100 - User180) | | + ## Scenario **Company:** A retailer with regional stores @@ -40,18 +47,19 @@ You don't need an AWS account. Your instructor prepares the S3 buckets, the refe | Source | Location | Content | | :--- | :--- | :--- | -| Sales file | `regional-landing-bkt/daily/` (region `eu-north-1`) | One row per sale: store, region, product, quantity, price | +| Sales file | `regional-landing-bkt/daily/sales_2026-10-14.csv` (region `eu-north-1`) | One row per sale: store, region, product, quantity, price | | Reference file | `regional-landing-bkt/reference/product_master.csv` (region `eu-north-1`) | Unit cost of each of the 15 products | | Curated output | `curated-corp/profit-margins/` (region `us-west-1`) | The enriched sales data, written by your flow | **Goal:** Calculate profit margin, a number that exists nowhere in the source file, for every sale, write the enriched data to the curated bucket, and summarize the run. -The AmazonWebServices adapter is the hero of this flow. You use it three ways: polling for new files, reading one file on demand, and writing the result. +The AmazonWebServices adapter is the hero of this flow. You use it three times: to read the sales file, to read the reference file, and to write the result. The flow you'll build: ```text -S3 sender (poll daily/, keep the file and process it again) +Start Timer (runs the flow once each time you deploy) + -> Read Sales File (Request Reply -> S3 read: daily/sales_2026-10-14.csv) -> CSV to XML Converter (CSV -> structured XML, using xml_schema.xsd) -> XML to JSON Converter (XML -> JSON) -> Prepare for Lookup (Content Modifier: ingestion metadata, stash the sales JSON) @@ -64,7 +72,8 @@ S3 sender (poll daily/, keep the file and process it again) | Part of the flow | What it does | | :--- | :--- | -| **S3 sender** | Polls the `daily/` folder for the sales CSV and leaves the file in place, so it is processed again on every poll | +| **Start Timer** | Starts the flow once, each time you deploy. No sender system is needed. | +| **Read Sales File** | Reads `sales_2026-10-14.csv` from the `daily/` folder and leaves the file in place | | **CSV to XML Converter** | Gives each row named fields, using the `xml_schema.xsd` schema | | **XML to JSON Converter** | Turns the XML into JSON, the shape the rest of the flow works with | | **Prepare for Lookup** | Records when and where the file was ingested, and sets the sales data aside so it survives the next step | @@ -75,17 +84,21 @@ S3 sender (poll daily/, keep the file and process it again) --- -### Create a Package and an Integration Flow +### Step 1 — Create a Package and an Integration Flow + +#### Create an Integration Package -In SAP Integration Suite, go to **Design → Integrations and APIs** and click **Create**. +Now that we understand the integration scenario, it is time to create the integration flow (iFlow). + +👉 Navigate to SAP Integration Suite - URL specified under System Access. Now, create a new Integration Package, name it ***AWS Pipeline for Revenue Management Integration Scenario User{{var:userNumber}}*** (replace the number with the number corresponding to your user) and select the **Save** button. | Field | Value | -| :--- | :--- | -| Name | `AWS Pipeline for Revenue Management` | -| Technical Name | *(auto-populated)* | -| Short Description | `Ingest and enrich sales data in Amazon S3` | +| ---------- | ------- | +| Name | AWS Pipeline for Revenue Management Integration Scenario User{{var:userNumber}} | +| Technical Name | AWSPipelineforRevenueManagementIntegrationScenarioUser{{var:userNumber}} | +| Short Description | This integration package contains artifacts part of the Developer Garage where we explore how to ingest and enrich sales data in Amazon S3 using the AmazonWebServices adapter. | -Click **Save**. +#### Create the Integration Flow On the package's **Artifacts** tab, click **Add → Integration Flow**. Keep **Create** selected and fill in: @@ -94,7 +107,7 @@ On the package's **Artifacts** tab, click **Add → Integration Flow**. Keep **C | Name | `AWS Pipeline for Revenue Management` | | ID | *(auto-populated)* | -Click **OK**, open the integration flow, and click **Edit**. The canvas starts with a **Sender**, a **Start** event, an **End** event, and a **Receiver**. +Click **OK**, open the integration flow, and click **Edit**. Delete the default **Sender** and **Receiver**. This flow is started by a timer and has no sender. > **Tip:** You add steps from the palette at the top of the canvas. Converters, Content Modifier, and Groovy Script are under **Message Transformers**; Request Reply is under **Call → External Call**; extra receivers are under **Participants**. Menu names can differ slightly between tenant versions. @@ -102,26 +115,29 @@ Click **Save** regularly as you build. --- -### Create Security Material - -Your instructor shares `From the Instructor.txt` with you through a secure medium. It holds the `access key id` and `secret access key`. Store them once; every AWS connection in your flow refers to them by name. +### Step 2 — Check the Credentials Provisioned for You -Go to **Monitor → Integrations and APIs → Security Material**. Create two secure parameters, one after the other, with **Create → Secure Parameter**: +The AWS adapters authenticate to Amazon S3 with two secure parameters. They have been created in your tenant, so you don't create them and you never see the AWS access key or secret. -| Name | Secure Parameter | -| :--- | :--- | -| `S3Key` | AWS Access Key ID | -| `S3Secret` | AWS Secret Access Key | +Check that they exist. Go to **Monitor → Integrations and APIs → Security Material**. In the list, find two entries named `S3Key` and `S3Secret` with type **Secure Parameter**. -For each one, enter the value in **Secure Parameter** and again in **Repeat Secure Parameter**, then click **Deploy**. +You enter these names in the **Access Key Alias** and **Secret Key Alias** fields of the AWS adapters in Steps 3, 6, and 8. If either is missing, ask a Developer Advocate. Don't create them yourself. -> **Caution:** Use the exact names `S3Key` and `S3Secret`, including capitals. The AWS adapters look the secure parameters up by these names (as aliases); a mismatch makes the deployment fail. Never paste the keys into chat, slides, screenshots, or a shared file. +> **Caution:** The names are case-sensitive: `S3Key` and `S3Secret`. The AWS adapters look the credentials up by these names; a mismatch makes the deployment fail. Never ask for, paste, or share the AWS keys in chat, slides, screenshots, or a shared file. --- -### Connect the S3 Sender +### Step 3 — Start the Flow and Read the Sales File + +#### Start Timer + +Replace the **Start** event with a **Timer Start** event. On its **Scheduler** tab, keep **Basic** selected and set **Enter As** to `Simple Schedule`, **Repeat** to `None`, and **Schedule** to `On Deployment`, so the flow runs once each time you deploy. + +![Timer Start — Scheduler tab](./assets/aws-s3-step3-timer-start.png) -Drag a connection from the **Sender** to the **Start** event. Choose adapter **AmazonWebServices** and message protocol **S3**. +#### Read Sales File + +Add a **Request Reply** after the timer and name it `Read Sales File`. Add a **Receiver** from the palette, connect **Read Sales File** to it, and choose adapter **AmazonWebServices** and message protocol **S3**. Fill in the adapter settings: @@ -130,35 +146,32 @@ Fill in the adapter settings: | Connection | Host | `amazonaws.com` | | Connection | Region Name | `eu-north-1 Europe (Stockholm)` (choose from the dropdown) | | Connection | Bucket Name | `regional-landing-bkt` | -| Connection | Polling Interval (in ms) | `300000` (every 5 minutes; each poll is a run) | | Connection | Authentication Type | `Access and Secret Key` | | Connection | Access Key Alias | `S3Key` | | Connection | Secret Key Alias | `S3Secret` | | Connection | Requester Pays | Unchecked | | Connection | Enable Legacy Connectivity | Unchecked | +| Processing | Operation | `Read Operation` | | Processing | Directory | `daily` | -| Processing | File Name | `sales*.csv` | -| Processing | Sorting | `None` | -| Processing | Include Sub-Directories | Unchecked | -| Processing | Execute Post-Processing When Message Successfully Processed | Unchecked | -| Processing | Post-Processing | `Keep File and Process Again` | -| Processing | Optional S3 Metadata | Leave empty | -| Processing | S3 Pre-Signed URL | Unchecked | -| Duplicate Handling | Duplicate Check | Unchecked | +| Processing | File Name | `sales_2026-10-14.csv` | +| Processing | Post-Processing | `Keep File` | +| Processing | Customer Decryption Key Alias | Leave empty | -![S3 sender — Connection tab](./assets/aws-s3-step3-sender-connection.png) +![Read Sales File — Connection tab](./assets/aws-s3-step3-read-sales-connection.png) -![S3 sender — Processing tab](./assets/aws-s3-step3-sender-processing.png) +![Read Sales File — Processing tab](./assets/aws-s3-step3-read-sales-processing.png) > **Caution:** Choose the region from the **Region Name** dropdown. A region label typed or pasted into a field makes the adapter fail with an `UnknownHostException`. -The file is never moved or deleted, so every poll processes the same sales file again. You may see the design-time warning "AWS_SND_HTTP_S3_PULL may not pass Text message to CSV To XML Converter". It's an advisory and doesn't stop the flow. +The file is never moved or deleted, so it stays in `daily/` and your flow can read it again each time you deploy. + +--- -### Convert the CSV to JSON +### Step 4 — Convert the CSV to JSON #### CSV to XML Converter -Add a **CSV to XML Converter** right after the **Start** event. On its **Processing** tab, click **Select** next to **XML Schema** and upload `xml_schema.xsd`. If you don't have the file, save the XML below as `xml_schema.xsd` and upload that. +Add a **CSV to XML Converter** right after **Read Sales File**. On its **Processing** tab, click **Select** next to **XML Schema** and upload `xml_schema.xsd`. If you don't have the file, save the XML below as `xml_schema.xsd` and upload that. | Field | Value | | :--- | :--- | @@ -167,6 +180,7 @@ Add a **CSV to XML Converter** right after the **Start** event. On its **Process | Record Marker in CSV | *(leave blank)* | | Field Separator in CSV | `Comma` | | Exclude First Line Header | ✓ | +| Configure CSV Headers to match | `XSD Elements` | ```xml @@ -199,7 +213,7 @@ Add a **CSV to XML Converter** right after the **Start** event. On its **Process ![CSV to XML Converter — Processing tab](./assets/aws-s3-step4-csv-to-xml.png) -> The converter matches the CSV's header row against the XSD element names exactly, so the sales file's header must be lowercase: `transaction_id,store_id,region,product_id,product_name,category,quantity,unit_price,total_amount,sale_timestamp,payment_method`. The files your instructor's feeder generates already are. +> The converter matches the CSV's header row against the XSD element names exactly, so the sales file's header must be lowercase: `transaction_id,store_id,region,product_id,product_name,category,quantity,unit_price,total_amount,sale_timestamp,payment_method`. The provided sales file already is. #### XML to JSON Converter @@ -215,7 +229,9 @@ Add an **XML to JSON Converter** after it: This converter writes every value as a JSON string. The Groovy script in Step 7 turns the numbers back into real numbers. -### Prepare for the Lookup +--- + +### Step 5 — Prepare for the Lookup Add a **Content Modifier** after the XML to JSON Converter and name it `Prepare for Lookup`. On its **Exchange Property** tab, add four rows: @@ -234,9 +250,11 @@ Add a **Content Modifier** after the XML to JSON Converter and name it `Prepare > **Caution:** Set the Data Type of `preEnrichmentJson` to `java.lang.String`. Without it, the property only points at the body's data stream, the lookup in the next step reads that stream to the end, and the script later finds the property empty ("Text must not be null or empty"). -### Look Up Unit Costs from S3 +--- + +### Step 6 — Look Up Unit Costs from S3 -Unit costs live in a reference file your instructor placed in S3: `regional-landing-bkt/reference/product_master.csv`. You read it mid-flow with the same AWS adapter, this time as a receiver. +Unit costs live in a reference file that has been placed in S3: `regional-landing-bkt/reference/product_master.csv`. You read it mid-flow with the same AWS adapter, the same way as the sales file. Add a **Request Reply** after Prepare for Lookup and name it `Cost LookUp`. Add a **Receiver** from the palette (its default name is `Receiver1`). Connect **Cost LookUp** to it and choose **AmazonWebServices**, protocol **S3**. @@ -248,12 +266,12 @@ Add a **Request Reply** after Prepare for Lookup and name it `Cost LookUp`. Add | Authentication Type | `Access and Secret Key` | | Access Key Alias | `S3Key` | | Secret Key Alias | `S3Secret` | -| Operation | `Read` | +| Operation | `Read Operation` | | Directory | `reference` | | File Name | `product_master.csv` | | Post-Processing | `Keep File` | -![Cost LookUp — S3 receiver, Read operation](./assets/aws-s3-step6-cost-lookup.png) +![Cost LookUp — Processing tab](./assets/aws-s3-step6-cost-lookup.png) > **Caution:** Post-Processing defaults to **Delete File**. Change it to **Keep File**. With the default, the read fails, because you're only allowed to read the reference folder. @@ -267,9 +285,11 @@ P-1003,5.99 ... ``` -The sender waits for files to arrive; this receiver fetches one specific file when the flow asks for it. Same adapter, two different jobs. +Read Sales File and Cost LookUp use the same adapter and the same operation. Only the file differs. + +--- -### Calculate Profit Margin +### Step 7 — Calculate Profit Margin Add a **Groovy Script** step after Cost LookUp and name it `Margin Calculation`. Click the step's create-script icon, replace the template in the editor with the script below, and click **OK**. @@ -400,8 +420,8 @@ Message processData(Message message) { // it only sets two exchange properties, runHeadline and runSummary, // which show up in Monitor -> the message's trace -> Properties for any // step after this one. Edit the two path constants if your buckets differ. - // The sender keeps the source file in place (Post-Processing: Keep File - // and Process Again), so every poll reprocesses the same file. + // Read Sales File keeps the source file in place (Post-Processing: Keep + // File), so it stays in daily/ for the next run. def sourcePath = "regional-landing-bkt/daily/ (kept for the next run)" def curatedPath = "curated-corp/profit-margins/sales_" + message.getProperty("businessDate") + ".json" @@ -452,11 +472,13 @@ Message processData(Message message) { > The **Read from** and **Published to** lines in the summary use fixed paths. If your folders differ from the defaults, edit the `sourcePath` and `curatedPath` lines in the script. The flow itself doesn't depend on them. -### Write the Curated File and Show the Summary +--- + +### Step 8 — Write the Curated File and Show the Summary #### S3 Write -Add a **Request Reply** after Margin Calculation and name it `S3 Write`. Connect it to the **Receiver** that came with the canvas and choose **AmazonWebServices**, protocol **S3**. +Add a **Request Reply** after Margin Calculation and name it `S3 Write`. Add a third **Receiver** from the palette, connect **S3 Write** to it, and choose **AmazonWebServices**, protocol **S3**. | Field | Value | | :--- | :--- | @@ -466,13 +488,15 @@ Add a **Request Reply** after Margin Calculation and name it `S3 Write`. Connect | Authentication Type | `Access and Secret Key` | | Access Key Alias | `S3Key` | | Secret Key Alias | `S3Secret` | -| Operation | `Write` | +| Operation | `Write Operation` | | Directory | `profit-margins` | | File Name | `sales_${property.businessDate}.json` | | Content Type | `application/json` | | Existing File Handling | `Overwrite` | -![S3 Write — S3 receiver, Write operation](./assets/aws-s3-step8-write-curated.png) +![S3 Write — Connection tab](./assets/aws-s3-step8-write-connection.png) + +![S3 Write — Processing tab](./assets/aws-s3-step8-write-curated.png) #### Show Run Summary @@ -492,17 +516,17 @@ Your finished flow: --- -### Deploy and Test +### Step 9 — Deploy and Test Click **Save**, then **Deploy**. Go to **Monitor → Integrations and APIs → Manage Integration Content** and wait until your integration flow shows **Started**. In the same view, select your integration flow and set its **Log Level** to **Trace**. Trace switches itself off after a short while, so do this right before testing. -Your flow processes the sales file in `daily/` on every poll, so a run starts within a few minutes of deploying. Open **Monitor Message Processing** and wait for your message to show **Completed**. Open it and click **Message Content**. The view shows the message *before* the step you select. +Your flow runs once each time you deploy it. Open **Monitor Message Processing** and wait for your message to show **Completed**. Open it and click **Message Content**. The view shows the message *before* the step you select. | Check | Where | What you should see | | :--- | :--- | :--- | -| Picked up | Monitor Message Processing | Your message appears. The file stays in `daily/` and is picked up again on the next poll | +| Picked up | Monitor Message Processing | Your message appears. The file stays in `daily/` | | Enriched | **S3 Write** → Payload | Every sale has `unit_cost`, `gross_margin`, and `margin_percentage` | | Summarized | **End 1** → Exchange Properties | `runSummary` holds the run summary | @@ -510,12 +534,12 @@ Your flow processes the sales file in `daily/` on every poll, so a run starts wi ![Message Content — run summary at End 1](./assets/aws-s3-step9-run-summary.png) -This example comes from a test run with the 1,000-row sample file `sales_2026-10-07.csv`: +This example comes from a test run with the 1,000-row sample file `sales_2026-10-14.csv`: ```text Profit margin run complete -Business date: 2026-10-07 +Business date: 2026-10-14 Records: 1000 (0 without cost data) Revenue: $91,022.76 Gross margin: $37,338.94 (41.0%) @@ -534,25 +558,25 @@ Published to: curated-corp/profit-margins/sales_.json | Issue | Fix | | :--- | :--- | | `UnknownHostException: s3.Europe (Stockholm)...` | The region was entered as a label instead of chosen from the dropdown. Select `eu-north-1 Europe (Stockholm)`. | -| Deployment fails mentioning a credential | The secure parameter names don't match. They must be exactly `S3Key` and `S3Secret`, deployed, and entered in the Access Key Alias and Secret Key Alias fields of all three AWS adapters. | +| Deployment fails mentioning a credential | The names in the Access Key Alias and Secret Key Alias fields of all three AWS adapters must be exactly `S3Key` and `S3Secret`. If they are, check Step 2 for both entries; if one is missing, ask a Developer Advocate. | | `Header TRANSACTION_ID not present in CSV headers` | The sales file's header doesn't match the XSD. It must be the lowercase header shown in Step 4. | -| Cost LookUp: access denied | Post-Processing must be **Keep File**, not Delete File. If it is, ask your instructor to check the reference folder permission. | +| Cost LookUp: access denied | Post-Processing must be **Keep File**, not Delete File. If it is, ask a Developer Advocate to check the reference folder permission. | | Cost LookUp: file not found | Directory must be `reference` and File Name `product_master.csv`, exactly, in Region `eu-north-1`. | | `Text must not be null or empty` (Margin Calculation) | The stashed sales data is empty. Set the Data Type of `preEnrichmentJson` to `java.lang.String` in Prepare for Lookup. | | `JsonException ... current character read is '<'` | The stashed property holds XML. The XML to JSON Converter is missing, or Prepare for Lookup sits before it. | | Every record shows `unit_cost` "unknown" | The lookup found no costs. Check in Message Content that the body after Cost LookUp starts with `product_id,unit_cost`. | | Curated file is named `sales_.json` | `businessDate` is missing. Add it in Prepare for Lookup (Step 5). | | Script check: JSONSlurper without streaming | A best-practice warning, not an error. The script already reads the body as a stream. | -| No Message Content in the Monitor | Trace wasn't active when the file was processed. Set the log level to Trace and wait for the next poll. | -| My message never appears | Check your flow is **Started** in Manage Integration Content, and that the sender Directory is `daily`. | +| No Message Content in the Monitor | Trace wasn't active when the file was processed. Set the log level to Trace and deploy again. | +| My message never appears | Check your flow is **Started** in Manage Integration Content, and that the Directory in Read Sales File is `daily`. | #### Try It Yourself - Before you open the summary, predict which region earned the most margin. Then check. -- Ask your instructor to raise the unit cost of the Standing Desk in `product_master.csv`. The next run's margin drops, even though the sales data didn't change. -- Ask your instructor to replace the file in `daily/` with one that contains a sale of product `P-9999`. The summary reports one record without cost data, and that record shows `unknown` for its margin. +- Ask a Developer Advocate to raise the unit cost of the Standing Desk in `product_master.csv`. The next run's margin drops, even though the sales data didn't change. +- Ask a Developer Advocate to replace the file in `daily/` with one that contains a sale of product `P-9999`. The summary reports one record without cost data, and that record shows `unknown` for its margin. -You built a flow that polls Amazon S3, enriches the data from a reference file in S3, writes the result back to S3 in another region, and summarizes the run. +You built a flow that reads from Amazon S3, enriches the data from a reference file in S3, writes the result back to S3 in another region, and summarizes the run. #### References diff --git a/tutorials/dev-garage-aws-pipeline-revenue-management/files/TransformCsvToJson.groovy b/tutorials/dev-garage-aws-pipeline-revenue-management/files/TransformCsvToJson.groovy index cd20876..f474996 100644 --- a/tutorials/dev-garage-aws-pipeline-revenue-management/files/TransformCsvToJson.groovy +++ b/tutorials/dev-garage-aws-pipeline-revenue-management/files/TransformCsvToJson.groovy @@ -115,8 +115,8 @@ Message processData(Message message) { // it only sets two exchange properties, runHeadline and runSummary, // which show up in Monitor -> the message's trace -> Properties for any // step after this one. Edit the two path constants if your buckets differ. - // The sender keeps the source file in place (Post-Processing: Keep File - // and Process Again), so every poll reprocesses the same file. + // Read Sales File keeps the source file in place (Post-Processing: Keep + // File), so it stays in daily/ for the next run. def sourcePath = "regional-landing-bkt/daily/ (kept for the next run)" def curatedPath = "curated-corp/profit-margins/sales_" + message.getProperty("businessDate") + ".json" diff --git a/tutorials/dev-garage-error-resolution-ai-adapter/assets/ai-adapter-step3-set-context.png b/tutorials/dev-garage-error-resolution-ai-adapter/assets/ai-adapter-step3-set-context.png index d19b1a4..83ec7d1 100644 Binary files a/tutorials/dev-garage-error-resolution-ai-adapter/assets/ai-adapter-step3-set-context.png and b/tutorials/dev-garage-error-resolution-ai-adapter/assets/ai-adapter-step3-set-context.png differ diff --git a/tutorials/dev-garage-error-resolution-ai-adapter/assets/ai-adapter-step5-build-prompt.png b/tutorials/dev-garage-error-resolution-ai-adapter/assets/ai-adapter-step5-build-prompt.png new file mode 100644 index 0000000..c7d0694 Binary files /dev/null and b/tutorials/dev-garage-error-resolution-ai-adapter/assets/ai-adapter-step5-build-prompt.png differ diff --git a/tutorials/dev-garage-error-resolution-ai-adapter/assets/ai-adapter-step6-build-prompt.png b/tutorials/dev-garage-error-resolution-ai-adapter/assets/ai-adapter-step6-build-prompt.png deleted file mode 100644 index a1bde7c..0000000 Binary files a/tutorials/dev-garage-error-resolution-ai-adapter/assets/ai-adapter-step6-build-prompt.png and /dev/null differ diff --git a/tutorials/dev-garage-error-resolution-ai-adapter/assets/ai-adapter-step6-connection.png b/tutorials/dev-garage-error-resolution-ai-adapter/assets/ai-adapter-step6-connection.png new file mode 100644 index 0000000..3c672cc Binary files /dev/null and b/tutorials/dev-garage-error-resolution-ai-adapter/assets/ai-adapter-step6-connection.png differ diff --git a/tutorials/dev-garage-error-resolution-ai-adapter/assets/ai-adapter-step6-local-process.png b/tutorials/dev-garage-error-resolution-ai-adapter/assets/ai-adapter-step6-local-process.png deleted file mode 100644 index 754e37d..0000000 Binary files a/tutorials/dev-garage-error-resolution-ai-adapter/assets/ai-adapter-step6-local-process.png and /dev/null differ diff --git a/tutorials/dev-garage-error-resolution-ai-adapter/assets/ai-adapter-step6-processing.png b/tutorials/dev-garage-error-resolution-ai-adapter/assets/ai-adapter-step6-processing.png new file mode 100644 index 0000000..d973ad1 Binary files /dev/null and b/tutorials/dev-garage-error-resolution-ai-adapter/assets/ai-adapter-step6-processing.png differ diff --git a/tutorials/dev-garage-error-resolution-ai-adapter/assets/ai-adapter-step7-connection.png b/tutorials/dev-garage-error-resolution-ai-adapter/assets/ai-adapter-step7-connection.png deleted file mode 100644 index b46e3b0..0000000 Binary files a/tutorials/dev-garage-error-resolution-ai-adapter/assets/ai-adapter-step7-connection.png and /dev/null differ diff --git a/tutorials/dev-garage-error-resolution-ai-adapter/assets/ai-adapter-step7-exception-subprocess.png b/tutorials/dev-garage-error-resolution-ai-adapter/assets/ai-adapter-step7-exception-subprocess.png new file mode 100644 index 0000000..03116b0 Binary files /dev/null and b/tutorials/dev-garage-error-resolution-ai-adapter/assets/ai-adapter-step7-exception-subprocess.png differ diff --git a/tutorials/dev-garage-error-resolution-ai-adapter/assets/ai-adapter-step7-finished-iflow.png b/tutorials/dev-garage-error-resolution-ai-adapter/assets/ai-adapter-step7-finished-iflow.png new file mode 100644 index 0000000..b4f713b Binary files /dev/null and b/tutorials/dev-garage-error-resolution-ai-adapter/assets/ai-adapter-step7-finished-iflow.png differ diff --git a/tutorials/dev-garage-error-resolution-ai-adapter/assets/ai-adapter-step7-processing.png b/tutorials/dev-garage-error-resolution-ai-adapter/assets/ai-adapter-step7-processing.png deleted file mode 100644 index e367346..0000000 Binary files a/tutorials/dev-garage-error-resolution-ai-adapter/assets/ai-adapter-step7-processing.png and /dev/null differ diff --git a/tutorials/dev-garage-error-resolution-ai-adapter/assets/ai-adapter-step9-ai-resolution.png b/tutorials/dev-garage-error-resolution-ai-adapter/assets/ai-adapter-step8-ai-resolution.png similarity index 100% rename from tutorials/dev-garage-error-resolution-ai-adapter/assets/ai-adapter-step9-ai-resolution.png rename to tutorials/dev-garage-error-resolution-ai-adapter/assets/ai-adapter-step8-ai-resolution.png diff --git a/tutorials/dev-garage-error-resolution-ai-adapter/assets/ai-adapter-step9-custom-headers.png b/tutorials/dev-garage-error-resolution-ai-adapter/assets/ai-adapter-step8-custom-headers.png similarity index 100% rename from tutorials/dev-garage-error-resolution-ai-adapter/assets/ai-adapter-step9-custom-headers.png rename to tutorials/dev-garage-error-resolution-ai-adapter/assets/ai-adapter-step8-custom-headers.png diff --git a/tutorials/dev-garage-error-resolution-ai-adapter/assets/ai-adapter-step8-exception-subprocess.png b/tutorials/dev-garage-error-resolution-ai-adapter/assets/ai-adapter-step8-exception-subprocess.png deleted file mode 100644 index 3661078..0000000 Binary files a/tutorials/dev-garage-error-resolution-ai-adapter/assets/ai-adapter-step8-exception-subprocess.png and /dev/null differ diff --git a/tutorials/dev-garage-error-resolution-ai-adapter/assets/ai-adapter-step8-finished-iflow.png b/tutorials/dev-garage-error-resolution-ai-adapter/assets/ai-adapter-step8-finished-iflow.png deleted file mode 100644 index 21e59dd..0000000 Binary files a/tutorials/dev-garage-error-resolution-ai-adapter/assets/ai-adapter-step8-finished-iflow.png and /dev/null differ diff --git a/tutorials/dev-garage-error-resolution-ai-adapter/assets/ai-adapter-step8-message-body.png b/tutorials/dev-garage-error-resolution-ai-adapter/assets/ai-adapter-step8-message-body.png new file mode 100644 index 0000000..bc28321 Binary files /dev/null and b/tutorials/dev-garage-error-resolution-ai-adapter/assets/ai-adapter-step8-message-body.png differ diff --git a/tutorials/dev-garage-error-resolution-ai-adapter/dev-garage-error-resolution-ai-adapter.md b/tutorials/dev-garage-error-resolution-ai-adapter/dev-garage-error-resolution-ai-adapter.md index 2351b79..441cd6c 100644 --- a/tutorials/dev-garage-error-resolution-ai-adapter/dev-garage-error-resolution-ai-adapter.md +++ b/tutorials/dev-garage-error-resolution-ai-adapter/dev-garage-error-resolution-ai-adapter.md @@ -13,68 +13,76 @@ primary_tag: software-product>sap-integration-suite Build an integration flow in SAP Integration Suite that catches an error, masks it, asks an LLM in SAP AI Core for a diagnosis through the AI Adapter, and attaches a readable resolution to the message. -## Prerequisites - -- An SAP BTP account with **SAP Integration Suite** and the **Cloud Integration** capability activated, and the **AI Adapter** available on your tenant -- [Assign roles to your user for Cloud Integration](https://help.sap.com/docs/integration-suite/sap-integration-suite/configuring-user-access?locale=en-US&state=PRODUCTION&version=CLOUD#loio2c6214a3228e4b4cba207f49fda92ed4__section_xbf_glz_l2c), including the `Integration_Developer` role collection -- The file `From the Instructor.txt`, which your instructor shares with you through a secure medium. It is not part of this repository. It contains the values from an **SAP AI Core** service key and deployment: `address`, `orchestration deployment id`, `ai resource group`, `model`, `token service url`, `clientid`, and `clientsecret` -- Familiarity with the SAP Integration Suite interface helps. No Groovy experience is required; all scripts are provided. - -You don't need your own SAP AI Core instance. Your instructor prepares it. Generative AI Hub and orchestration need the **extended** SAP AI Core service plan. - ## You will learn - How to connect Cloud Integration to SAP AI Core with the AI Adapter (AICore-GenAIHub variant) -- How to store SAP AI Core credentials as OAuth2 Security Material +- How the AI Adapter uses an OAuth2 credential that has been provisioned for you - How to catch and handle errors in an exception subprocess without losing the original error - How to mask personal data before anything is sent to an LLM -- How to turn an AI answer into readable attachments and searchable fields in the Monitor +- How to turn an AI answer into readable attachments, a readable message body, and searchable fields in the Monitor + +## Prerequisites + +- Contact a Developer Advocate in the Developer Garage to share the necessary credentials for accessing the provisioned systems. +- Various systems have been provisioned for you to complete this tutorial. + +### System access + +As part of this tutorial, you will need to access the provisioned systems using the credentials shared with you. In the table below, you will find the necessary information to access each system. + +> There are two SAP Integration Suite environments available for use in this tutorial. Use the one corresponding to your user range. + +| System | URL | +| --- | --- | +| SAP Integration Suite - Dev Garage 1 (User01 - User99) | | +| SAP Integration Suite - Dev Garage 2 (User100 - User180) | | ## Scenario **Challenge:** Integration errors are often cryptic, and diagnosing them takes time. Support teams need a plain-language explanation and a suggested fix next to the error. -**Approach:** You build an integration flow that fails on purpose. A sales order contains a text value where the schema expects a number. The flow catches the error, adds context, masks personal data, and sends the result to an LLM in SAP AI Core through the AI Adapter. The answer is attached to the message as a readable resolution. +**Approach:** You build an integration flow that generates a test error. A sales order contains a text value where the schema expects a number. The flow catches the error, adds context, masks personal data, and sends the result to an LLM in SAP AI Core through the AI Adapter. The answer is attached to the message as a readable resolution. The AI Adapter is the hero of this flow. It calls the SAP AI Core orchestration service with OAuth2, and you never handle a token yourself. -The flow consists of three connected pieces: +The flow consists of two connected pieces: ```text Main flow: Start Timer -> Set Order Payload -> Set Context -> Validate Order -> End Exception Subprocess 1 (runs when anything in the main flow fails): - Error Start -> Capture Error -> Get AI Diagnosis (Process Call) -> Attach AI Diagnosis -> End Message - -Local process "Get AI Diagnosis": - Start -> Build Prompt -> Request Reply (AI Adapter -> SAP AI Core) -> End - with its own Exception Subprocess 2: Error Start -> SetAIStatus -> End Message + Error Start -> Capture Error -> Build Prompt -> Call AI (AI Adapter -> SAP AI Core) -> Attach AI Diagnosis -> End Message ``` | Part of the flow | What it does | | :--- | :--- | | **Start Timer** | Starts the flow once, each time you deploy. No sender system is needed. | -| **Set Order Payload** | Creates a sample sales order with a deliberate error. | +| **Set Order Payload** | Creates a sample sales order that contains a test error. | | **Set Context** | Records the step and business object, so the error handler knows where it failed. | | **Validate Order** | Checks the order against a schema and fails with a precise reason. | | **Capture Error** | Collects the error and context, masks personal data, and prepares the AI input. | -| **Get AI Diagnosis** | Local process that builds the prompt and calls SAP AI Core through the AI Adapter. | -| **Attach AI Diagnosis** | Turns the AI answer into readable attachments and searchable fields in the Monitor. | +| **Build Prompt** | Puts the masked error into the instructions for the AI. | +| **Call AI** | Calls SAP AI Core through the AI Adapter and receives the diagnosis. | +| **Attach AI Diagnosis** | Turns the AI answer into readable attachments, a readable message body, and searchable fields in the Monitor. | --- -### Create a Package and an Integration Flow +### Step 1 — Create a Package and an Integration Flow + +#### Create an Integration Package + +Now that we understand the integration scenario, it is time to create the integration flow (iFlow). -In SAP Integration Suite, go to **Design → Integrations and APIs** and click **Create**. +👉 Navigate to SAP Integration Suite - URL specified under System Access. Now, create a new Integration Package, name it ***Error Resolution with AI Adapter Integration Scenario User{{var:userNumber}}*** (replace the number with the number corresponding to your user) and select the **Save** button. | Field | Value | -| :--- | :--- | -| Name | `Error Resolution with AI Adapter` | -| Technical Name | *(auto-populated)* | -| Short Description | `Resolve integration errors with AI using the AI Adapter and SAP AI Core` | +| ---------- | ------- | +| Name | Error Resolution with AI Adapter Integration Scenario User{{var:userNumber}} | +| Technical Name | ErrorResolutionwithAIAdapterIntegrationScenarioUser{{var:userNumber}} | +| Short Description | This integration package contains artifacts part of the Developer Garage where we explore use an LLM to resolve integration errors with AI using the AI Adapter and SAP AI Core. | -Click **Save**. +#### Create the Integration Flow On the package's **Artifacts** tab, click **Add → Integration Flow**. Keep **Create** selected and fill in: @@ -85,37 +93,31 @@ On the package's **Artifacts** tab, click **Add → Integration Flow**. Keep **C Click **OK**, open the integration flow, and click **Edit**. Delete the default **Sender** and **Receiver**. This flow is started by a timer and has no sender. -> **Tip:** You add steps from the palette at the top of the canvas. Content Modifier and Groovy Script are under **Message Transformers**; Request Reply and Process Call are under **Call**; Exception Subprocess and Local Integration Process are under **Process**. Menu names can differ slightly between tenant versions. +> **Tip:** You add steps from the palette at the top of the canvas. Content Modifier and Groovy Script are under **Message Transformers**; Request Reply is under **Call**; Exception Subprocess is under **Process**. Menu names can differ slightly between tenant versions. Click **Save** regularly as you build. -### Create Security Material for SAP AI Core +--- -Your instructor shares `From the Instructor.txt` with you through a secure medium. It holds the `clientid`, `clientsecret`, and `token service url` from the SAP AI Core service key. Store them once; the AI Adapter refers to them by name. +### Step 2 — Check the Credential Provisioned for You -Go to **Monitor → Integrations and APIs → Security Material** and click **Create → OAuth2 Client Credentials**. +The AI Adapter authenticates to SAP AI Core with an OAuth2 credential. It has been created in your tenant, so you don't create it and you never see the client ID, client secret, or token URL. -| Field | Value | -| :--- | :--- | -| Name | `GenAI_Creds` | -| Token Service URL | `token service url` from `From the Instructor.txt`. It is the service key's `url` value with `/oauth/token` added, for example `https://.authentication..hana.ondemand.com/oauth/token` | -| Client ID | `clientid` from `From the Instructor.txt` | -| Client Secret | `clientsecret` from `From the Instructor.txt` | -| Client Authentication | `Send as Request Header` (Basic Authentication). `Send as Body Parameter` normally works as well. | +Check that it exists. Go to **Monitor → Integrations and APIs → Security Material**. In the list, find the entry named `AICore-resolution` with type **OAuth2 Client Credentials**. -Click **Deploy**. +You use this name in Step 6. If `AICore-resolution` is missing, ask a Developer Advocate. Don't create it yourself. -> **Caution:** The token URL comes from the SAP AI Core service key. It contains `.authentication.` and ends with `/oauth/token`. It is **not** the OAuth endpoint of your Cloud Integration tenant, and it is **not** the AI API URL (`https://api.ai.…`), which belongs in the adapter's Address field in Step 7. A wrong token URL fails with `403 Forbidden` when the adapter retrieves the access token. +> **Caution:** Never ask for, paste, or share the client secret in chat, slides, screenshots, or a shared file. The Developer Garage team keeps it. -> **Caution:** Never paste the client secret into chat, slides, screenshots, or a shared file. +--- -### Build the Main Flow That Fails on Purpose +### Step 3 — Build the Main Flow to Generate a Test Error -The main flow creates a sales order with a deliberate error and validates it. A realistic, specific error gives the AI something concrete to diagnose. +The main flow creates a sales order that contains a test error and validates it. A realistic, specific error gives the AI something concrete to diagnose. #### Start Timer -Replace the **Start** event with a **Timer Start** event. On its **Scheduler** tab, choose **Run Once**, so the flow runs each time you deploy. +Replace the **Start** event with a **Timer Start** event. On its **Scheduler** tab, keep **Basic** selected and set **Enter As** to `Simple Schedule`, **Repeat** to `None`, and **Schedule** to `On Deployment`, so the flow runs once each time you deploy. #### Set Order Payload @@ -255,21 +257,9 @@ Connect **Validate Order** to the **End** event. ![Validate Order script in the editor](./assets/ai-adapter-step3-validate-order.png) -### Deploy and See the Failure - -Click **Save**, then **Deploy**. The timer fires once. - -Go to **Monitor → Integrations and APIs → Monitor Message Processing** and open the newest message for your integration flow. - -The status is **Failed** at **Validate Order**, with an error similar to: - -```text -IllegalStateException: SalesOrder schema validation failed: Line 13: cvc-datatype-valid.1.2.1: 'five' is not a valid value for 'integer'. | Line 13: cvc-type.3.1.3: The value 'five' of element 'Quantity' is not valid. -``` - -That's expected. The rest of the tutorial turns this error into an AI-written resolution. +--- -### Capture the Error in an Exception Subprocess +### Step 4 — Capture the Error in an Exception Subprocess From the palette, choose **Process → Exception Subprocess** and drop it inside the **Integration Process**. Keep the **End Message** event that the subprocess comes with. @@ -377,13 +367,11 @@ The script: > **Caution:** Masking with regular expressions is a safety net. It can't find every personal detail, such as names written as free text. For real customer payloads, consider sending no payload extract at all. -### Create the Local Process That Calls the AI - -The AI call lives in its own local process. If the AI is unreachable, that failure is caught there and can't hide the original error. +--- -From the palette, choose **Process → Local Integration Process**. Drop it **outside** the main Integration Process and name it `Get AI Diagnosis`. +### Step 5 — Build the Prompt and Call the AI -Add a **Content Modifier** named `Build Prompt`, then a **Request Reply** (**Call → External Call**). Connect **Start → Build Prompt → Request Reply → End**. +In **Exception Subprocess 1**, add a **Content Modifier** after **Capture Error** and name it `Build Prompt`. Add a **Request Reply** (**Call → External Call**) after it and name it `Call AI`. Connect **Capture Error → Build Prompt → Call AI**. In **Build Prompt**, open the **Message Body** tab, set **Type** to `Expression`, and paste: @@ -405,39 +393,28 @@ ${property.aiErrorContext} > **Caution:** **Type** must be **Expression**. With **Constant**, the AI receives the literal text `${property.aiErrorContext}` instead of your error and answers "insufficient information". -![Build Prompt, Message Body tab with Type Expression](./assets/ai-adapter-step6-build-prompt.png) - -#### Exception Subprocess 2 - -Drop an **Exception Subprocess** inside the `Get AI Diagnosis` local process. Add a **Content Modifier** named `SetAIStatus` between its **Error Start** and **End Message** events. On the **Exchange Property** tab, add: - -| Action | Name | Source Type | Value | -| :--- | :--- | :--- | :--- | -| Create | `aiStatus` | `Constant` | `UNAVAILABLE` | -| Create | `aiError` | `Expression` | `${exception.message}` | - -Keep the **End Message** event here. The AI failure is handled, and processing returns to Exception Subprocess 1. +![Build Prompt, Message Body tab with Type Expression](./assets/ai-adapter-step5-build-prompt.png) -![Local process Get AI Diagnosis](./assets/ai-adapter-step6-local-process.png) +--- -### Configure the AI Adapter +### Step 6 — Configure the AI Adapter -From the palette, add a **Receiver** to the right of the canvas and name it `AI_Core`. Drag a connection from **Request Reply** to `AI_Core`, choose adapter type **AI**, and select the **AICore-GenAIHub** variant. +From the palette, add a **Receiver** to the right of the canvas and name it `AI_Core`. Drag a connection from **Call AI** to `AI_Core`, choose adapter type **AI**, and select the **AICore-GenAIHub** variant. -Fill in the adapter settings, using the values from `From the Instructor.txt`: +Fill in the adapter settings: | Tab | Field | Value | | :--- | :--- | :--- | -| Connection | Address | `address` from `From the Instructor.txt`, usually `https://api.ai..ml.hana.ondemand.com` | -| Connection | OAuth2 Client Credentials | `GenAI_Creds` | +| Connection | Address | `https://api.ai.prod-eu20.westeurope.azure.ml.hana.ondemand.com` | +| Connection | OAuth2 Client Credentials | `AICore-resolution` | | Connection | Reuse Connection | Unchecked | | Connection | Connection Timeout (in ms) | `60000` | | Connection | Response Timeout (in ms) | `60000` | | Processing | Operation | `Orchestration - Completion` | -| Processing | Orchestration Deployment ID | `orchestration deployment id` from `From the Instructor.txt` | -| Processing | AI Resource Group | `ai resource group` from `From the Instructor.txt` (usually `default`) | +| Processing | Orchestration Deployment ID | `d6bc04de07fb1d32` | +| Processing | AI Resource Group | `default` | | Processing | Request Payload Source | `UI Configurable` | -| Processing | Model | `model` from `From the Instructor.txt`, for example `gpt-4o-mini` | +| Processing | Model | `gpt-4o-mini` | | Processing | Role | `user` | | Processing | Input | `${body}` | @@ -445,28 +422,30 @@ Fill in the adapter settings, using the values from `From the Instructor.txt`: > **Note:** Field labels can differ slightly between adapter versions. If yours differ, match them by purpose. -![AI Adapter, Connection tab](./assets/ai-adapter-step7-connection.png) +![AI Adapter, Connection tab](./assets/ai-adapter-step6-connection.png) -![AI Adapter, Processing tab](./assets/ai-adapter-step7-processing.png) +![AI Adapter, Processing tab](./assets/ai-adapter-step6-processing.png) -### Attach the AI Resolution to the Message +--- -In **Exception Subprocess 1**, add a **Process Call** (**Call → Local Call**) after **Capture Error**. Name it `Get AI Diagnosis` and select the local process **Get AI Diagnosis**. +### Step 7 — Attach the AI Resolution to the Message -Add a **Groovy Script** after it, name it `Attach AI Diagnosis`, and paste the script below. Name the script file `AttachDiagnosis.groovy` and click **OK**. +In **Exception Subprocess 1**, add a **Groovy Script** after **Call AI**, name it `Attach AI Diagnosis`, and paste the script below. Name the script file `AttachDiagnosis.groovy` and click **OK**. ```groovy /* * AttachDiagnosis.groovy - * Exception subprocess 1 - runs after the "Get AI Diagnosis" process call. + * Exception subprocess 1 - runs after the AI Adapter's Call AI step. * Reads the AI response, parses the JSON diagnosis, and writes: + * - MPL attachment "AI Resolution" (the problem and numbered fix steps) * - MPL attachment "AI Diagnosis" (human-readable report) * - custom header properties AI_Status, AI_Category, AI_Confidence, AI_NeedsHuman + * - the message body: the resolution text followed by the original payload (personal data masked) * * Handles three cases: - * 1. aiStatus = UNAVAILABLE / CONTEXT_FAILED -> attaches a "diagnosis unavailable" note - * 2. Response is (or contains) valid diagnosis JSON -> formatted report - * 3. Response is not parseable -> attaches raw response marked "unstructured" + * 1. aiStatus = CONTEXT_FAILED (Capture Error failed) -> attaches a "diagnosis unavailable" note + * 2. Response is (or contains) valid diagnosis JSON -> formatted report + * 3. Response is not parseable -> attaches raw response marked "unstructured" * * The response parser accepts either the model's text directly, or a full * orchestration response (final_result / orchestration_result / choices[0].message.content). @@ -494,59 +473,70 @@ def Message processData(Message message) { r << "Error class : " << origClass << "\n" r << "Error : " << origMsg << "\n\n" - // Case 1 - AI not called or AI call failed + String resolutionText = null + if (status != "OK") { + // Case 1 - Capture Error failed, so the AI had no error to diagnose r << "AI diagnosis unavailable (" << status << ").\n" - r << "Reason: " << (message.getProperty("aiError") ?: message.getProperty("contextError") ?: "-") << "\n" + r << "Reason: " << (message.getProperty("contextError") ?: "-") << "\n" r << "The original error above is unaffected - investigate manually.\n" log?.addAttachmentAsString("AI Diagnosis", r.toString(), "text/plain") log?.addCustomHeaderProperty("AI_Status", status) - return message - } - - String body = message.getBody(String) ?: "" - String content = extractContent(body) - def diag = parseJson(content) - - if (diag instanceof Map && diag.root_cause) { - // Case 2 - structured diagnosis - String category = (diag.category ?: "OTHER").toString() - String confidence = (diag.confidence ?: "LOW").toString() - String needsHuman = (diag.needs_human == null ? "true" : diag.needs_human.toString()) - - r << "Root cause : " << diag.root_cause << "\n" - r << "Category : " << category << "\n" - r << "Confidence : " << confidence << "\n" - r << "Needs human : " << needsHuman << "\n\n" - r << "Suggested fix steps:\n" - def steps = (diag.fix_steps instanceof List) ? diag.fix_steps : [diag.fix_steps ?: "-"] - steps.eachWithIndex { s, i -> r << " " << (i + 1) << ". " << s << "\n" } - - // Separate, resolution-only attachment for clean viewing - StringBuilder res = new StringBuilder() - res << "RESOLUTION\n" - res << "==========\n\n" - res << "Problem:\n " << diag.root_cause << "\n\n" - res << "What to do:\n" - steps.eachWithIndex { s, i -> res << " " << (i + 1) << ". " << s << "\n" } - res << "\nCategory: " << category << " | Confidence: " << confidence - res << " | Needs human: " << needsHuman << "\n" - res << "\n---\nAI-generated suggestion - verify before acting.\n" - log?.addAttachmentAsString("AI Resolution", res.toString(), "text/plain") - - log?.addCustomHeaderProperty("AI_Status", "OK") - log?.addCustomHeaderProperty("AI_Category", category) - log?.addCustomHeaderProperty("AI_Confidence", confidence) - log?.addCustomHeaderProperty("AI_NeedsHuman", needsHuman) + resolutionText = r.toString() } else { - // Case 3 - model did not return the expected JSON - r << "AI response (unstructured - did not match expected JSON):\n" - r << (content ?: body) << "\n" - log?.addCustomHeaderProperty("AI_Status", "UNSTRUCTURED") + String body = message.getBody(String) ?: "" + String content = extractContent(body) + def diag = parseJson(content) + + if (diag instanceof Map && diag.root_cause) { + // Case 2 - structured diagnosis + String category = (diag.category ?: "OTHER").toString() + String confidence = (diag.confidence ?: "LOW").toString() + String needsHuman = (diag.needs_human == null ? "true" : diag.needs_human.toString()) + + r << "Root cause : " << diag.root_cause << "\n" + r << "Category : " << category << "\n" + r << "Confidence : " << confidence << "\n" + r << "Needs human : " << needsHuman << "\n\n" + r << "Suggested fix steps:\n" + def steps = (diag.fix_steps instanceof List) ? diag.fix_steps : [diag.fix_steps ?: "-"] + steps.eachWithIndex { s, i -> r << " " << (i + 1) << ". " << s << "\n" } + + // Separate, resolution-only attachment for clean viewing + StringBuilder res = new StringBuilder() + res << "RESOLUTION\n" + res << "==========\n\n" + res << "Problem:\n " << diag.root_cause << "\n\n" + res << "What to do:\n" + steps.eachWithIndex { s, i -> res << " " << (i + 1) << ". " << s << "\n" } + res << "\nCategory: " << category << " | Confidence: " << confidence + res << " | Needs human: " << needsHuman << "\n" + res << "\n---\nAI-generated suggestion - verify before acting.\n" + log?.addAttachmentAsString("AI Resolution", res.toString(), "text/plain") + resolutionText = res.toString() + + log?.addCustomHeaderProperty("AI_Status", "OK") + log?.addCustomHeaderProperty("AI_Category", category) + log?.addCustomHeaderProperty("AI_Confidence", confidence) + log?.addCustomHeaderProperty("AI_NeedsHuman", needsHuman) + } else { + // Case 3 - model did not return the expected JSON + r << "AI response (unstructured - did not match expected JSON):\n" + r << (content ?: body) << "\n" + log?.addCustomHeaderProperty("AI_Status", "UNSTRUCTURED") + } + + r << "\n---\nAI-generated suggestion - verify before acting.\n" + log?.addAttachmentAsString("AI Diagnosis", r.toString(), "text/plain") + if (resolutionText == null) resolutionText = r.toString() } - r << "\n---\nAI-generated suggestion - verify before acting.\n" - log?.addAttachmentAsString("AI Diagnosis", r.toString(), "text/plain") + // Message body: the resolution, then the original payload (masked), so both show in the Monitor + StringBuilder out = new StringBuilder(resolutionText) + out << "\nORIGINAL PAYLOAD (personal data masked)\n" + out << "=======================================\n" + out << maskedPayload(message) << "\n" + message.setBody(out.toString()) } catch (Exception e) { log?.addAttachmentAsString("AI Diagnosis", "Could not build AI diagnosis report: " + e.getMessage(), "text/plain") @@ -555,6 +545,16 @@ def Message processData(Message message) { return message } +// The masked payload that Capture Error saved inside the aiErrorContext property +String maskedPayload(Message message) { + try { + def ctx = new JsonSlurper().parseText(message.getProperty("aiErrorContext") as String) + return (ctx.payloadExtract ?: "-").toString() + } catch (ignored) { + return "-" + } +} + // Pull the model's text out of the response, whatever shape the adapter returns String extractContent(String body) { try { @@ -588,9 +588,9 @@ def parseJson(String s) { } ``` -Connect the steps: **Error Start → Capture Error → Get AI Diagnosis → Attach AI Diagnosis → End Message**. +Connect the steps: **Error Start → Capture Error → Build Prompt → Call AI → Attach AI Diagnosis → End Message**. -![Exception Subprocess 1 complete](./assets/ai-adapter-step8-exception-subprocess.png) +![Exception Subprocess with the AI call](./assets/ai-adapter-step7-exception-subprocess.png) The script reads the model's answer from the orchestration response and writes: @@ -599,36 +599,36 @@ The script reads the model's answer from the orchestration response and writes: | Attachment **AI Resolution** | The problem and numbered fix steps only, for clean viewing | | Attachment **AI Diagnosis** | The full report: iFlow, failed step, original error, root cause, category, confidence, and fix steps | | Custom header properties | `ErrorHandled`, `AI_Status`, `AI_Category`, `AI_Confidence`, and `AI_NeedsHuman`, searchable in the Monitor | +| Message body | The same text as **AI Resolution**, followed by the original payload with personal data masked. You see it in the Monitor with the log level **Trace** (Step 8). | #### Optional: Debug AI Output -While building, you can add a script between **Get AI Diagnosis** and **Attach AI Diagnosis** that attaches the raw adapter response. It shows exactly what the AI received and returned. Remove it when you're done. +While building, you can add a script between **Call AI** and **Attach AI Diagnosis** that attaches the raw adapter response. It shows exactly what the AI returned. Remove it when you're done. ```groovy import com.sap.gateway.ip.core.customdev.util.Message def Message processData(Message message) { def log = messageLogFactory.getMessageLog(message) - String status = message.getProperty("aiStatus") ?: "OK" - String report = "aiStatus: " + status + "\n" + - "aiError: " + (message.getProperty("aiError") ?: "-") + "\n\n" + - "AI response body:\n" + (message.getBody(String) ?: "") + String report = "AI response body:\n" + (message.getBody(String) ?: "") log?.addAttachmentAsString("AI output (debug)", report, "text/plain") return message } ``` -Here is the finished flow: +Your finished integration flow looks like this: -![The complete integration flow](./assets/ai-adapter-step8-finished-iflow.png) +![Finished integration flow](./assets/ai-adapter-step7-finished-iflow.png) -### Deploy and Read the AI Resolution +--- + +### Step 8 — Deploy and Read the AI Resolution Click **Save**, then **Deploy**. Go to **Monitor → Integrations and APIs → Manage Integration Content** and check that your integration flow shows **Started**. Open **Monitor Message Processing** and open the newest message. Its status is **Completed**: the error was caught and handled. Click **Attachments** and open **AI Resolution**. -![AI Resolution attachment, status Completed](./assets/ai-adapter-step9-ai-resolution.png) +![AI Resolution attachment, status Completed](./assets/ai-adapter-step8-ai-resolution.png) The AI Resolution looks similar to this. The wording varies between runs. @@ -656,8 +656,17 @@ AI-generated suggestion - verify before acting. | Original error kept | **Attachments → AI Diagnosis** | The `'five'` validation error for `Quantity` | | Readable resolution | **Attachments → AI Resolution** | The problem and 2 to 4 fix steps that name `Quantity` | | Searchable result | **Custom Header Properties** | `AI_Category = DATA`, `AI_Status = OK`, and `ErrorHandled = true` | +| Resolution in the body | **Trace → End → Message Content → Payload** | The same text as **AI Resolution**, then the masked original order | + +![Custom Header Properties with AI_ and ErrorHandled values](./assets/ai-adapter-step8-custom-headers.png) + +#### View the Resolution in the Message Body + +The body is only stored when the log level is **Trace**. In **Manage Integration Content**, select your integration flow and set its **Log Level** to **Trace**. The timer runs once per deployment, so click **Deploy** again. + +In **Monitor Message Processing**, open the newest message, choose **Logs**, and click **Trace**. In the step list, select **End** (the End Message event), then open **Message Content** and the **Payload** tab. The view shows the message before the step you select, so you see the body that **Attach AI Diagnosis** set: the resolution, followed by the original payload with personal data masked. -![Custom Header Properties with AI_ and ErrorHandled values](./assets/ai-adapter-step9-custom-headers.png) +![Message Content, Payload tab of the End step](./assets/ai-adapter-step8-message-body.png) > **Run it again:** The timer runs once per deployment. To run again, deploy again. The AI's wording varies between runs; the category and the field it names should stay the same. @@ -668,20 +677,22 @@ AI-generated suggestion - verify before acting. | Issue | Fix | | :--- | :--- | | Error says "Refer to MPL attachment (XML Validator Error Document)" | You're using the standard XML Validator. Use the Validate Order script from Step 3. | -| `403 Forbidden` while retrieving the access token | The Token Service URL is wrong. Use the service key's `url` plus `/oauth/token`, not your Cloud Integration tenant URL (Step 2). | -| `400 Bad Request` with `Missing Tenant Id` | Likely cause (unverified): the token and the address come from different SAP AI Core instances. Check the client ID, secret, token URL, Address, and deployment ID all come from the same service key and instance, redeploy the Security Material, then redeploy the flow. | -| Deployment fails: `URISyntaxException: Illegal character in opaque part` | The prompt is in the adapter's Input field. Set Input to `${body}` only and keep the prompt in Build Prompt (Step 6). | +| `403 Forbidden` while retrieving the access token | The credential `AICore-resolution` is set up incorrectly. Tell a Developer Advocate; you can't fix it from your side. | +| `400 Bad Request` with `Missing Tenant Id` | Likely cause (unverified): the token and the address come from different SAP AI Core instances. Check that the Address and Orchestration Deployment ID match the values in Step 6. If they do, tell a Developer Advocate that `AICore-resolution` and the Address may come from different instances. | +| Deployment fails: `URISyntaxException: Illegal character in opaque part` | The prompt is in the adapter's Input field. Set Input to `${body}` only and keep the prompt in Build Prompt (Step 5). | | `404` from the AI Adapter | The deployment ID and resource group don't match. Both must belong to the same orchestration deployment. | -| `401` from the AI Adapter | Check the client ID and secret in the Security Material come from the same service key. | -| Model not found | That model isn't enabled for orchestration. Ask your instructor which models are available. | +| `401` from the AI Adapter | The credential `AICore-resolution` is rejected. Tell a Developer Advocate. | +| Model not found | That model isn't enabled for orchestration. Ask a Developer Advocate which models are available. | | The AI answers "insufficient information" with confidence LOW | The AI never received the error. Set the Build Prompt body Type to **Expression**. The Debug AI Output script shows exactly what was sent. | | The error message appears three times | Cloud Integration wraps script errors. Use the provided Capture Error script, which reads only the innermost exception. | -| `OrderDate` shows `[PHONE]` in the AI input | An older masking pattern matched ISO dates. Use the Capture Error script from Step 5. | -| AI Resolution is missing; only AI Diagnosis appears | An older Attach AI Diagnosis script is deployed. Paste the script from Step 8 and deploy again. | -| AI Diagnosis says "AI diagnosis unavailable (UNAVAILABLE)" | The AI call failed and Exception Subprocess 2 caught it. Check `aiError` in the Debug AI Output attachment for the reason. | -| `AI_Status = UNSTRUCTURED` | The model didn't return the expected JSON. Check the prompt in Build Prompt is complete, then compare with the raw response in Debug AI Output. | +| `OrderDate` shows `[PHONE]` in the AI input | An older masking pattern matched ISO dates. Use the Capture Error script from Step 4. | +| AI Resolution is missing; only AI Diagnosis appears, and `AI_Status` is `OK` | An older Attach AI Diagnosis script is deployed. Paste the script from Step 7 and deploy again. | +| AI Resolution is missing; only AI Diagnosis appears, and `AI_Status` is `UNSTRUCTURED` | The model didn't return the expected JSON, so the script writes no AI Resolution. The raw answer is in the AI Diagnosis attachment and in the message body. Check the prompt in Build Prompt is complete, then compare with the raw response in Debug AI Output. | +| `AI_Status = CONTEXT_FAILED` | Capture Error failed, so the AI had no error to diagnose. The reason is in the AI Diagnosis attachment. Paste the Capture Error script from Step 4 again and deploy. | +| `AI_Status = REPORT_FAILED` | Attach AI Diagnosis itself failed. The AI Diagnosis attachment contains the script's error message. Paste the script from Step 7 again and deploy. | +| The message shows Failed and the error comes from the AI Adapter (for example 401, 403, 404, or a timeout) | The AI call failed inside the error handler. Expected result (unverified, not tested for this tutorial): no resolution is written and no `AI_` header properties are set. Fix the cause using the rows above, then deploy again. | | The message shows Failed instead of Completed | Exception Subprocess 1 ends with an Error End. Replace it with an **End Message** event. | -| Message Content shows raw JSON, not the resolution | Message Content is the message body, which still holds the adapter response. The resolution is under **Attachments → AI Resolution**. | +| Message Content shows raw JSON, not the resolution | You selected a step before **Attach AI Diagnosis**, so the body still holds the adapter response. Select **End** and open **Payload**, or read **Attachments → AI Resolution**. | | My message never appears | The timer runs once per deployment. Deploy again, and check the flow is **Started** in Manage Integration Content. | #### Try It Yourself @@ -698,7 +709,7 @@ Change the payload in **Set Order Payload** and deploy again to compare the diag Then try these: - In the Monitor, filter by the custom header property `AI_Category`. -- Change the client secret in your Security Material to a wrong value and deploy. AI Diagnosis says the diagnosis is unavailable, while the original error is kept. Then restore the secret. +- Set the log level to **Trace** and compare the body before and after **Attach AI Diagnosis**: the adapter's raw JSON becomes the readable resolution. - Remove the Build Prompt rule that asks for 2 to 4 fix steps and compare the resolution. You built a flow that catches an integration error, masks it, asks SAP AI Core for a diagnosis through the AI Adapter, and attaches a readable resolution to the message. diff --git a/tutorials/dev-garage-error-resolution-ai-adapter/files/AttachDiagnosis.groovy b/tutorials/dev-garage-error-resolution-ai-adapter/files/AttachDiagnosis.groovy index 2c43d51..df70675 100644 --- a/tutorials/dev-garage-error-resolution-ai-adapter/files/AttachDiagnosis.groovy +++ b/tutorials/dev-garage-error-resolution-ai-adapter/files/AttachDiagnosis.groovy @@ -1,14 +1,16 @@ /* * AttachDiagnosis.groovy - * Exception subprocess 1 - runs after the "Get AI Diagnosis" process call. + * Exception subprocess 1 - runs after the AI Adapter's Request Reply. * Reads the AI response, parses the JSON diagnosis, and writes: + * - MPL attachment "AI Resolution" (the problem and numbered fix steps) * - MPL attachment "AI Diagnosis" (human-readable report) * - custom header properties AI_Status, AI_Category, AI_Confidence, AI_NeedsHuman + * - the message body: the resolution text followed by the original payload (personal data masked) * * Handles three cases: - * 1. aiStatus = UNAVAILABLE / CONTEXT_FAILED -> attaches a "diagnosis unavailable" note - * 2. Response is (or contains) valid diagnosis JSON -> formatted report - * 3. Response is not parseable -> attaches raw response marked "unstructured" + * 1. aiStatus = CONTEXT_FAILED (Capture Error failed) -> attaches a "diagnosis unavailable" note + * 2. Response is (or contains) valid diagnosis JSON -> formatted report + * 3. Response is not parseable -> attaches raw response marked "unstructured" * * The response parser accepts either the model's text directly, or a full * orchestration response (final_result / orchestration_result / choices[0].message.content). @@ -36,59 +38,70 @@ def Message processData(Message message) { r << "Error class : " << origClass << "\n" r << "Error : " << origMsg << "\n\n" - // Case 1 - AI not called or AI call failed + String resolutionText = null + if (status != "OK") { + // Case 1 - Capture Error failed, so the AI had no error to diagnose r << "AI diagnosis unavailable (" << status << ").\n" - r << "Reason: " << (message.getProperty("aiError") ?: message.getProperty("contextError") ?: "-") << "\n" + r << "Reason: " << (message.getProperty("contextError") ?: "-") << "\n" r << "The original error above is unaffected - investigate manually.\n" log?.addAttachmentAsString("AI Diagnosis", r.toString(), "text/plain") log?.addCustomHeaderProperty("AI_Status", status) - return message - } + resolutionText = r.toString() + } else { + String body = message.getBody(String) ?: "" + String content = extractContent(body) + def diag = parseJson(content) - String body = message.getBody(String) ?: "" - String content = extractContent(body) - def diag = parseJson(content) + if (diag instanceof Map && diag.root_cause) { + // Case 2 - structured diagnosis + String category = (diag.category ?: "OTHER").toString() + String confidence = (diag.confidence ?: "LOW").toString() + String needsHuman = (diag.needs_human == null ? "true" : diag.needs_human.toString()) - if (diag instanceof Map && diag.root_cause) { - // Case 2 - structured diagnosis - String category = (diag.category ?: "OTHER").toString() - String confidence = (diag.confidence ?: "LOW").toString() - String needsHuman = (diag.needs_human == null ? "true" : diag.needs_human.toString()) + r << "Root cause : " << diag.root_cause << "\n" + r << "Category : " << category << "\n" + r << "Confidence : " << confidence << "\n" + r << "Needs human : " << needsHuman << "\n\n" + r << "Suggested fix steps:\n" + def steps = (diag.fix_steps instanceof List) ? diag.fix_steps : [diag.fix_steps ?: "-"] + steps.eachWithIndex { s, i -> r << " " << (i + 1) << ". " << s << "\n" } - r << "Root cause : " << diag.root_cause << "\n" - r << "Category : " << category << "\n" - r << "Confidence : " << confidence << "\n" - r << "Needs human : " << needsHuman << "\n\n" - r << "Suggested fix steps:\n" - def steps = (diag.fix_steps instanceof List) ? diag.fix_steps : [diag.fix_steps ?: "-"] - steps.eachWithIndex { s, i -> r << " " << (i + 1) << ". " << s << "\n" } + // Separate, resolution-only attachment for clean viewing + StringBuilder res = new StringBuilder() + res << "RESOLUTION\n" + res << "==========\n\n" + res << "Problem:\n " << diag.root_cause << "\n\n" + res << "What to do:\n" + steps.eachWithIndex { s, i -> res << " " << (i + 1) << ". " << s << "\n" } + res << "\nCategory: " << category << " | Confidence: " << confidence + res << " | Needs human: " << needsHuman << "\n" + res << "\n---\nAI-generated suggestion - verify before acting.\n" + log?.addAttachmentAsString("AI Resolution", res.toString(), "text/plain") + resolutionText = res.toString() - // Separate, resolution-only attachment for clean viewing - StringBuilder res = new StringBuilder() - res << "RESOLUTION\n" - res << "==========\n\n" - res << "Problem:\n " << diag.root_cause << "\n\n" - res << "What to do:\n" - steps.eachWithIndex { s, i -> res << " " << (i + 1) << ". " << s << "\n" } - res << "\nCategory: " << category << " | Confidence: " << confidence - res << " | Needs human: " << needsHuman << "\n" - res << "\n---\nAI-generated suggestion - verify before acting.\n" - log?.addAttachmentAsString("AI Resolution", res.toString(), "text/plain") + log?.addCustomHeaderProperty("AI_Status", "OK") + log?.addCustomHeaderProperty("AI_Category", category) + log?.addCustomHeaderProperty("AI_Confidence", confidence) + log?.addCustomHeaderProperty("AI_NeedsHuman", needsHuman) + } else { + // Case 3 - model did not return the expected JSON + r << "AI response (unstructured - did not match expected JSON):\n" + r << (content ?: body) << "\n" + log?.addCustomHeaderProperty("AI_Status", "UNSTRUCTURED") + } - log?.addCustomHeaderProperty("AI_Status", "OK") - log?.addCustomHeaderProperty("AI_Category", category) - log?.addCustomHeaderProperty("AI_Confidence", confidence) - log?.addCustomHeaderProperty("AI_NeedsHuman", needsHuman) - } else { - // Case 3 - model did not return the expected JSON - r << "AI response (unstructured - did not match expected JSON):\n" - r << (content ?: body) << "\n" - log?.addCustomHeaderProperty("AI_Status", "UNSTRUCTURED") + r << "\n---\nAI-generated suggestion - verify before acting.\n" + log?.addAttachmentAsString("AI Diagnosis", r.toString(), "text/plain") + if (resolutionText == null) resolutionText = r.toString() } - r << "\n---\nAI-generated suggestion - verify before acting.\n" - log?.addAttachmentAsString("AI Diagnosis", r.toString(), "text/plain") + // Message body: the resolution, then the original payload (masked), so both show in the Monitor + StringBuilder out = new StringBuilder(resolutionText) + out << "\nORIGINAL PAYLOAD (personal data masked)\n" + out << "=======================================\n" + out << maskedPayload(message) << "\n" + message.setBody(out.toString()) } catch (Exception e) { log?.addAttachmentAsString("AI Diagnosis", "Could not build AI diagnosis report: " + e.getMessage(), "text/plain") @@ -97,6 +110,16 @@ def Message processData(Message message) { return message } +// The masked payload that Capture Error saved inside the aiErrorContext property +String maskedPayload(Message message) { + try { + def ctx = new JsonSlurper().parseText(message.getProperty("aiErrorContext") as String) + return (ctx.payloadExtract ?: "-").toString() + } catch (ignored) { + return "-" + } +} + // Pull the model's text out of the response, whatever shape the adapter returns String extractContent(String body) { try { @@ -127,4 +150,4 @@ def parseJson(String s) { } catch (ignored) { return null } -} \ No newline at end of file +} diff --git a/tutorials/dev-garage-error-resolution-ai-adapter/files/DebugAIOutput.groovy b/tutorials/dev-garage-error-resolution-ai-adapter/files/DebugAIOutput.groovy index 4db7943..2a10e38 100644 --- a/tutorials/dev-garage-error-resolution-ai-adapter/files/DebugAIOutput.groovy +++ b/tutorials/dev-garage-error-resolution-ai-adapter/files/DebugAIOutput.groovy @@ -2,10 +2,7 @@ import com.sap.gateway.ip.core.customdev.util.Message def Message processData(Message message) { def log = messageLogFactory.getMessageLog(message) - String status = message.getProperty("aiStatus") ?: "OK" - String report = "aiStatus: " + status + "\n" + - "aiError: " + (message.getProperty("aiError") ?: "-") + "\n\n" + - "AI response body:\n" + (message.getBody(String) ?: "") + String report = "AI response body:\n" + (message.getBody(String) ?: "") log?.addAttachmentAsString("AI output (debug)", report, "text/plain") return message -} \ No newline at end of file +} diff --git a/tutorials/dev-garage-error-resolution-ai-adapter/files/SalesOrder.xsd b/tutorials/dev-garage-error-resolution-ai-adapter/files/SalesOrder.xsd deleted file mode 100644 index 9eab1b4..0000000 --- a/tutorials/dev-garage-error-resolution-ai-adapter/files/SalesOrder.xsd +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file