From 292e1305d53f6fe0f16c14812f9bc1ea676bdd68 Mon Sep 17 00:00:00 2001 From: D062408 Date: Fri, 17 Apr 2026 17:01:25 +0200 Subject: [PATCH 1/6] add first version of joule deployment exercise for python a2a app --- exercises/Python/07-deploy-agent-to-cf.md | 1 + .../Python/08-integrate-agent-into-joule.md | 653 ++++++++++++++++++ project/Python/solution/config/agents.yaml | 6 +- project/Python/solution/joule/da.sapdas.yaml | 7 + .../capability.sapdas.yaml | 12 + .../functions/investigate_function.yaml | 10 + .../scenarios/investigate_scenario.yaml | 9 + 7 files changed, 695 insertions(+), 3 deletions(-) create mode 100644 exercises/Python/08-integrate-agent-into-joule.md create mode 100644 project/Python/solution/joule/da.sapdas.yaml create mode 100644 project/Python/solution/joule/investigator_capability/capability.sapdas.yaml create mode 100644 project/Python/solution/joule/investigator_capability/functions/investigate_function.yaml create mode 100644 project/Python/solution/joule/investigator_capability/scenarios/investigate_scenario.yaml diff --git a/exercises/Python/07-deploy-agent-to-cf.md b/exercises/Python/07-deploy-agent-to-cf.md index a6f458c..57528da 100644 --- a/exercises/Python/07-deploy-agent-to-cf.md +++ b/exercises/Python/07-deploy-agent-to-cf.md @@ -530,6 +530,7 @@ CF Router → investigator-crew-a2a (uvicorn / FastAPI) 5. āœ… [Add the Grounding Service](05-add-the-grounding-service.md) 6. āœ… [Solve the crime](06-solve-the-crime.md) 7. āœ… [Deploy your agent to CF with A2A](07-deploy-agent-to-cf.md) (this exercise) +8. šŸ“Œ [Integrate your agent into SAP Joule](08-integrate-agent-into-joule.md) --- diff --git a/exercises/Python/08-integrate-agent-into-joule.md b/exercises/Python/08-integrate-agent-into-joule.md new file mode 100644 index 0000000..e5000ea --- /dev/null +++ b/exercises/Python/08-integrate-agent-into-joule.md @@ -0,0 +1,653 @@ +# Integrate Your Agent into SAP Joule + +## Overview + +Your investigator crew is deployed to Cloud Foundry and speaks A2A. But right now, only developers who know the A2A protocol can talk to it. In this exercise, you'll connect your agent to **SAP Joule** — SAP's AI copilot — so that any business user can interact with it through natural language in the Joule chat interface. + +To do that, you'll create a **Joule capability**: a set of YAML files that tell Joule what your agent can do, how to reach it, and how to display its responses. + +By the end of this exercise, your investigator crew will be: + +- āœ… Discoverable by Joule through a scenario description +- āœ… Callable from the Joule chat via the A2A protocol +- āœ… Deployed as a Joule capability using the Joule Studio CLI + +--- + +## Understand Joule Capabilities + +### What is a Capability? + +A **capability** is a package of skills that you add to Joule. It's defined entirely in YAML — no JavaScript or Python needed. Joule uses these YAML files to understand what your agent can do and how to call it. + +| Concept | What it is | Example | +|---|---|---| +| **Capability** | A group of skills packaged together | `investigator_capability` | +| **Scenario** | A user-facing skill description — Joule matches user questions against it | "Investigate an art theft" | +| **Function** | The executable logic a scenario triggers | Calls the A2A agent endpoint | +| **System Alias** | A named reference to a BTP Destination | `INVESTIGATOR_AGENT_XX` | +| **Digital Assistant** | The top-level config that assembles capabilities | `da.sapdas.yaml` | + +### How the Pieces Fit Together + +```text +User asks: "Investigate the art theft" + │ + ā–¼ +SAP Joule + │ + ā”œā”€ā”€ 1. Scenario Matching + │ Joule reads all scenario descriptions and picks the best match + │ + ā”œā”€ā”€ 2. Function Execution + │ The matched scenario triggers a function + │ + ā”œā”€ā”€ 3. agent-request Action + │ The function calls your A2A agent via a BTP Destination + │ + ā”œā”€ā”€ 4. BTP Destination → Cloud Foundry + │ Routes the request to your deployed app + │ + ā”œā”€ā”€ 5. InvestigatorCrew runs + │ Your CrewAI agents do the investigation + │ + └── 6. Response displayed in Joule chat + The function extracts the result and shows it to the user +``` + +> šŸ’” **Key insight:** Joule doesn't need to know that your agent uses CrewAI, Python, or any specific framework. It only speaks A2A — the same protocol you set up in Exercise 07. The capability YAML is just the bridge between Joule's chat UI and your A2A endpoint. + +--- + +## Install the Joule Studio CLI + +The **Joule Studio CLI** is the command-line tool for building, deploying, and testing Joule capabilities. + +### Step 1: Check Your Node.js Version + +The CLI requires **Node.js v20.12.0 – v24**. + +šŸ‘‰ Check your installed version: + +```bash +node -v +``` + +> āš ļø **If your version is below 20.12.0 or above 24**, download a compatible version from [https://nodejs.org/en/download/releases/](https://nodejs.org/en/download/releases/). + +### Step 2: Install the CLI + +šŸ‘‰ Run the following command to install the Joule Studio CLI globally: + +```bash +npm install -g @sap/joule-studio-cli +``` + +šŸ‘‰ Verify the installation: + +```bash +joule -V +``` + +You should see a version number printed (e.g., `1.0.90`). + +> šŸ’” **What does the CLI do?** It handles the full lifecycle of Joule capabilities: linting YAML files for errors, compiling them into deployable archives (`.daar` files), deploying to your Joule instance, and launching a test client. Think of it as `cf push` but for Joule. + +--- + +## Log In to Joule + +Before you can deploy anything, the CLI needs to authenticate against your Joule instance. + +> šŸ’” **For this CodeJam**, your instructor will provide the login credentials. You do **not** need to set up IAS applications or client secrets yourself. + +### Step 1: Get Your Credentials + +šŸ‘‰ Your instructor will provide the following values: + +| Field | Description | Example | +|---|---|---| +| **Authentication URL** | Your IAS tenant URL | `https://.accounts.ondemand.com` | +| **API URL** | The Joule API endpoint | `https://..sapdas.cloud.sap` | +| **Client ID** | OAuth client identifier | `abc123-...` | +| **Client Secret** | OAuth client secret | (provided by instructor) | +| **Username** | Your BTP user email | `your.email@example.com` | +| **Password** | Your BTP user password | (your password) | + +### Step 2: Log In + +šŸ‘‰ Run the login command and enter the values when prompted: + +```bash +joule login +``` + +The CLI will prompt you interactively: + +```text +āœ” Authentication URL: https://.accounts.ondemand.com +āœ” API URL: https://..sapdas.cloud.sap +āœ” Instance Client ID: +āœ” Instance Client Secret: +āœ” Username: your.email@example.com +āœ” Password: ******** + +API URL: https://..sapdas.cloud.sap +You are logged in as your.email@example.com +``` + +### Step 3: Verify + +šŸ‘‰ Confirm your session is active: + +```bash +joule status +``` + +> šŸ’” **Tip:** Your login session will expire after some time. If a later command fails with an authentication error, simply run `joule login` again. + +--- + +## Create the BTP Destination + +The `agent-request` action in your Joule function doesn't call your CF app directly. Instead, it goes through a **BTP Destination** — a named HTTP endpoint registered in the SAP BTP cockpit. This decouples your capability YAML from the physical URL of your agent. + +### Step 1: Open the BTP Cockpit + +šŸ‘‰ Navigate to your **BTP Subaccount** → **Connectivity** → **Destinations**. + +### Step 2: Create a New Destination + +šŸ‘‰ Click **New Destination** and fill in the following values: + +| Field | Value | +|---|---| +| **Name** | `INVESTIGATOR_AGENT_XX` | +| **Type** | HTTP | +| **URL** | `https://investigator-crew-a2a-.cfapps.eu10-004.hana.ondemand.com` | +| **Proxy Type** | Internet | +| **Authentication** | NoAuthentication | + +> āš ļø **Replace `XX` with your participant number** (e.g., `INVESTIGATOR_AGENT_01`, `INVESTIGATOR_AGENT_02`). This ensures each participant has a unique destination. You'll use this exact name in all YAML files below. + +> āš ļø **Replace ``** with the actual route of your CF app from Exercise 07. You can find it by running `cf app investigator-crew-a2a` or by checking the URL you noted at the end of Exercise 07. + +šŸ‘‰ Click **Save**. + +> šŸ’” **How does Joule use this destination?** +> +> When Joule triggers an `agent-request`, it: +> 1. Looks up the **system alias** in your capability YAML +> 2. Resolves it to the **BTP Destination** name +> 3. Uses the destination's URL to call `GET /.well-known/agent-card.json` on your agent +> 4. Reads the Agent Card to discover the communication endpoint and protocol +> 5. Sends an A2A `message/send` request with the user's message +> +> This is the same discovery flow you tested manually with `curl` in Exercise 07 — now Joule does it automatically. + +--- + +## Create the Joule Capability + +Now you'll create the YAML files that define your Joule capability. These files tell Joule: +- **What** your agent can do (scenario) +- **How** to call it (function with `agent-request`) +- **Where** to find it (system alias → BTP Destination) + +### Project Structure + +The complete directory structure you'll create: + +```text +/project/Python/starter-project/joule/ +ā”œā”€ā”€ investigator_capability/ +│ ā”œā”€ā”€ functions/ +│ │ └── investigate_function.yaml +│ ā”œā”€ā”€ scenarios/ +│ │ └── investigate_scenario.yaml +│ └── capability.sapdas.yaml +└── da.sapdas.yaml +``` + +### Step 1: Create the Directory Structure + +šŸ‘‰ From your terminal, create the directories: + +```bash +# macOS / Linux +mkdir -p project/Python/starter-project/joule/investigator_capability/functions +mkdir -p project/Python/starter-project/joule/investigator_capability/scenarios +``` + +```powershell +# Windows (PowerShell) +New-Item -ItemType Directory -Path project\Python\starter-project\joule\investigator_capability\functions -Force +New-Item -ItemType Directory -Path project\Python\starter-project\joule\investigator_capability\scenarios -Force +``` + +### Step 2: Create capability.sapdas.yaml + +This is the root configuration file for your capability. It defines metadata, the schema version, and the system aliases that map to BTP Destinations. + +šŸ‘‰ Create a new file [`/project/Python/starter-project/joule/investigator_capability/capability.sapdas.yaml`](/project/Python/starter-project/joule/investigator_capability/capability.sapdas.yaml): + +```yaml +schema_version: 3.28.0 + +metadata: + namespace: joule.ext + name: investigator_capability + version: 1.0.0 + display_name: Investigator_Capability + description: Capability containing the investigator crew agent for art theft investigation + +system_aliases: + INVESTIGATOR_AGENT_XX: + destination: INVESTIGATOR_AGENT_XX +``` + +> āš ļø **Replace `XX`** with your participant number in both the alias key and the destination value. These must match the BTP Destination name you created in the previous section. + +> šŸ’” **Understanding each field:** +> +> | Field | Purpose | +> |---|---| +> | `schema_version: 3.28.0` | Minimum schema version that supports code-based agents via `agent-request`. Using a lower version will cause a compile error. | +> | `namespace: joule.ext` | **Required** for all custom capabilities. Any other namespace is treated as an SAP-internal namespace and will fail deployment. | +> | `name` | Internal identifier for the capability. Must be unique within your Joule instance. | +> | `display_name` | Human-readable name shown in Joule Studio. Alphanumeric + underscore + hyphen only. | +> | `description` | Short description of what this capability does (max 512 chars). | +> | `system_aliases` | Maps a logical name (used in function YAMLs) to a BTP Destination name. The key (`INVESTIGATOR_AGENT_XX`) is referenced by the `system_alias` field in the function. | + +### Step 3: Create the Scenario + +The scenario is what Joule uses to match user requests to your capability. When a user types a question, Joule compares it against all scenario descriptions and picks the best match. + +šŸ‘‰ Create a new file [`/project/Python/starter-project/joule/investigator_capability/scenarios/investigate_scenario.yaml`](/project/Python/starter-project/joule/investigator_capability/scenarios/investigate_scenario.yaml): + +```yaml +description: >- + This skill investigates art theft cases by appraising the value of stolen + items, analyzing evidence from security logs, bank records, phone records, + and criminal histories, and identifying the most likely suspect based on + available evidence. It coordinates multiple specialist agents to deliver + a comprehensive investigation report. +target: + name: investigate_function + type: function +``` + +> šŸ’” **Understanding the scenario:** +> +> | Field | Purpose | +> |---|---| +> | `description` | The text Joule uses for intent matching. Write it like you're explaining the skill to a colleague — specific, clear, keyword-rich. Joule's language model evaluates this against the user's message to decide whether to activate this scenario. | +> | `target.name` | References the function YAML file name (without `.yaml`). When this scenario is triggered, Joule runs this function. | +> | `target.type: function` | Currently the only supported target type. Tells Joule to execute a dialog function. | +> +> **Writing good descriptions matters.** If the description is too vague ("helps with investigations"), Joule may not match it reliably. If it's too narrow ("only for the Grand Museum heist"), it won't match variations. The description above includes multiple keywords: "art theft", "stolen items", "evidence", "security logs", "suspect", "investigation report". + +### Step 4: Create the Function + +The function is where the action happens. It defines the `agent-request` action that calls your A2A agent and a `message` action that displays the result. + +šŸ‘‰ Create a new file [`/project/Python/starter-project/joule/investigator_capability/functions/investigate_function.yaml`](/project/Python/starter-project/joule/investigator_capability/functions/investigate_function.yaml): + +```yaml +action_groups: + - actions: + - type: agent-request + system_alias: INVESTIGATOR_AGENT_XX + agent_type: remote + result_variable: "apiResponse" + - type: message + message: + type: text + content: "" +``` + +> āš ļø **Replace `XX`** with your participant number in `system_alias`. This must match the key you defined in `capability.sapdas.yaml`. + +> šŸ’” **Understanding the `agent-request` action:** +> +> | Field | Purpose | +> |---|---| +> | `type: agent-request` | Tells Joule to call an external agent using the A2A protocol (available since DTA schema v3.28.0). | +> | `system_alias: INVESTIGATOR_AGENT_XX` | References the system alias from `capability.sapdas.yaml`, which maps to the BTP Destination pointing to your CF app. | +> | `agent_type: remote` | Specifies this is a **code-based** (Bring Your Own Agent) agent hosted outside of Joule. Use `local` for content-based agents built inside the Joule framework. | +> | `result_variable: "apiResponse"` | Stores the full A2A response in a variable. You access it in subsequent actions using SpEL expressions. | + +> šŸ’” **Understanding the `message` action:** +> +> | Field | Purpose | +> |---|---| +> | `type: message` | Sends a message back to the user in the Joule chat. | +> | `type: text` | The message format. Joule also supports `card`, `list`, `carousel`, and other rich formats. | +> | `content` | The message text. The `` delimiters indicate a **SpEL expression** — Joule's scripting language for extracting values from variables. | + +### Understanding the A2A Response + +When the `agent-request` action calls your CF app, it sends the user's message via the A2A protocol and receives a response. The response is stored in `apiResponse` and has this structure: + +```json +{ + "headers": {}, + "body": { + "kind": "task", + "contextId": "abc-123-def-456", + "history": [ + { + "role": "user", + "kind": "message", + "parts": [{ "kind": "text", "text": "Investigate the art theft..." }] + } + ], + "artifacts": [ + { + "name": "investigation_result", + "parts": [ + { + "kind": "text", + "text": "Based on the investigation, the primary suspect is..." + } + ], + "artifactId": "investigation_result" + } + ], + "status": { + "state": "completed" + } + } +} +``` + +The expression `apiResponse.body.artifacts[0].parts[0].text` walks this JSON path: + +| Path segment | What it accesses | +|---|---| +| `apiResponse` | The full response variable | +| `.body` | The A2A task response body | +| `.artifacts[0]` | The first artifact — your `investigation_result` from `server.py` | +| `.parts[0]` | The first part of that artifact | +| `.text` | The actual text content of the investigation result | + +> šŸ’” **This maps directly to what you built in Exercise 07.** In `server.py`, your `InvestigatorExecutor` emits a `TaskArtifactUpdateEvent` with `artifactId="investigation_result"` and a `TextPart`. That's exactly what Joule reads here through `artifacts[0].parts[0].text`. + +### Step 5: Create the Digital Assistant Descriptor + +The `da.sapdas.yaml` file is the top-level manifest. It tells the Joule CLI which capabilities to include when compiling and deploying. + +šŸ‘‰ Create a new file [`/project/Python/starter-project/joule/da.sapdas.yaml`](/project/Python/starter-project/joule/da.sapdas.yaml): + +```yaml +schema_version: 1.4.0 + +name: investigator_assistant + +capabilities: + - type: local + folder: ./investigator_capability +``` + +> šŸ’” **Understanding the DA file:** +> +> | Field | Purpose | +> |---|---| +> | `schema_version: 1.4.0` | Version of the Digital Assistant schema (separate from the capability schema). | +> | `name` | The name of the assistant. Used when deploying with `joule deploy -n`. | +> | `capabilities` | List of capabilities to include. `type: local` means the capability is in a local folder (as opposed to a remote reference). | +> | `folder` | Path to the capability directory, relative to this file. | + +--- + +## Deploy the Capability + +With all YAML files in place, you can now compile and deploy the capability to your Joule instance. + +### Step 1: Navigate to the Joule Directory + +šŸ‘‰ Change to the directory containing your `da.sapdas.yaml`: + +```bash +cd project/Python/starter-project/joule +``` + +### Step 2: Compile and Deploy + +šŸ‘‰ Run the following command to compile the YAML files and deploy them as a test assistant: + +```bash +joule deploy -c -n "investigator_test_XX" +``` + +> āš ļø **Replace `XX`** with your participant number (e.g., `investigator_test_01`). + +> šŸ’” **Understanding the flags:** +> +> | Flag | Purpose | +> |---|---| +> | `-c` | Compile before deploying. This validates your YAML files against the schema, checks for errors, and packages them into a `.daar` archive (Design-time Artifact Archive). | +> | `-n "investigator_test_XX"` | Name of the test assistant to create. Using `-n` creates a standalone assistant for testing — it doesn't affect the production Joule instance. | + +You should see output similar to: + +```text +āœ” Building designtime artifact (investigator_capability) +āœ” Trigger compilation +āœ” Compiled + +Detailed logs: +WARNINGS: +Message: DTA did not define an optional i18n folder +Path: +Category: I18N +Severity: LOW + +āœ” Downloaded runtime artifact (joule.ext_investigator_capability_1.0.0.daar) +āœ” Building runtime artifact + > joule.ext_investigator_capability_1.0.0.daar added to the RTA +āœ” Triggering deployment (investigator_test_XX) +āœ” Your digital assistant (investigator_test_XX) deployed successfully +``` + +> šŸ’” **The i18n warning is expected** — it just means you haven't added localization files, which are optional for this exercise. + +### Step 3: Verify Deployment + +šŸ‘‰ List your deployed assistants to confirm: + +```bash +joule list +``` + +You should see `investigator_test_XX` in the list. + +--- + +## Test in Joule + +### Step 1: Launch the Test Client + +šŸ‘‰ Open the Joule web client for your test assistant: + +```bash +joule launch "investigator_test_XX" +``` + +This opens a browser tab with the Joule chat interface connected to your test assistant. + +### Step 2: Send a Test Message + +šŸ‘‰ In the Joule chat, type: + +```text +Investigate the art theft at the museum. The suspects are Sophie Dubois, Marcus Chen, and Viktor Petrov. +``` + +> āš ļø **The investigation takes 1–2 minutes** because the full CrewAI pipeline runs on Cloud Foundry: the Appraiser calls RPT-1, the Evidence Analyst queries the Grounding Service, and the Lead Detective synthesizes the findings. Joule's synchronous timeout is 60 seconds — if your crew takes longer, you may see a timeout error. If this happens, try a simpler request or check the troubleshooting section below. + +### Step 3: Review the Response + +If everything is connected correctly, Joule displays the investigation result directly in the chat — the same markdown report your crew generates, now accessible through a conversational interface. + +> šŸ’” **What just happened behind the scenes:** +> 1. You typed a question in Joule +> 2. Joule matched your question to `investigate_scenario` based on the description +> 3. The scenario triggered `investigate_function` +> 4. The function's `agent-request` action called your BTP Destination +> 5. The destination routed to your CF app's A2A endpoint +> 6. Your `InvestigatorExecutor` ran the full CrewAI crew +> 7. The result came back as an A2A artifact +> 8. The function's `message` action extracted the text and displayed it in Joule + +--- + +## Understanding What Just Happened + +### The Full Architecture + +You now have a complete end-to-end pipeline from Joule's chat UI to your multi-agent CrewAI system: + +```text +User + │ + ā–¼ +SAP Joule (Chat UI) + │ + ā”œā”€ā”€ Scenario Match → investigate_scenario + │ │ + │ ā–¼ + │ investigate_function + │ │ + │ ā”œā”€ā”€ agent-request (A2A over HTTPS) + │ │ │ + │ │ ā–¼ + │ │ BTP Destination: INVESTIGATOR_AGENT_XX + │ │ │ + │ │ ā–¼ + │ │ CF App: investigator-crew-a2a + │ │ │ + │ │ ā–¼ + │ │ InvestigatorExecutor → InvestigatorCrew + │ │ ā”œā”€ā”€ Appraiser Agent (RPT-1) + │ │ ā”œā”€ā”€ Evidence Analyst (Grounding) + │ │ └── Lead Detective (GPT-4o) + │ │ │ + │ │ ā–¼ + │ │ A2A Response (artifacts[0].parts[0].text) + │ │ + │ ā”œā”€ā”€ message → Displays result to user + │ │ + ā–¼ ā–¼ +User sees investigation result in Joule chat +``` + +### How Each Layer Communicates + +| Step | Component | Protocol | What happens | +|---|---|---|---| +| 1 | User → Joule | Chat UI | User types a question in natural language | +| 2 | Joule → Scenario | Internal | Joule matches the question to the best scenario description | +| 3 | Scenario → Function | Internal | The matched scenario triggers the linked function | +| 4 | Function → Destination | HTTP | The `agent-request` action resolves the system alias to a BTP Destination | +| 5 | Destination → CF App | HTTPS | The destination routes to your Cloud Foundry app URL | +| 6 | CF App → CrewAI | Internal | The `InvestigatorExecutor` runs `InvestigatorCrew().crew().kickoff()` | +| 7 | CrewAI → Response | A2A JSON-RPC | The result is returned as an A2A `TaskArtifactUpdateEvent` | +| 8 | Function → User | Chat UI | The `message` action extracts the text and shows it in Joule | + +--- + +## Key Takeaways + +- **Joule capabilities** are YAML-based packages that extend Joule with custom skills — no runtime code needed on the Joule side +- **`agent-request`** is the action type that bridges Joule to external agents via the A2A protocol +- **`agent_type: remote`** distinguishes code-based agents (your CF app) from content-based agents built inside Joule +- **System aliases** decouple the capability from the physical URL — the BTP Destination handles routing, so you can change the URL without redeploying the capability +- **`schema_version: 3.28.0`** is the minimum DTA version required for code-based agent support +- **The Joule Studio CLI** provides a complete workflow: `login` → `deploy` → `launch` → `update` +- **The A2A protocol** enables framework-agnostic integration — Joule doesn't care whether your agent uses CrewAI, LangChain, LangGraph, or any other framework + +--- + +## Next Steps + +1. āœ… [Set up your development space](01-setup-dev-space.md) +2. āœ… [Build a basic agent](02-build-a-basic-agent.md) +3. āœ… [Add custom tools](03-add-your-first-tool.md) +4. āœ… [Build a multi-agent system](04-building-multi-agent-system.md) +5. āœ… [Add the Grounding Service](05-add-the-grounding-service.md) +6. āœ… [Solve the crime](06-solve-the-crime.md) +7. āœ… [Deploy your agent to CF with A2A](07-deploy-agent-to-cf.md) +8. āœ… [Integrate your agent into SAP Joule](08-integrate-agent-into-joule.md) (this exercise) + +šŸŽ‰ **Congratulations!** You've completed the full CodeJam. You built a multi-agent AI system from scratch using CrewAI, deployed it to Cloud Foundry as an A2A server, and integrated it into SAP Joule — making it accessible to business users through natural language. + +--- + +## Troubleshooting + +**Issue**: `joule: command not found` after installing + +- **Solution**: The global npm bin directory may not be in your PATH. Run `npm config get prefix` — the result + `/bin` should be in your PATH. On macOS/Linux, add it to your shell profile (e.g., `export PATH="$(npm config get prefix)/bin:$PATH"`). + +**Issue**: `joule login` fails with an authentication error + +- **Solution**: Double-check all credentials with your instructor. Common mistakes: + - Trailing spaces in the Auth URL or API URL + - Wrong Client ID / Client Secret pair + - Password with special characters that need escaping (`$` → `\$`) + - Expired or incorrect IAS credentials + +**Issue**: `joule deploy` fails with a schema version error + +- **Solution**: Ensure `schema_version: 3.28.0` in `capability.sapdas.yaml`. The `agent-request` action type requires this minimum version. Lower versions like `3.26.0` will not recognize the action. + +**Issue**: `joule deploy` fails with "User is not authorized to deploy sap capabilities" + +- **Solution**: Your `namespace` must be `joule.ext`. Any other namespace (e.g., `joule.custom`, `my.namespace`) is treated as an SAP-internal namespace and requires special permissions. + +**Issue**: Deployment fails with "destination not found" or agent-request returns an error + +- **Solution**: Verify that: + 1. The BTP Destination name (`INVESTIGATOR_AGENT_XX`) matches **exactly** the `system_aliases` key in `capability.sapdas.yaml` + 2. The destination exists in the BTP cockpit under Connectivity → Destinations + 3. The destination URL is correct and your CF app is running (`cf app investigator-crew-a2a`) + +**Issue**: Joule responds with an empty message or an error + +- **Solution**: Your CF app may have crashed or returned an unexpected response. Check: + - App status: `cf app investigator-crew-a2a` + - App logs: `cf logs investigator-crew-a2a --recent` + - Health endpoint: `curl https:///health` + - Ensure the Agent Card is accessible: `curl https:///.well-known/agent-card.json` + +**Issue**: "Scenario not matched" — Joule doesn't recognize your question + +- **Solution**: Joule's intent matching depends on the scenario description. Try rephrasing your question to include keywords from the description: "investigate", "art theft", "stolen items", "suspects", "evidence". If testing with a standalone assistant, ensure the capability was deployed successfully with `joule list`. + +**Issue**: Timeout error — the agent takes too long to respond + +- **Solution**: Joule expects a synchronous response within **60 seconds**. The full CrewAI crew may take longer. Options: + - Try a simpler request that requires less processing + - Check `cf logs` to see where time is spent + - Ensure your CF app has enough memory (1024M in `manifest.yml`) + - Consider reducing the number of grounding service queries in the Evidence Analyst's task + +**Issue**: YAML indentation or compile errors + +- **Solution**: YAML is whitespace-sensitive — use **2 spaces** for indentation, never tabs. Common mistakes: + - Mixed tabs and spaces + - Missing space after `:` in key-value pairs + - Incorrect nesting of `actions` under `action_groups` + - Run `joule lint` to check for errors before deploying + +--- + +## Resources + +- [Joule Development Guide](https://help.sap.com/docs/joule/joule-development-guide-ba88d1ec6a1b442098863d577c19b0c0/joule-development) +- [Code-Based Agents (Bring Your Own Agent)](https://help.sap.com/docs/joule/joule-development-guide-ba88d1ec6a1b442098863d577c19b0c0/code-based-agents-bring-your-own-agent) +- [Install and Update the Joule Studio CLI](https://help.sap.com/docs/joule/joule-development-guide-ba88d1ec6a1b442098863d577c19b0c0/install-and-update-joule-studio-cli) +- [A2A Protocol Specification](https://a2a-protocol.org/latest/) +- [SAP BTP Destinations Documentation](https://help.sap.com/docs/connectivity/sap-btp-connectivity-cf/create-http-destinations) +- [Blog: Joule A2A — Connect Code-Based Agents into Joule](https://community.sap.com/t5/technology-blog-posts-by-sap/joule-a2a-connect-code-based-agents-into-joule/ba-p/14329279) diff --git a/project/Python/solution/config/agents.yaml b/project/Python/solution/config/agents.yaml index 52c8281..f8b653c 100644 --- a/project/Python/solution/config/agents.yaml +++ b/project/Python/solution/config/agents.yaml @@ -6,7 +6,7 @@ appraiser_agent: Do NOT invent or estimate values yourself. If the tool call fails, report the failure. backstory: > You are an insurance appraiser who relies strictly on model predictions. You never guess values. - llm: sap/anthropic--claude-4.5-opus + llm: sap/gpt-4o evidence_analyst_agent: role: > @@ -17,7 +17,7 @@ evidence_analyst_agent: Report only what the tool returns. backstory: > You are a methodical evidence analyst who bases conclusions strictly on retrieved documents. You never assume facts. - llm: sap/anthropic--claude-4.5-opus + llm: sap/gpt-4o lead_detective_agent: role: > @@ -28,6 +28,6 @@ lead_detective_agent: for you to analyze. backstory: > You are a lead detective who makes conclusions only from evidence and appraisals provided by your team. You never speculate. - llm: sap/anthropic--claude-4.5-opus + llm: sap/anthropic--claude-4.5-sonnet \ No newline at end of file diff --git a/project/Python/solution/joule/da.sapdas.yaml b/project/Python/solution/joule/da.sapdas.yaml new file mode 100644 index 0000000..bd8791e --- /dev/null +++ b/project/Python/solution/joule/da.sapdas.yaml @@ -0,0 +1,7 @@ +schema_version: 1.4.0 + +name: investigator_assistant + +capabilities: + - type: local + folder: ./investigator_capability \ No newline at end of file diff --git a/project/Python/solution/joule/investigator_capability/capability.sapdas.yaml b/project/Python/solution/joule/investigator_capability/capability.sapdas.yaml new file mode 100644 index 0000000..01e5a97 --- /dev/null +++ b/project/Python/solution/joule/investigator_capability/capability.sapdas.yaml @@ -0,0 +1,12 @@ +schema_version: 3.28.0 + +metadata: + namespace: joule.ext + name: investigator_capability + version: 1.0.0 + display_name: Investigator_Capability + description: Capability containing the investigator crew agent for art theft investigation + +system_aliases: + INVESTIGATOR_AGENT_XX: + destination: INVESTIGATOR_AGENT \ No newline at end of file diff --git a/project/Python/solution/joule/investigator_capability/functions/investigate_function.yaml b/project/Python/solution/joule/investigator_capability/functions/investigate_function.yaml new file mode 100644 index 0000000..eb2be30 --- /dev/null +++ b/project/Python/solution/joule/investigator_capability/functions/investigate_function.yaml @@ -0,0 +1,10 @@ +action_groups: + - actions: + - type: agent-request + system_alias: INVESTIGATOR_AGENT + agent_type: remote + result_variable: "apiResponse" + - type: message + message: + type: text + content: "" \ No newline at end of file diff --git a/project/Python/solution/joule/investigator_capability/scenarios/investigate_scenario.yaml b/project/Python/solution/joule/investigator_capability/scenarios/investigate_scenario.yaml new file mode 100644 index 0000000..023db82 --- /dev/null +++ b/project/Python/solution/joule/investigator_capability/scenarios/investigate_scenario.yaml @@ -0,0 +1,9 @@ +description: >- + This skill investigates art theft cases by appraising the value of stolen + items, analyzing evidence from security logs, bank records, phone records, + and criminal histories, and identifying the most likely suspect based on + available evidence. It coordinates multiple specialist agents to deliver + a comprehensive investigation report. +target: + name: investigate_function + type: function \ No newline at end of file From 51c3e1ee62c0f86b75ff3fa44b21073a59d8b470 Mon Sep 17 00:00:00 2001 From: D062408 Date: Fri, 17 Apr 2026 18:43:22 +0200 Subject: [PATCH 2/6] improve exercise, add status update for joule capability --- .../Python/08-integrate-agent-into-joule.md | 43 +++++++++++-------- .../capability.sapdas.yaml | 2 +- .../functions/investigate_function.yaml | 3 ++ 3 files changed, 29 insertions(+), 19 deletions(-) diff --git a/exercises/Python/08-integrate-agent-into-joule.md b/exercises/Python/08-integrate-agent-into-joule.md index e5000ea..a32d30e 100644 --- a/exercises/Python/08-integrate-agent-into-joule.md +++ b/exercises/Python/08-integrate-agent-into-joule.md @@ -25,7 +25,7 @@ A **capability** is a package of skills that you add to Joule. It's defined enti | **Capability** | A group of skills packaged together | `investigator_capability` | | **Scenario** | A user-facing skill description — Joule matches user questions against it | "Investigate an art theft" | | **Function** | The executable logic a scenario triggers | Calls the A2A agent endpoint | -| **System Alias** | A named reference to a BTP Destination | `INVESTIGATOR_AGENT_XX` | +| **System Alias** | A named reference to a BTP Destination | `INVESTIGATOR_AGENT` | | **Digital Assistant** | The top-level config that assembles capabilities | `da.sapdas.yaml` | ### How the Pieces Fit Together @@ -225,6 +225,8 @@ New-Item -ItemType Directory -Path project\Python\starter-project\joule\investig New-Item -ItemType Directory -Path project\Python\starter-project\joule\investigator_capability\scenarios -Force ``` +> šŸ’” **In Business Application Studio (BAS):** You can create the folders visually instead. Right-click the `starter-project` folder in the Explorer panel and choose **New Folder**. Create `joule`, then inside it `investigator_capability`, and inside that `functions` and `scenarios`. + ### Step 2: Create capability.sapdas.yaml This is the root configuration file for your capability. It defines metadata, the schema version, and the system aliases that map to BTP Destinations. @@ -242,11 +244,11 @@ metadata: description: Capability containing the investigator crew agent for art theft investigation system_aliases: - INVESTIGATOR_AGENT_XX: + INVESTIGATOR_AGENT: destination: INVESTIGATOR_AGENT_XX ``` -> āš ļø **Replace `XX`** with your participant number in both the alias key and the destination value. These must match the BTP Destination name you created in the previous section. +> āš ļø **Replace `XX`** in `destination` only with your participant number (e.g., `INVESTIGATOR_AGENT_01`). This must match the BTP Destination name you created in the previous section. The system alias key (`INVESTIGATOR_AGENT`) stays the same for everyone — only the destination name is unique per participant. > šŸ’” **Understanding each field:** > @@ -257,7 +259,7 @@ system_aliases: > | `name` | Internal identifier for the capability. Must be unique within your Joule instance. | > | `display_name` | Human-readable name shown in Joule Studio. Alphanumeric + underscore + hyphen only. | > | `description` | Short description of what this capability does (max 512 chars). | -> | `system_aliases` | Maps a logical name (used in function YAMLs) to a BTP Destination name. The key (`INVESTIGATOR_AGENT_XX`) is referenced by the `system_alias` field in the function. | +> | `system_aliases` | Maps a logical name (used in function YAMLs) to a BTP Destination name. The key (`INVESTIGATOR_AGENT`) is referenced by the `system_alias` field in the function. | ### Step 3: Create the Scenario @@ -287,33 +289,36 @@ target: > > **Writing good descriptions matters.** If the description is too vague ("helps with investigations"), Joule may not match it reliably. If it's too narrow ("only for the Grand Museum heist"), it won't match variations. The description above includes multiple keywords: "art theft", "stolen items", "evidence", "security logs", "suspect", "investigation report". +> āš ļø **The `investigate_function` does not exist yet** — you'll create it in the next step. If you run `joule lint` now, it will report that the target function is missing. That is expected. Continue to Step 4 to resolve it. + ### Step 4: Create the Function The function is where the action happens. It defines the `agent-request` action that calls your A2A agent and a `message` action that displays the result. +> šŸ’” **In Business Application Studio (BAS):** Right-click the `functions` folder in the Explorer panel and choose **New File** to create the file below. + šŸ‘‰ Create a new file [`/project/Python/starter-project/joule/investigator_capability/functions/investigate_function.yaml`](/project/Python/starter-project/joule/investigator_capability/functions/investigate_function.yaml): ```yaml action_groups: - actions: - type: agent-request - system_alias: INVESTIGATOR_AGENT_XX + system_alias: INVESTIGATOR_AGENT agent_type: remote result_variable: "apiResponse" - type: message message: type: text + markdown: true content: "" ``` -> āš ļø **Replace `XX`** with your participant number in `system_alias`. This must match the key you defined in `capability.sapdas.yaml`. - > šŸ’” **Understanding the `agent-request` action:** > > | Field | Purpose | > |---|---| > | `type: agent-request` | Tells Joule to call an external agent using the A2A protocol (available since DTA schema v3.28.0). | -> | `system_alias: INVESTIGATOR_AGENT_XX` | References the system alias from `capability.sapdas.yaml`, which maps to the BTP Destination pointing to your CF app. | +> | `system_alias: INVESTIGATOR_AGENT` | References the system alias from `capability.sapdas.yaml`, which maps to your participant-specific BTP Destination. | > | `agent_type: remote` | Specifies this is a **code-based** (Bring Your Own Agent) agent hosted outside of Joule. Use `local` for content-based agents built inside the Joule framework. | > | `result_variable: "apiResponse"` | Stores the full A2A response in a variable. You access it in subsequent actions using SpEL expressions. | @@ -323,6 +328,7 @@ action_groups: > |---|---| > | `type: message` | Sends a message back to the user in the Joule chat. | > | `type: text` | The message format. Joule also supports `card`, `list`, `carousel`, and other rich formats. | +> | `markdown: true` | Tells Joule to render the response as formatted markdown. Without this, Joule displays the raw text including all the `#`, `**`, and `-` characters. With it, headers, bullet points, and bold text are rendered properly — making the investigation report significantly more readable in the chat. | > | `content` | The message text. The `` delimiters indicate a **SpEL expression** — Joule's scripting language for extracting values from variables. | ### Understanding the A2A Response @@ -417,17 +423,17 @@ cd project/Python/starter-project/joule šŸ‘‰ Run the following command to compile the YAML files and deploy them as a test assistant: ```bash -joule deploy -c -n "investigator_test_XX" +joule deploy -c -n "investigator_assistant_XX" ``` -> āš ļø **Replace `XX`** with your participant number (e.g., `investigator_test_01`). +> āš ļø **Replace `XX`** with your participant number (e.g., `investigator_assistant_01`). > šŸ’” **Understanding the flags:** > > | Flag | Purpose | > |---|---| > | `-c` | Compile before deploying. This validates your YAML files against the schema, checks for errors, and packages them into a `.daar` archive (Design-time Artifact Archive). | -> | `-n "investigator_test_XX"` | Name of the test assistant to create. Using `-n` creates a standalone assistant for testing — it doesn't affect the production Joule instance. | +> | `-n "investigator_assistant_XX"` | Name of the test assistant to create. Using `-n` creates a standalone assistant for testing — it doesn't affect the production Joule instance. | You should see output similar to: @@ -446,8 +452,8 @@ Severity: LOW āœ” Downloaded runtime artifact (joule.ext_investigator_capability_1.0.0.daar) āœ” Building runtime artifact > joule.ext_investigator_capability_1.0.0.daar added to the RTA -āœ” Triggering deployment (investigator_test_XX) -āœ” Your digital assistant (investigator_test_XX) deployed successfully +āœ” Triggering deployment (investigator_assistant_XX) +āœ” Your digital assistant (investigator_assistant_XX) deployed successfully ``` > šŸ’” **The i18n warning is expected** — it just means you haven't added localization files, which are optional for this exercise. @@ -460,7 +466,7 @@ Severity: LOW joule list ``` -You should see `investigator_test_XX` in the list. +You should see `investigator_assistant_XX` in the list. --- @@ -471,7 +477,7 @@ You should see `investigator_test_XX` in the list. šŸ‘‰ Open the Joule web client for your test assistant: ```bash -joule launch "investigator_test_XX" +joule launch "investigator_assistant_XX" ``` This opens a browser tab with the Joule chat interface connected to your test assistant. @@ -609,9 +615,10 @@ User sees investigation result in Joule chat **Issue**: Deployment fails with "destination not found" or agent-request returns an error - **Solution**: Verify that: - 1. The BTP Destination name (`INVESTIGATOR_AGENT_XX`) matches **exactly** the `system_aliases` key in `capability.sapdas.yaml` - 2. The destination exists in the BTP cockpit under Connectivity → Destinations - 3. The destination URL is correct and your CF app is running (`cf app investigator-crew-a2a`) + 1. The BTP Destination name (`INVESTIGATOR_AGENT_XX`) matches **exactly** the `destination` value in `capability.sapdas.yaml`'s `system_aliases` block + 2. The system alias key is `INVESTIGATOR_AGENT` (no XX) in both `capability.sapdas.yaml` and `investigate_function.yaml` + 3. The destination exists in the BTP cockpit under Connectivity → Destinations + 4. The destination URL is correct and your CF app is running (`cf app investigator-crew-a2a`) **Issue**: Joule responds with an empty message or an error diff --git a/project/Python/solution/joule/investigator_capability/capability.sapdas.yaml b/project/Python/solution/joule/investigator_capability/capability.sapdas.yaml index 01e5a97..4adc140 100644 --- a/project/Python/solution/joule/investigator_capability/capability.sapdas.yaml +++ b/project/Python/solution/joule/investigator_capability/capability.sapdas.yaml @@ -8,5 +8,5 @@ metadata: description: Capability containing the investigator crew agent for art theft investigation system_aliases: - INVESTIGATOR_AGENT_XX: + INVESTIGATOR_AGENT: destination: INVESTIGATOR_AGENT \ No newline at end of file diff --git a/project/Python/solution/joule/investigator_capability/functions/investigate_function.yaml b/project/Python/solution/joule/investigator_capability/functions/investigate_function.yaml index eb2be30..809e410 100644 --- a/project/Python/solution/joule/investigator_capability/functions/investigate_function.yaml +++ b/project/Python/solution/joule/investigator_capability/functions/investigate_function.yaml @@ -1,5 +1,7 @@ action_groups: - actions: + - type: status-update + message: Investigating... - type: agent-request system_alias: INVESTIGATOR_AGENT agent_type: remote @@ -7,4 +9,5 @@ action_groups: - type: message message: type: text + markdown: true content: "" \ No newline at end of file From b04b7c1c982bf5f4722e74b3d908734b414ad5f0 Mon Sep 17 00:00:00 2001 From: D062408 Date: Tue, 30 Jun 2026 14:46:15 +0200 Subject: [PATCH 3/6] add opus model back in for agents in agents.yaml --- project/Python/solution/config/agents.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/project/Python/solution/config/agents.yaml b/project/Python/solution/config/agents.yaml index f8b653c..52c8281 100644 --- a/project/Python/solution/config/agents.yaml +++ b/project/Python/solution/config/agents.yaml @@ -6,7 +6,7 @@ appraiser_agent: Do NOT invent or estimate values yourself. If the tool call fails, report the failure. backstory: > You are an insurance appraiser who relies strictly on model predictions. You never guess values. - llm: sap/gpt-4o + llm: sap/anthropic--claude-4.5-opus evidence_analyst_agent: role: > @@ -17,7 +17,7 @@ evidence_analyst_agent: Report only what the tool returns. backstory: > You are a methodical evidence analyst who bases conclusions strictly on retrieved documents. You never assume facts. - llm: sap/gpt-4o + llm: sap/anthropic--claude-4.5-opus lead_detective_agent: role: > @@ -28,6 +28,6 @@ lead_detective_agent: for you to analyze. backstory: > You are a lead detective who makes conclusions only from evidence and appraisals provided by your team. You never speculate. - llm: sap/anthropic--claude-4.5-sonnet + llm: sap/anthropic--claude-4.5-opus \ No newline at end of file From d8ea69aa5fe6aac22fd67afeb2a1f6fe516ee07c Mon Sep 17 00:00:00 2001 From: D062408 Date: Wed, 8 Jul 2026 09:15:22 +0200 Subject: [PATCH 4/6] =?UTF-8?q?rename=20JS=20track=20'crew'=20=E2=86=92=20?= =?UTF-8?q?'graph'=20to=20align=20with=20LangGraph=20identity?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The JavaScript track uses LangGraph JS internally but was labelled "Investigator Crew" / investigator-crew-ts — a holdover from when it was described as the TypeScript port of the Python CrewAI solution. Rename to "Investigator Graph" / investigator-graph-ts throughout exercise 07, the solution manifest, Agent Card, package.json, and README so the JS track reads as the LangGraph JS sibling of the Python-LangGraph track, not as a CrewAI clone. Comparison table rows that explicitly refer to "Python (CrewAI)" are intentionally left unchanged — they are historical comparisons, not product names for this track. --- .../JavaScript/07-deploy-agent-to-cf-ts.md | 34 +++++++++---------- project/JavaScript/solution/README.md | 6 ++-- project/JavaScript/solution/manifest.yml | 2 +- project/JavaScript/solution/package.json | 2 +- project/JavaScript/solution/src/server.ts | 4 +-- 5 files changed, 24 insertions(+), 24 deletions(-) diff --git a/exercises/JavaScript/07-deploy-agent-to-cf-ts.md b/exercises/JavaScript/07-deploy-agent-to-cf-ts.md index 987786a..c5cf97d 100644 --- a/exercises/JavaScript/07-deploy-agent-to-cf-ts.md +++ b/exercises/JavaScript/07-deploy-agent-to-cf-ts.md @@ -311,9 +311,9 @@ function resolveAppUrl(): string { const APP_URL = process.env.APP_URL ?? resolveAppUrl(); const agentCard = { - name: "Investigator Crew", + name: "Investigator Graph", description: - "Multi-agent art theft investigation workflow exposed as an A2A server", + "Multi-agent art theft investigation graph exposed as an A2A server", url: APP_URL, version: "1.0.0", protocolVersion: "0.3.0", @@ -410,7 +410,7 @@ Cloud Foundry uses a `manifest.yml` file to know how to run your application. It ```yaml applications: - - name: investigator-crew-ts- + - name: investigator-graph-ts- memory: 512M disk_quota: 1024M instances: 1 @@ -527,9 +527,9 @@ CF will: Once the push succeeds, CF prints the assigned route: ``` -name: investigator-crew-ts- +name: investigator-graph-ts- requested state: started -routes: investigator-crew-ts--.cfapps.eu10-004.hana.ondemand.com +routes: investigator-graph-ts--.cfapps.eu10-004.hana.ondemand.com ``` > āš ļø **The first push can take a few minutes** — CF is downloading and installing all npm packages. Subsequent pushes are faster. @@ -537,15 +537,15 @@ routes: investigator-crew-ts--.cfapps.eu10-004.han > šŸ’” **Monitoring the deployment:** `cf push` prints staging progress inline. To get more detail, open a second terminal and stream live logs while the push is running: > > ```bash -> cf logs investigator-crew-ts- +> cf logs investigator-graph-ts- > ``` > > After the push completes (or fails), use these commands to investigate: > > ```bash -> cf logs investigator-crew-ts- --recent # recent log output -> cf app investigator-crew-ts- # current status and instance health -> cf events investigator-crew-ts- # deployment and crash events +> cf logs investigator-graph-ts- --recent # recent log output +> cf app investigator-graph-ts- # current status and instance health +> cf events investigator-graph-ts- # deployment and crash events > ``` > šŸ’” The app reads its public URL from `VCAP_APPLICATION` at startup — a JSON object CF injects into every running app containing the assigned routes. The Agent Card always serves the correct URL automatically. @@ -568,9 +568,9 @@ You should see your agent's description: ```json { - "name": "Investigator Crew", - "description": "Multi-agent art theft investigation workflow exposed as an A2A server", - "url": "https://investigator-crew-ts--.cfapps.eu10-004.hana.ondemand.com", + "name": "Investigator Graph", + "description": "Multi-agent art theft investigation graph exposed as an A2A server", + "url": "https://investigator-graph-ts--.cfapps.eu10-004.hana.ondemand.com", "version": "1.0.0", "skills": [...] } @@ -610,7 +610,7 @@ Expected response: `{"status":"ok"}` If something went wrong during startup: ```bash -cf logs investigator-crew-ts- --recent +cf logs investigator-graph-ts- --recent ``` --- @@ -625,7 +625,7 @@ You now have a live, publicly reachable multi-agent system: flowchart TD Internet([Internet]) Router[CF Router] - App["investigator-crew-ts-YOUR-NAME\nNode.js / Express"] + App["investigator-graph-ts-YOUR-NAME\nNode.js / Express"] Card["AgentCard\n/.well-known/agent.json"] Health["Health check\n/health"] JSONRPC["A2A JSON-RPC handler\nPOST /"] @@ -695,7 +695,7 @@ flowchart TD **Issue**: `cf push` fails with `health check failed` -- **Solution**: Check `cf logs investigator-crew-ts- --recent`. Common causes: +- **Solution**: Check `cf logs investigator-graph-ts- --recent`. Common causes: - TypeScript compile error — run `npm run build` locally first to verify - Missing dependency in `package.json` - Service binding not found — verify the service name matches exactly (`generative-ai-hub`) @@ -706,11 +706,11 @@ flowchart TD **Issue**: `/.well-known/agent.json` returns a wrong URL -- **Solution**: This should not happen — the URL is read from `VCAP_APPLICATION` automatically. If it does, verify that `VCAP_APPLICATION` is present by checking `cf env investigator-crew-ts-`. +- **Solution**: This should not happen — the URL is read from `VCAP_APPLICATION` automatically. If it does, verify that `VCAP_APPLICATION` is present by checking `cf env investigator-graph-ts-`. **Issue**: App crashes immediately after startup -- **Solution**: Check `cf logs investigator-crew-ts- --recent`. Likely causes: +- **Solution**: Check `cf logs investigator-graph-ts- --recent`. Likely causes: - Missing `GROUNDING_PIPELINE_ID` or `MODEL_NAME` in `manifest.yml` - TypeScript not compiled — verify `dist/server.js` exists by running `npm run build` locally diff --git a/project/JavaScript/solution/README.md b/project/JavaScript/solution/README.md index 2953334..d0b0a66 100644 --- a/project/JavaScript/solution/README.md +++ b/project/JavaScript/solution/README.md @@ -1,6 +1,6 @@ -# Investigator Crew - TypeScript Solution +# Investigator Graph - TypeScript Solution -A multi-agent investigation system built with **LangGraph** and **SAP Cloud SDK for AI** to solve an art theft case. This TypeScript implementation provides a sophisticated agent orchestration system that mirrors the functionality of the Python CrewAI solution. +A multi-agent investigation system built with **LangGraph** and **SAP Cloud SDK for AI** to solve an art theft case. This TypeScript implementation provides a sophisticated agent orchestration system that mirrors the functionality of the Python LangGraph solution. ## šŸ—ļø Architecture @@ -101,7 +101,7 @@ The system will: ``` solution/ ā”œā”€ā”€ src/ -│ ā”œā”€ā”€ investigatorCrew.ts # LangGraph orchestration and agent definitions +│ ā”œā”€ā”€ investigationWorkflow.ts # LangGraph orchestration and agent definitions │ ā”œā”€ā”€ main.ts # Entry point │ ā”œā”€ā”€ payload.ts # Stolen items data │ ā”œā”€ā”€ rptClient.ts # RPT-1 API client diff --git a/project/JavaScript/solution/manifest.yml b/project/JavaScript/solution/manifest.yml index f08861b..bf7c51e 100644 --- a/project/JavaScript/solution/manifest.yml +++ b/project/JavaScript/solution/manifest.yml @@ -1,5 +1,5 @@ applications: - - name: investigator-crew-ts-Kevin-Riedelsheimer + - name: investigator-graph-ts-Kevin-Riedelsheimer memory: 512M disk_quota: 1024M instances: 1 diff --git a/project/JavaScript/solution/package.json b/project/JavaScript/solution/package.json index ae32260..76feb0d 100644 --- a/project/JavaScript/solution/package.json +++ b/project/JavaScript/solution/package.json @@ -1,5 +1,5 @@ { - "name": "investigator-crew-typescript", + "name": "investigator-graph-typescript", "version": "1.0.0", "description": "Multi-agent investigation system using LangGraph and SAP Cloud SDK for AI", "main": "dist/main.js", diff --git a/project/JavaScript/solution/src/server.ts b/project/JavaScript/solution/src/server.ts index 5ea18ca..45540f7 100644 --- a/project/JavaScript/solution/src/server.ts +++ b/project/JavaScript/solution/src/server.ts @@ -131,9 +131,9 @@ function resolveAppUrl(): string { const APP_URL = process.env.APP_URL ?? resolveAppUrl(); const agentCard = { - name: "Investigator Crew", + name: "Investigator Graph", description: - "Multi-agent art theft investigation workflow exposed as an A2A server", + "Multi-agent art theft investigation graph exposed as an A2A server", url: APP_URL, version: "1.0.0", protocolVersion: "0.3.0", From b7e677e690e7d3b5941f6504b06bdebf336c4acf Mon Sep 17 00:00:00 2001 From: D062408 Date: Wed, 8 Jul 2026 09:21:43 +0200 Subject: [PATCH 5/6] add exercise 08 (Joule integration) for JavaScript and Python-LangGraph tracks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Port exercises/Python/08-integrate-agent-into-joule.md to both the JavaScript (LangGraph TS) and Python-LangGraph tracks with per-track naming: - JavaScript: CF app investigator-graph-ts-, agent card at /.well-known/agent.json, InvestigationWorkflow.kickoff() - Python-LangGraph: CF app investigator-graph-, agent card at /.well-known/agent-card.json, investigator_graph.invoke() The Joule YAML (capability, scenario, function) is identical across all three tracks since the A2A response shape — artifacts[0].parts[0].text — is framework-agnostic. Also scaffold project/JavaScript/solution/joule/ and project/Python-LangGraph/solution/joule/ as reference solutions for instructors, mirroring the existing project/Python/solution/joule/. --- .../08-integrate-agent-into-joule.md | 660 ++++++++++++++++++ .../08-integrate-agent-into-joule.md | 660 ++++++++++++++++++ .../JavaScript/solution/joule/da.sapdas.yaml | 7 + .../capability.sapdas.yaml | 12 + .../functions/investigate_function.yaml | 13 + .../scenarios/investigate_scenario.yaml | 9 + .../solution/joule/da.sapdas.yaml | 7 + .../capability.sapdas.yaml | 12 + .../functions/investigate_function.yaml | 13 + .../scenarios/investigate_scenario.yaml | 9 + 10 files changed, 1402 insertions(+) create mode 100644 exercises/JavaScript/08-integrate-agent-into-joule.md create mode 100644 exercises/Python-LangGraph/08-integrate-agent-into-joule.md create mode 100644 project/JavaScript/solution/joule/da.sapdas.yaml create mode 100644 project/JavaScript/solution/joule/investigator_capability/capability.sapdas.yaml create mode 100644 project/JavaScript/solution/joule/investigator_capability/functions/investigate_function.yaml create mode 100644 project/JavaScript/solution/joule/investigator_capability/scenarios/investigate_scenario.yaml create mode 100644 project/Python-LangGraph/solution/joule/da.sapdas.yaml create mode 100644 project/Python-LangGraph/solution/joule/investigator_capability/capability.sapdas.yaml create mode 100644 project/Python-LangGraph/solution/joule/investigator_capability/functions/investigate_function.yaml create mode 100644 project/Python-LangGraph/solution/joule/investigator_capability/scenarios/investigate_scenario.yaml diff --git a/exercises/JavaScript/08-integrate-agent-into-joule.md b/exercises/JavaScript/08-integrate-agent-into-joule.md new file mode 100644 index 0000000..1d56df0 --- /dev/null +++ b/exercises/JavaScript/08-integrate-agent-into-joule.md @@ -0,0 +1,660 @@ +# Integrate Your Agent into SAP Joule + +## Overview + +Your investigator graph is deployed to Cloud Foundry and speaks A2A. But right now, only developers who know the A2A protocol can talk to it. In this exercise, you'll connect your agent to **SAP Joule** — SAP's AI copilot — so that any business user can interact with it through natural language in the Joule chat interface. + +To do that, you'll create a **Joule capability**: a set of YAML files that tell Joule what your agent can do, how to reach it, and how to display its responses. + +By the end of this exercise, your investigator graph will be: + +- āœ… Discoverable by Joule through a scenario description +- āœ… Callable from the Joule chat via the A2A protocol +- āœ… Deployed as a Joule capability using the Joule Studio CLI + +--- + +## Understand Joule Capabilities + +### What is a Capability? + +A **capability** is a package of skills that you add to Joule. It's defined entirely in YAML — no JavaScript or Python needed. Joule uses these YAML files to understand what your agent can do and how to call it. + +| Concept | What it is | Example | +|---|---|---| +| **Capability** | A group of skills packaged together | `investigator_capability` | +| **Scenario** | A user-facing skill description — Joule matches user questions against it | "Investigate an art theft" | +| **Function** | The executable logic a scenario triggers | Calls the A2A agent endpoint | +| **System Alias** | A named reference to a BTP Destination | `INVESTIGATOR_AGENT` | +| **Digital Assistant** | The top-level config that assembles capabilities | `da.sapdas.yaml` | + +### How the Pieces Fit Together + +```text +User asks: "Investigate the art theft" + │ + ā–¼ +SAP Joule + │ + ā”œā”€ā”€ 1. Scenario Matching + │ Joule reads all scenario descriptions and picks the best match + │ + ā”œā”€ā”€ 2. Function Execution + │ The matched scenario triggers a function + │ + ā”œā”€ā”€ 3. agent-request Action + │ The function calls your A2A agent via a BTP Destination + │ + ā”œā”€ā”€ 4. BTP Destination → Cloud Foundry + │ Routes the request to your deployed app + │ + ā”œā”€ā”€ 5. InvestigationWorkflow runs + │ Your LangGraph nodes do the investigation + │ + └── 6. Response displayed in Joule chat + The function extracts the result and shows it to the user +``` + +> šŸ’” **Key insight:** Joule doesn't need to know that your agent uses LangGraph, TypeScript, or any specific framework. It only speaks A2A — the same protocol you set up in Exercise 07. The capability YAML is just the bridge between Joule's chat UI and your A2A endpoint. + +--- + +## Install the Joule Studio CLI + +The **Joule Studio CLI** is the command-line tool for building, deploying, and testing Joule capabilities. + +### Step 1: Check Your Node.js Version + +The CLI requires **Node.js v20.12.0 – v24**. + +šŸ‘‰ Check your installed version: + +```bash +node -v +``` + +> āš ļø **If your version is below 20.12.0 or above 24**, download a compatible version from [https://nodejs.org/en/download/releases/](https://nodejs.org/en/download/releases/). + +### Step 2: Install the CLI + +šŸ‘‰ Run the following command to install the Joule Studio CLI globally: + +```bash +npm install -g @sap/joule-studio-cli +``` + +šŸ‘‰ Verify the installation: + +```bash +joule -V +``` + +You should see a version number printed (e.g., `1.0.90`). + +> šŸ’” **What does the CLI do?** It handles the full lifecycle of Joule capabilities: linting YAML files for errors, compiling them into deployable archives (`.daar` files), deploying to your Joule instance, and launching a test client. Think of it as `cf push` but for Joule. + +--- + +## Log In to Joule + +Before you can deploy anything, the CLI needs to authenticate against your Joule instance. + +> šŸ’” **For this CodeJam**, your instructor will provide the login credentials. You do **not** need to set up IAS applications or client secrets yourself. + +### Step 1: Get Your Credentials + +šŸ‘‰ Your instructor will provide the following values: + +| Field | Description | Example | +|---|---|---| +| **Authentication URL** | Your IAS tenant URL | `https://.accounts.ondemand.com` | +| **API URL** | The Joule API endpoint | `https://..sapdas.cloud.sap` | +| **Client ID** | OAuth client identifier | `abc123-...` | +| **Client Secret** | OAuth client secret | (provided by instructor) | +| **Username** | Your BTP user email | `your.email@example.com` | +| **Password** | Your BTP user password | (your password) | + +### Step 2: Log In + +šŸ‘‰ Run the login command and enter the values when prompted: + +```bash +joule login +``` + +The CLI will prompt you interactively: + +```text +āœ” Authentication URL: https://.accounts.ondemand.com +āœ” API URL: https://..sapdas.cloud.sap +āœ” Instance Client ID: +āœ” Instance Client Secret: +āœ” Username: your.email@example.com +āœ” Password: ******** + +API URL: https://..sapdas.cloud.sap +You are logged in as your.email@example.com +``` + +### Step 3: Verify + +šŸ‘‰ Confirm your session is active: + +```bash +joule status +``` + +> šŸ’” **Tip:** Your login session will expire after some time. If a later command fails with an authentication error, simply run `joule login` again. + +--- + +## Create the BTP Destination + +The `agent-request` action in your Joule function doesn't call your CF app directly. Instead, it goes through a **BTP Destination** — a named HTTP endpoint registered in the SAP BTP cockpit. This decouples your capability YAML from the physical URL of your agent. + +### Step 1: Open the BTP Cockpit + +šŸ‘‰ Navigate to your **BTP Subaccount** → **Connectivity** → **Destinations**. + +### Step 2: Create a New Destination + +šŸ‘‰ Click **New Destination** and fill in the following values: + +| Field | Value | +|---|---| +| **Name** | `INVESTIGATOR_AGENT_XX` | +| **Type** | HTTP | +| **URL** | `https://investigator-graph-ts-.cfapps.eu10-004.hana.ondemand.com` | +| **Proxy Type** | Internet | +| **Authentication** | NoAuthentication | + +> āš ļø **Replace `XX` with your participant number** (e.g., `INVESTIGATOR_AGENT_01`, `INVESTIGATOR_AGENT_02`). This ensures each participant has a unique destination. You'll use this exact name in all YAML files below. + +> āš ļø **Replace ``** with the actual route of your CF app from Exercise 07. You can find it by running `cf apps` or by checking the URL you noted at the end of Exercise 07. + +šŸ‘‰ Click **Save**. + +> šŸ’” **How does Joule use this destination?** +> +> When Joule triggers an `agent-request`, it: +> 1. Looks up the **system alias** in your capability YAML +> 2. Resolves it to the **BTP Destination** name +> 3. Uses the destination's URL to call `GET /.well-known/agent.json` on your agent +> 4. Reads the Agent Card to discover the communication endpoint and protocol +> 5. Sends an A2A `message/send` request with the user's message +> +> This is the same discovery flow you tested manually with `curl` in Exercise 07 — now Joule does it automatically. + +--- + +## Create the Joule Capability + +Now you'll create the YAML files that define your Joule capability. These files tell Joule: +- **What** your agent can do (scenario) +- **How** to call it (function with `agent-request`) +- **Where** to find it (system alias → BTP Destination) + +### Project Structure + +The complete directory structure you'll create: + +```text +/project/JavaScript/starter-project/joule/ +ā”œā”€ā”€ investigator_capability/ +│ ā”œā”€ā”€ functions/ +│ │ └── investigate_function.yaml +│ ā”œā”€ā”€ scenarios/ +│ │ └── investigate_scenario.yaml +│ └── capability.sapdas.yaml +└── da.sapdas.yaml +``` + +### Step 1: Create the Directory Structure + +šŸ‘‰ From your terminal, create the directories: + +```bash +# macOS / Linux +mkdir -p project/JavaScript/starter-project/joule/investigator_capability/functions +mkdir -p project/JavaScript/starter-project/joule/investigator_capability/scenarios +``` + +```powershell +# Windows (PowerShell) +New-Item -ItemType Directory -Path project\JavaScript\starter-project\joule\investigator_capability\functions -Force +New-Item -ItemType Directory -Path project\JavaScript\starter-project\joule\investigator_capability\scenarios -Force +``` + +> šŸ’” **In Business Application Studio (BAS):** You can create the folders visually instead. Right-click the `starter-project` folder in the Explorer panel and choose **New Folder**. Create `joule`, then inside it `investigator_capability`, and inside that `functions` and `scenarios`. + +### Step 2: Create capability.sapdas.yaml + +This is the root configuration file for your capability. It defines metadata, the schema version, and the system aliases that map to BTP Destinations. + +šŸ‘‰ Create a new file [`/project/JavaScript/starter-project/joule/investigator_capability/capability.sapdas.yaml`](/project/JavaScript/starter-project/joule/investigator_capability/capability.sapdas.yaml): + +```yaml +schema_version: 3.28.0 + +metadata: + namespace: joule.ext + name: investigator_capability + version: 1.0.0 + display_name: Investigator_Capability + description: Capability containing the investigator graph agent for art theft investigation + +system_aliases: + INVESTIGATOR_AGENT: + destination: INVESTIGATOR_AGENT_XX +``` + +> āš ļø **Replace `XX`** in `destination` only with your participant number (e.g., `INVESTIGATOR_AGENT_01`). This must match the BTP Destination name you created in the previous section. The system alias key (`INVESTIGATOR_AGENT`) stays the same for everyone — only the destination name is unique per participant. + +> šŸ’” **Understanding each field:** +> +> | Field | Purpose | +> |---|---| +> | `schema_version: 3.28.0` | Minimum schema version that supports code-based agents via `agent-request`. Using a lower version will cause a compile error. | +> | `namespace: joule.ext` | **Required** for all custom capabilities. Any other namespace is treated as an SAP-internal namespace and will fail deployment. | +> | `name` | Internal identifier for the capability. Must be unique within your Joule instance. | +> | `display_name` | Human-readable name shown in Joule Studio. Alphanumeric + underscore + hyphen only. | +> | `description` | Short description of what this capability does (max 512 chars). | +> | `system_aliases` | Maps a logical name (used in function YAMLs) to a BTP Destination name. The key (`INVESTIGATOR_AGENT`) is referenced by the `system_alias` field in the function. | + +### Step 3: Create the Scenario + +The scenario is what Joule uses to match user requests to your capability. When a user types a question, Joule compares it against all scenario descriptions and picks the best match. + +šŸ‘‰ Create a new file [`/project/JavaScript/starter-project/joule/investigator_capability/scenarios/investigate_scenario.yaml`](/project/JavaScript/starter-project/joule/investigator_capability/scenarios/investigate_scenario.yaml): + +```yaml +description: >- + This skill investigates art theft cases by appraising the value of stolen + items, analyzing evidence from security logs, bank records, phone records, + and criminal histories, and identifying the most likely suspect based on + available evidence. It coordinates multiple specialist agents to deliver + a comprehensive investigation report. +target: + name: investigate_function + type: function +``` + +> šŸ’” **Understanding the scenario:** +> +> | Field | Purpose | +> |---|---| +> | `description` | The text Joule uses for intent matching. Write it like you're explaining the skill to a colleague — specific, clear, keyword-rich. Joule's language model evaluates this against the user's message to decide whether to activate this scenario. | +> | `target.name` | References the function YAML file name (without `.yaml`). When this scenario is triggered, Joule runs this function. | +> | `target.type: function` | Currently the only supported target type. Tells Joule to execute a dialog function. | +> +> **Writing good descriptions matters.** If the description is too vague ("helps with investigations"), Joule may not match it reliably. If it's too narrow ("only for the Grand Museum heist"), it won't match variations. The description above includes multiple keywords: "art theft", "stolen items", "evidence", "security logs", "suspect", "investigation report". + +> āš ļø **The `investigate_function` does not exist yet** — you'll create it in the next step. If you run `joule lint` now, it will report that the target function is missing. That is expected. Continue to Step 4 to resolve it. + +### Step 4: Create the Function + +The function is where the action happens. It defines the `agent-request` action that calls your A2A agent and a `message` action that displays the result. + +> šŸ’” **In Business Application Studio (BAS):** Right-click the `functions` folder in the Explorer panel and choose **New File** to create the file below. + +šŸ‘‰ Create a new file [`/project/JavaScript/starter-project/joule/investigator_capability/functions/investigate_function.yaml`](/project/JavaScript/starter-project/joule/investigator_capability/functions/investigate_function.yaml): + +```yaml +action_groups: + - actions: + - type: agent-request + system_alias: INVESTIGATOR_AGENT + agent_type: remote + result_variable: "apiResponse" + - type: message + message: + type: text + markdown: true + content: "" +``` + +> šŸ’” **Understanding the `agent-request` action:** +> +> | Field | Purpose | +> |---|---| +> | `type: agent-request` | Tells Joule to call an external agent using the A2A protocol (available since DTA schema v3.28.0). | +> | `system_alias: INVESTIGATOR_AGENT` | References the system alias from `capability.sapdas.yaml`, which maps to your participant-specific BTP Destination. | +> | `agent_type: remote` | Specifies this is a **code-based** (Bring Your Own Agent) agent hosted outside of Joule. Use `local` for content-based agents built inside the Joule framework. | +> | `result_variable: "apiResponse"` | Stores the full A2A response in a variable. You access it in subsequent actions using SpEL expressions. | + +> šŸ’” **Understanding the `message` action:** +> +> | Field | Purpose | +> |---|---| +> | `type: message` | Sends a message back to the user in the Joule chat. | +> | `type: text` | The message format. Joule also supports `card`, `list`, `carousel`, and other rich formats. | +> | `markdown: true` | Tells Joule to render the response as formatted markdown. Without this, Joule displays the raw text including all the `#`, `**`, and `-` characters. With it, headers, bullet points, and bold text are rendered properly — making the investigation report significantly more readable in the chat. | +> | `content` | The message text. The `` delimiters indicate a **SpEL expression** — Joule's scripting language for extracting values from variables. | + +### Understanding the A2A Response + +When the `agent-request` action calls your CF app, it sends the user's message via the A2A protocol and receives a response. The response is stored in `apiResponse` and has this structure: + +```json +{ + "headers": {}, + "body": { + "kind": "task", + "contextId": "abc-123-def-456", + "history": [ + { + "role": "user", + "kind": "message", + "parts": [{ "kind": "text", "text": "Investigate the art theft..." }] + } + ], + "artifacts": [ + { + "name": "investigation_result", + "parts": [ + { + "kind": "text", + "text": "Based on the investigation, the primary suspect is..." + } + ], + "artifactId": "investigation_result" + } + ], + "status": { + "state": "completed" + } + } +} +``` + +The expression `apiResponse.body.artifacts[0].parts[0].text` walks this JSON path: + +| Path segment | What it accesses | +|---|---| +| `apiResponse` | The full response variable | +| `.body` | The A2A task response body | +| `.artifacts[0]` | The first artifact — your `investigation_result` from `src/server.ts` | +| `.parts[0]` | The first part of that artifact | +| `.text` | The actual text content of the investigation result | + +> šŸ’” **This maps directly to what you built in Exercise 07.** In `src/server.ts`, your `InvestigatorExecutor` emits a `TaskArtifactUpdateEvent` with `artifactId: "investigation_result"` and `parts: [{ kind: "text", text: result }]` — the output of `new InvestigationWorkflow(...).kickoff(...)`. That's exactly what Joule reads here through `artifacts[0].parts[0].text`. + +### Step 5: Create the Digital Assistant Descriptor + +The `da.sapdas.yaml` file is the top-level manifest. It tells the Joule CLI which capabilities to include when compiling and deploying. + +šŸ‘‰ Create a new file [`/project/JavaScript/starter-project/joule/da.sapdas.yaml`](/project/JavaScript/starter-project/joule/da.sapdas.yaml): + +```yaml +schema_version: 1.4.0 + +name: investigator_assistant + +capabilities: + - type: local + folder: ./investigator_capability +``` + +> šŸ’” **Understanding the DA file:** +> +> | Field | Purpose | +> |---|---| +> | `schema_version: 1.4.0` | Version of the Digital Assistant schema (separate from the capability schema). | +> | `name` | The name of the assistant. Used when deploying with `joule deploy -n`. | +> | `capabilities` | List of capabilities to include. `type: local` means the capability is in a local folder (as opposed to a remote reference). | +> | `folder` | Path to the capability directory, relative to this file. | + +--- + +## Deploy the Capability + +With all YAML files in place, you can now compile and deploy the capability to your Joule instance. + +### Step 1: Navigate to the Joule Directory + +šŸ‘‰ Change to the directory containing your `da.sapdas.yaml`: + +```bash +cd project/JavaScript/starter-project/joule +``` + +### Step 2: Compile and Deploy + +šŸ‘‰ Run the following command to compile the YAML files and deploy them as a test assistant: + +```bash +joule deploy -c -n "investigator_assistant_XX" +``` + +> āš ļø **Replace `XX`** with your participant number (e.g., `investigator_assistant_01`). + +> šŸ’” **Understanding the flags:** +> +> | Flag | Purpose | +> |---|---| +> | `-c` | Compile before deploying. This validates your YAML files against the schema, checks for errors, and packages them into a `.daar` archive (Design-time Artifact Archive). | +> | `-n "investigator_assistant_XX"` | Name of the test assistant to create. Using `-n` creates a standalone assistant for testing — it doesn't affect the production Joule instance. | + +You should see output similar to: + +```text +āœ” Building designtime artifact (investigator_capability) +āœ” Trigger compilation +āœ” Compiled + +Detailed logs: +WARNINGS: +Message: DTA did not define an optional i18n folder +Path: +Category: I18N +Severity: LOW + +āœ” Downloaded runtime artifact (joule.ext_investigator_capability_1.0.0.daar) +āœ” Building runtime artifact + > joule.ext_investigator_capability_1.0.0.daar added to the RTA +āœ” Triggering deployment (investigator_assistant_XX) +āœ” Your digital assistant (investigator_assistant_XX) deployed successfully +``` + +> šŸ’” **The i18n warning is expected** — it just means you haven't added localization files, which are optional for this exercise. + +### Step 3: Verify Deployment + +šŸ‘‰ List your deployed assistants to confirm: + +```bash +joule list +``` + +You should see `investigator_assistant_XX` in the list. + +--- + +## Test in Joule + +### Step 1: Launch the Test Client + +šŸ‘‰ Open the Joule web client for your test assistant: + +```bash +joule launch "investigator_assistant_XX" +``` + +This opens a browser tab with the Joule chat interface connected to your test assistant. + +### Step 2: Send a Test Message + +šŸ‘‰ In the Joule chat, type: + +```text +Investigate the art theft at the museum. The suspects are Sophie Dubois, Marcus Chen, and Viktor Petrov. +``` + +> āš ļø **The investigation takes 1–2 minutes** because the full LangGraph pipeline runs on Cloud Foundry: the Appraiser calls RPT-1, the Evidence Analyst queries the Grounding Service, and the Lead Detective synthesizes the findings. Joule's synchronous timeout is 60 seconds — if your graph takes longer, you may see a timeout error. If this happens, try a simpler request or check the troubleshooting section below. + +### Step 3: Review the Response + +If everything is connected correctly, Joule displays the investigation result directly in the chat — the same markdown report your graph generates, now accessible through a conversational interface. + +> šŸ’” **What just happened behind the scenes:** +> 1. You typed a question in Joule +> 2. Joule matched your question to `investigate_scenario` based on the description +> 3. The scenario triggered `investigate_function` +> 4. The function's `agent-request` action called your BTP Destination +> 5. The destination routed to your CF app's A2A endpoint +> 6. Your `InvestigatorExecutor` ran `new InvestigationWorkflow(...).kickoff(...)` — the full LangGraph pipeline +> 7. The result came back as an A2A artifact +> 8. The function's `message` action extracted the text and displayed it in Joule + +--- + +## Understanding What Just Happened + +### The Full Architecture + +You now have a complete end-to-end pipeline from Joule's chat UI to your LangGraph system: + +```text +User + │ + ā–¼ +SAP Joule (Chat UI) + │ + ā”œā”€ā”€ Scenario Match → investigate_scenario + │ │ + │ ā–¼ + │ investigate_function + │ │ + │ ā”œā”€ā”€ agent-request (A2A over HTTPS) + │ │ │ + │ │ ā–¼ + │ │ BTP Destination: INVESTIGATOR_AGENT_XX + │ │ │ + │ │ ā–¼ + │ │ CF App: investigator-graph-ts- + │ │ │ + │ │ ā–¼ + │ │ InvestigatorExecutor → InvestigationWorkflow.kickoff(...) + │ │ ā”œā”€ā”€ Appraiser Node (RPT-1) + │ │ ā”œā”€ā”€ Evidence Analyst Node (Grounding) + │ │ └── Lead Detective Node (GPT-4o) + │ │ │ + │ │ ā–¼ + │ │ A2A Response (artifacts[0].parts[0].text) + │ │ + │ ā”œā”€ā”€ message → Displays result to user + │ │ + ā–¼ ā–¼ +User sees investigation result in Joule chat +``` + +### How Each Layer Communicates + +| Step | Component | Protocol | What happens | +|---|---|---|---| +| 1 | User → Joule | Chat UI | User types a question in natural language | +| 2 | Joule → Scenario | Internal | Joule matches the question to the best scenario description | +| 3 | Scenario → Function | Internal | The matched scenario triggers the linked function | +| 4 | Function → Destination | HTTP | The `agent-request` action resolves the system alias to a BTP Destination | +| 5 | Destination → CF App | HTTPS | The destination routes to your Cloud Foundry app URL | +| 6 | CF App → LangGraph | Internal | The `InvestigatorExecutor` runs `new InvestigationWorkflow(...).kickoff(...)` | +| 7 | LangGraph → Response | A2A JSON-RPC | The result is returned as an A2A `TaskArtifactUpdateEvent` | +| 8 | Function → User | Chat UI | The `message` action extracts the text and shows it in Joule | + +--- + +## Key Takeaways + +- **Joule capabilities** are YAML-based packages that extend Joule with custom skills — no runtime code needed on the Joule side +- **`agent-request`** is the action type that bridges Joule to external agents via the A2A protocol +- **`agent_type: remote`** distinguishes code-based agents (your CF app) from content-based agents built inside Joule +- **System aliases** decouple the capability from the physical URL — the BTP Destination handles routing, so you can change the URL without redeploying the capability +- **`schema_version: 3.28.0`** is the minimum DTA version required for code-based agent support +- **The Joule Studio CLI** provides a complete workflow: `login` → `deploy` → `launch` → `update` +- **The A2A protocol** enables framework-agnostic integration — Joule doesn't care whether your agent uses LangGraph, Python, or any other framework + +--- + +## Next Steps + +1. āœ… [Set up your development space](01-setup-dev-space.md) +2. āœ… [Build a basic agent](02-build-a-basic-agent.md) +3. āœ… [Add custom tools](03-add-your-first-tool.md) +4. āœ… [Build a multi-agent system](04-building-multi-agent-system.md) +5. āœ… [Add the Grounding Service](05-add-the-grounding-service.md) +6. āœ… [Solve the crime](06-solve-the-crime.md) +7. āœ… [Deploy your agent to CF with A2A](07-deploy-agent-to-cf-ts.md) +8. āœ… [Integrate your agent into SAP Joule](08-integrate-agent-into-joule.md) (this exercise) + +šŸŽ‰ **Congratulations!** You've completed the full CodeJam. You built a multi-agent AI system from scratch using LangGraph (TypeScript), deployed it to Cloud Foundry as an A2A server, and integrated it into SAP Joule — making it accessible to business users through natural language. + +--- + +## Troubleshooting + +**Issue**: `joule: command not found` after installing + +- **Solution**: The global npm bin directory may not be in your PATH. Run `npm config get prefix` — the result + `/bin` should be in your PATH. On macOS/Linux, add it to your shell profile (e.g., `export PATH="$(npm config get prefix)/bin:$PATH"`). + +**Issue**: `joule login` fails with an authentication error + +- **Solution**: Double-check all credentials with your instructor. Common mistakes: + - Trailing spaces in the Auth URL or API URL + - Wrong Client ID / Client Secret pair + - Password with special characters that need escaping (`$` → `\$`) + - Expired or incorrect IAS credentials + +**Issue**: `joule deploy` fails with a schema version error + +- **Solution**: Ensure `schema_version: 3.28.0` in `capability.sapdas.yaml`. The `agent-request` action type requires this minimum version. Lower versions like `3.26.0` will not recognize the action. + +**Issue**: `joule deploy` fails with "User is not authorized to deploy sap capabilities" + +- **Solution**: Your `namespace` must be `joule.ext`. Any other namespace (e.g., `joule.custom`, `my.namespace`) is treated as an SAP-internal namespace and requires special permissions. + +**Issue**: Deployment fails with "destination not found" or agent-request returns an error + +- **Solution**: Verify that: + 1. The BTP Destination name (`INVESTIGATOR_AGENT_XX`) matches **exactly** the `destination` value in `capability.sapdas.yaml`'s `system_aliases` block + 2. The system alias key is `INVESTIGATOR_AGENT` (no XX) in both `capability.sapdas.yaml` and `investigate_function.yaml` + 3. The destination exists in the BTP cockpit under Connectivity → Destinations + 4. The destination URL is correct and your CF app is running (`cf apps`) + +**Issue**: Joule responds with an empty message or an error + +- **Solution**: Your CF app may have crashed or returned an unexpected response. Check: + - App status: `cf apps` + - App logs: `cf logs investigator-graph-ts- --recent` + - Health endpoint: `curl https:///health` + - Ensure the Agent Card is accessible: `curl https:///.well-known/agent.json` + +**Issue**: "Scenario not matched" — Joule doesn't recognize your question + +- **Solution**: Joule's intent matching depends on the scenario description. Try rephrasing your question to include keywords from the description: "investigate", "art theft", "stolen items", "suspects", "evidence". If testing with a standalone assistant, ensure the capability was deployed successfully with `joule list`. + +**Issue**: Timeout error — the agent takes too long to respond + +- **Solution**: Joule expects a synchronous response within **60 seconds**. The full LangGraph pipeline may take longer. Options: + - Try a simpler request that requires less processing + - Check `cf logs investigator-graph-ts- --recent` to see where time is spent + - Ensure your CF app has enough memory (512M in `manifest.yml`) + - Consider reducing the number of grounding service queries in the Evidence Analyst node + +**Issue**: YAML indentation or compile errors + +- **Solution**: YAML is whitespace-sensitive — use **2 spaces** for indentation, never tabs. Common mistakes: + - Mixed tabs and spaces + - Missing space after `:` in key-value pairs + - Incorrect nesting of `actions` under `action_groups` + - Run `joule lint` to check for errors before deploying + +--- + +## Resources + +- [Joule Development Guide](https://help.sap.com/docs/joule/joule-development-guide-ba88d1ec6a1b442098863d577c19b0c0/joule-development) +- [Code-Based Agents (Bring Your Own Agent)](https://help.sap.com/docs/joule/joule-development-guide-ba88d1ec6a1b442098863d577c19b0c0/code-based-agents-bring-your-own-agent) +- [Install and Update the Joule Studio CLI](https://help.sap.com/docs/joule/joule-development-guide-ba88d1ec6a1b442098863d577c19b0c0/install-and-update-joule-studio-cli) +- [A2A Protocol Specification](https://a2a-protocol.org/latest/) +- [SAP BTP Destinations Documentation](https://help.sap.com/docs/connectivity/sap-btp-connectivity-cf/create-http-destinations) +- [Blog: Joule A2A — Connect Code-Based Agents into Joule](https://community.sap.com/t5/technology-blog-posts-by-sap/joule-a2a-connect-code-based-agents-into-joule/ba-p/14329279) diff --git a/exercises/Python-LangGraph/08-integrate-agent-into-joule.md b/exercises/Python-LangGraph/08-integrate-agent-into-joule.md new file mode 100644 index 0000000..0472d4e --- /dev/null +++ b/exercises/Python-LangGraph/08-integrate-agent-into-joule.md @@ -0,0 +1,660 @@ +# Integrate Your Agent into SAP Joule + +## Overview + +Your investigator graph is deployed to Cloud Foundry and speaks A2A. But right now, only developers who know the A2A protocol can talk to it. In this exercise, you'll connect your agent to **SAP Joule** — SAP's AI copilot — so that any business user can interact with it through natural language in the Joule chat interface. + +To do that, you'll create a **Joule capability**: a set of YAML files that tell Joule what your agent can do, how to reach it, and how to display its responses. + +By the end of this exercise, your investigator graph will be: + +- āœ… Discoverable by Joule through a scenario description +- āœ… Callable from the Joule chat via the A2A protocol +- āœ… Deployed as a Joule capability using the Joule Studio CLI + +--- + +## Understand Joule Capabilities + +### What is a Capability? + +A **capability** is a package of skills that you add to Joule. It's defined entirely in YAML — no JavaScript or Python needed. Joule uses these YAML files to understand what your agent can do and how to call it. + +| Concept | What it is | Example | +|---|---|---| +| **Capability** | A group of skills packaged together | `investigator_capability` | +| **Scenario** | A user-facing skill description — Joule matches user questions against it | "Investigate an art theft" | +| **Function** | The executable logic a scenario triggers | Calls the A2A agent endpoint | +| **System Alias** | A named reference to a BTP Destination | `INVESTIGATOR_AGENT` | +| **Digital Assistant** | The top-level config that assembles capabilities | `da.sapdas.yaml` | + +### How the Pieces Fit Together + +```text +User asks: "Investigate the art theft" + │ + ā–¼ +SAP Joule + │ + ā”œā”€ā”€ 1. Scenario Matching + │ Joule reads all scenario descriptions and picks the best match + │ + ā”œā”€ā”€ 2. Function Execution + │ The matched scenario triggers a function + │ + ā”œā”€ā”€ 3. agent-request Action + │ The function calls your A2A agent via a BTP Destination + │ + ā”œā”€ā”€ 4. BTP Destination → Cloud Foundry + │ Routes the request to your deployed app + │ + ā”œā”€ā”€ 5. InvestigatorGraph runs + │ Your LangGraph agents do the investigation + │ + └── 6. Response displayed in Joule chat + The function extracts the result and shows it to the user +``` + +> šŸ’” **Key insight:** Joule doesn't need to know that your agent uses LangGraph, Python, or any specific framework. It only speaks A2A — the same protocol you set up in Exercise 07. The capability YAML is just the bridge between Joule's chat UI and your A2A endpoint. + +--- + +## Install the Joule Studio CLI + +The **Joule Studio CLI** is the command-line tool for building, deploying, and testing Joule capabilities. + +### Step 1: Check Your Node.js Version + +The CLI requires **Node.js v20.12.0 – v24**. + +šŸ‘‰ Check your installed version: + +```bash +node -v +``` + +> āš ļø **If your version is below 20.12.0 or above 24**, download a compatible version from [https://nodejs.org/en/download/releases/](https://nodejs.org/en/download/releases/). + +### Step 2: Install the CLI + +šŸ‘‰ Run the following command to install the Joule Studio CLI globally: + +```bash +npm install -g @sap/joule-studio-cli +``` + +šŸ‘‰ Verify the installation: + +```bash +joule -V +``` + +You should see a version number printed (e.g., `1.0.90`). + +> šŸ’” **What does the CLI do?** It handles the full lifecycle of Joule capabilities: linting YAML files for errors, compiling them into deployable archives (`.daar` files), deploying to your Joule instance, and launching a test client. Think of it as `cf push` but for Joule. + +--- + +## Log In to Joule + +Before you can deploy anything, the CLI needs to authenticate against your Joule instance. + +> šŸ’” **For this CodeJam**, your instructor will provide the login credentials. You do **not** need to set up IAS applications or client secrets yourself. + +### Step 1: Get Your Credentials + +šŸ‘‰ Your instructor will provide the following values: + +| Field | Description | Example | +|---|---|---| +| **Authentication URL** | Your IAS tenant URL | `https://.accounts.ondemand.com` | +| **API URL** | The Joule API endpoint | `https://..sapdas.cloud.sap` | +| **Client ID** | OAuth client identifier | `abc123-...` | +| **Client Secret** | OAuth client secret | (provided by instructor) | +| **Username** | Your BTP user email | `your.email@example.com` | +| **Password** | Your BTP user password | (your password) | + +### Step 2: Log In + +šŸ‘‰ Run the login command and enter the values when prompted: + +```bash +joule login +``` + +The CLI will prompt you interactively: + +```text +āœ” Authentication URL: https://.accounts.ondemand.com +āœ” API URL: https://..sapdas.cloud.sap +āœ” Instance Client ID: +āœ” Instance Client Secret: +āœ” Username: your.email@example.com +āœ” Password: ******** + +API URL: https://..sapdas.cloud.sap +You are logged in as your.email@example.com +``` + +### Step 3: Verify + +šŸ‘‰ Confirm your session is active: + +```bash +joule status +``` + +> šŸ’” **Tip:** Your login session will expire after some time. If a later command fails with an authentication error, simply run `joule login` again. + +--- + +## Create the BTP Destination + +The `agent-request` action in your Joule function doesn't call your CF app directly. Instead, it goes through a **BTP Destination** — a named HTTP endpoint registered in the SAP BTP cockpit. This decouples your capability YAML from the physical URL of your agent. + +### Step 1: Open the BTP Cockpit + +šŸ‘‰ Navigate to your **BTP Subaccount** → **Connectivity** → **Destinations**. + +### Step 2: Create a New Destination + +šŸ‘‰ Click **New Destination** and fill in the following values: + +| Field | Value | +|---|---| +| **Name** | `INVESTIGATOR_AGENT_XX` | +| **Type** | HTTP | +| **URL** | `https://investigator-graph-.cfapps.eu10-004.hana.ondemand.com` | +| **Proxy Type** | Internet | +| **Authentication** | NoAuthentication | + +> āš ļø **Replace `XX` with your participant number** (e.g., `INVESTIGATOR_AGENT_01`, `INVESTIGATOR_AGENT_02`). This ensures each participant has a unique destination. You'll use this exact name in all YAML files below. + +> āš ļø **Replace ``** with the actual route of your CF app from Exercise 07. You can find it by running `cf apps` or by checking the URL you noted at the end of Exercise 07. + +šŸ‘‰ Click **Save**. + +> šŸ’” **How does Joule use this destination?** +> +> When Joule triggers an `agent-request`, it: +> 1. Looks up the **system alias** in your capability YAML +> 2. Resolves it to the **BTP Destination** name +> 3. Uses the destination's URL to call `GET /.well-known/agent-card.json` on your agent +> 4. Reads the Agent Card to discover the communication endpoint and protocol +> 5. Sends an A2A `message/send` request with the user's message +> +> This is the same discovery flow you tested manually with `curl` in Exercise 07 — now Joule does it automatically. + +--- + +## Create the Joule Capability + +Now you'll create the YAML files that define your Joule capability. These files tell Joule: +- **What** your agent can do (scenario) +- **How** to call it (function with `agent-request`) +- **Where** to find it (system alias → BTP Destination) + +### Project Structure + +The complete directory structure you'll create: + +```text +/project/Python-LangGraph/starter-project/joule/ +ā”œā”€ā”€ investigator_capability/ +│ ā”œā”€ā”€ functions/ +│ │ └── investigate_function.yaml +│ ā”œā”€ā”€ scenarios/ +│ │ └── investigate_scenario.yaml +│ └── capability.sapdas.yaml +└── da.sapdas.yaml +``` + +### Step 1: Create the Directory Structure + +šŸ‘‰ From your terminal, create the directories: + +```bash +# macOS / Linux +mkdir -p project/Python-LangGraph/starter-project/joule/investigator_capability/functions +mkdir -p project/Python-LangGraph/starter-project/joule/investigator_capability/scenarios +``` + +```powershell +# Windows (PowerShell) +New-Item -ItemType Directory -Path project\Python-LangGraph\starter-project\joule\investigator_capability\functions -Force +New-Item -ItemType Directory -Path project\Python-LangGraph\starter-project\joule\investigator_capability\scenarios -Force +``` + +> šŸ’” **In Business Application Studio (BAS):** You can create the folders visually instead. Right-click the `starter-project` folder in the Explorer panel and choose **New Folder**. Create `joule`, then inside it `investigator_capability`, and inside that `functions` and `scenarios`. + +### Step 2: Create capability.sapdas.yaml + +This is the root configuration file for your capability. It defines metadata, the schema version, and the system aliases that map to BTP Destinations. + +šŸ‘‰ Create a new file [`/project/Python-LangGraph/starter-project/joule/investigator_capability/capability.sapdas.yaml`](/project/Python-LangGraph/starter-project/joule/investigator_capability/capability.sapdas.yaml): + +```yaml +schema_version: 3.28.0 + +metadata: + namespace: joule.ext + name: investigator_capability + version: 1.0.0 + display_name: Investigator_Capability + description: Capability containing the investigator graph agent for art theft investigation + +system_aliases: + INVESTIGATOR_AGENT: + destination: INVESTIGATOR_AGENT_XX +``` + +> āš ļø **Replace `XX`** in `destination` only with your participant number (e.g., `INVESTIGATOR_AGENT_01`). This must match the BTP Destination name you created in the previous section. The system alias key (`INVESTIGATOR_AGENT`) stays the same for everyone — only the destination name is unique per participant. + +> šŸ’” **Understanding each field:** +> +> | Field | Purpose | +> |---|---| +> | `schema_version: 3.28.0` | Minimum schema version that supports code-based agents via `agent-request`. Using a lower version will cause a compile error. | +> | `namespace: joule.ext` | **Required** for all custom capabilities. Any other namespace is treated as an SAP-internal namespace and will fail deployment. | +> | `name` | Internal identifier for the capability. Must be unique within your Joule instance. | +> | `display_name` | Human-readable name shown in Joule Studio. Alphanumeric + underscore + hyphen only. | +> | `description` | Short description of what this capability does (max 512 chars). | +> | `system_aliases` | Maps a logical name (used in function YAMLs) to a BTP Destination name. The key (`INVESTIGATOR_AGENT`) is referenced by the `system_alias` field in the function. | + +### Step 3: Create the Scenario + +The scenario is what Joule uses to match user requests to your capability. When a user types a question, Joule compares it against all scenario descriptions and picks the best match. + +šŸ‘‰ Create a new file [`/project/Python-LangGraph/starter-project/joule/investigator_capability/scenarios/investigate_scenario.yaml`](/project/Python-LangGraph/starter-project/joule/investigator_capability/scenarios/investigate_scenario.yaml): + +```yaml +description: >- + This skill investigates art theft cases by appraising the value of stolen + items, analyzing evidence from security logs, bank records, phone records, + and criminal histories, and identifying the most likely suspect based on + available evidence. It coordinates multiple specialist agents to deliver + a comprehensive investigation report. +target: + name: investigate_function + type: function +``` + +> šŸ’” **Understanding the scenario:** +> +> | Field | Purpose | +> |---|---| +> | `description` | The text Joule uses for intent matching. Write it like you're explaining the skill to a colleague — specific, clear, keyword-rich. Joule's language model evaluates this against the user's message to decide whether to activate this scenario. | +> | `target.name` | References the function YAML file name (without `.yaml`). When this scenario is triggered, Joule runs this function. | +> | `target.type: function` | Currently the only supported target type. Tells Joule to execute a dialog function. | +> +> **Writing good descriptions matters.** If the description is too vague ("helps with investigations"), Joule may not match it reliably. If it's too narrow ("only for the Grand Museum heist"), it won't match variations. The description above includes multiple keywords: "art theft", "stolen items", "evidence", "security logs", "suspect", "investigation report". + +> āš ļø **The `investigate_function` does not exist yet** — you'll create it in the next step. If you run `joule lint` now, it will report that the target function is missing. That is expected. Continue to Step 4 to resolve it. + +### Step 4: Create the Function + +The function is where the action happens. It defines the `agent-request` action that calls your A2A agent and a `message` action that displays the result. + +> šŸ’” **In Business Application Studio (BAS):** Right-click the `functions` folder in the Explorer panel and choose **New File** to create the file below. + +šŸ‘‰ Create a new file [`/project/Python-LangGraph/starter-project/joule/investigator_capability/functions/investigate_function.yaml`](/project/Python-LangGraph/starter-project/joule/investigator_capability/functions/investigate_function.yaml): + +```yaml +action_groups: + - actions: + - type: agent-request + system_alias: INVESTIGATOR_AGENT + agent_type: remote + result_variable: "apiResponse" + - type: message + message: + type: text + markdown: true + content: "" +``` + +> šŸ’” **Understanding the `agent-request` action:** +> +> | Field | Purpose | +> |---|---| +> | `type: agent-request` | Tells Joule to call an external agent using the A2A protocol (available since DTA schema v3.28.0). | +> | `system_alias: INVESTIGATOR_AGENT` | References the system alias from `capability.sapdas.yaml`, which maps to your participant-specific BTP Destination. | +> | `agent_type: remote` | Specifies this is a **code-based** (Bring Your Own Agent) agent hosted outside of Joule. Use `local` for content-based agents built inside the Joule framework. | +> | `result_variable: "apiResponse"` | Stores the full A2A response in a variable. You access it in subsequent actions using SpEL expressions. | + +> šŸ’” **Understanding the `message` action:** +> +> | Field | Purpose | +> |---|---| +> | `type: message` | Sends a message back to the user in the Joule chat. | +> | `type: text` | The message format. Joule also supports `card`, `list`, `carousel`, and other rich formats. | +> | `markdown: true` | Tells Joule to render the response as formatted markdown. Without this, Joule displays the raw text including all the `#`, `**`, and `-` characters. With it, headers, bullet points, and bold text are rendered properly — making the investigation report significantly more readable in the chat. | +> | `content` | The message text. The `` delimiters indicate a **SpEL expression** — Joule's scripting language for extracting values from variables. | + +### Understanding the A2A Response + +When the `agent-request` action calls your CF app, it sends the user's message via the A2A protocol and receives a response. The response is stored in `apiResponse` and has this structure: + +```json +{ + "headers": {}, + "body": { + "kind": "task", + "contextId": "abc-123-def-456", + "history": [ + { + "role": "user", + "kind": "message", + "parts": [{ "kind": "text", "text": "Investigate the art theft..." }] + } + ], + "artifacts": [ + { + "name": "investigation_result", + "parts": [ + { + "kind": "text", + "text": "Based on the investigation, the primary suspect is..." + } + ], + "artifactId": "investigation_result" + } + ], + "status": { + "state": "completed" + } + } +} +``` + +The expression `apiResponse.body.artifacts[0].parts[0].text` walks this JSON path: + +| Path segment | What it accesses | +|---|---| +| `apiResponse` | The full response variable | +| `.body` | The A2A task response body | +| `.artifacts[0]` | The first artifact — your `investigation_result` from `server.py` | +| `.parts[0]` | The first part of that artifact | +| `.text` | The actual text content of the investigation result | + +> šŸ’” **This maps directly to what you built in Exercise 07.** In `server.py`, your `InvestigatorExecutor` emits a `TaskArtifactUpdateEvent` with `artifactId="investigation_result"` and a `TextPart` — the output from `investigator_graph.invoke(...)`. That's exactly what Joule reads here through `artifacts[0].parts[0].text`. + +### Step 5: Create the Digital Assistant Descriptor + +The `da.sapdas.yaml` file is the top-level manifest. It tells the Joule CLI which capabilities to include when compiling and deploying. + +šŸ‘‰ Create a new file [`/project/Python-LangGraph/starter-project/joule/da.sapdas.yaml`](/project/Python-LangGraph/starter-project/joule/da.sapdas.yaml): + +```yaml +schema_version: 1.4.0 + +name: investigator_assistant + +capabilities: + - type: local + folder: ./investigator_capability +``` + +> šŸ’” **Understanding the DA file:** +> +> | Field | Purpose | +> |---|---| +> | `schema_version: 1.4.0` | Version of the Digital Assistant schema (separate from the capability schema). | +> | `name` | The name of the assistant. Used when deploying with `joule deploy -n`. | +> | `capabilities` | List of capabilities to include. `type: local` means the capability is in a local folder (as opposed to a remote reference). | +> | `folder` | Path to the capability directory, relative to this file. | + +--- + +## Deploy the Capability + +With all YAML files in place, you can now compile and deploy the capability to your Joule instance. + +### Step 1: Navigate to the Joule Directory + +šŸ‘‰ Change to the directory containing your `da.sapdas.yaml`: + +```bash +cd project/Python-LangGraph/starter-project/joule +``` + +### Step 2: Compile and Deploy + +šŸ‘‰ Run the following command to compile the YAML files and deploy them as a test assistant: + +```bash +joule deploy -c -n "investigator_assistant_XX" +``` + +> āš ļø **Replace `XX`** with your participant number (e.g., `investigator_assistant_01`). + +> šŸ’” **Understanding the flags:** +> +> | Flag | Purpose | +> |---|---| +> | `-c` | Compile before deploying. This validates your YAML files against the schema, checks for errors, and packages them into a `.daar` archive (Design-time Artifact Archive). | +> | `-n "investigator_assistant_XX"` | Name of the test assistant to create. Using `-n` creates a standalone assistant for testing — it doesn't affect the production Joule instance. | + +You should see output similar to: + +```text +āœ” Building designtime artifact (investigator_capability) +āœ” Trigger compilation +āœ” Compiled + +Detailed logs: +WARNINGS: +Message: DTA did not define an optional i18n folder +Path: +Category: I18N +Severity: LOW + +āœ” Downloaded runtime artifact (joule.ext_investigator_capability_1.0.0.daar) +āœ” Building runtime artifact + > joule.ext_investigator_capability_1.0.0.daar added to the RTA +āœ” Triggering deployment (investigator_assistant_XX) +āœ” Your digital assistant (investigator_assistant_XX) deployed successfully +``` + +> šŸ’” **The i18n warning is expected** — it just means you haven't added localization files, which are optional for this exercise. + +### Step 3: Verify Deployment + +šŸ‘‰ List your deployed assistants to confirm: + +```bash +joule list +``` + +You should see `investigator_assistant_XX` in the list. + +--- + +## Test in Joule + +### Step 1: Launch the Test Client + +šŸ‘‰ Open the Joule web client for your test assistant: + +```bash +joule launch "investigator_assistant_XX" +``` + +This opens a browser tab with the Joule chat interface connected to your test assistant. + +### Step 2: Send a Test Message + +šŸ‘‰ In the Joule chat, type: + +```text +Investigate the art theft at the museum. The suspects are Sophie Dubois, Marcus Chen, and Viktor Petrov. +``` + +> āš ļø **The investigation takes 1–2 minutes** because the full LangGraph pipeline runs on Cloud Foundry: the Appraiser calls RPT-1, the Evidence Analyst queries the Grounding Service, and the Lead Detective synthesizes the findings. Joule's synchronous timeout is 60 seconds — if your graph takes longer, you may see a timeout error. If this happens, try a simpler request or check the troubleshooting section below. + +### Step 3: Review the Response + +If everything is connected correctly, Joule displays the investigation result directly in the chat — the same markdown report your graph generates, now accessible through a conversational interface. + +> šŸ’” **What just happened behind the scenes:** +> 1. You typed a question in Joule +> 2. Joule matched your question to `investigate_scenario` based on the description +> 3. The scenario triggered `investigate_function` +> 4. The function's `agent-request` action called your BTP Destination +> 5. The destination routed to your CF app's A2A endpoint +> 6. Your `InvestigatorExecutor` ran `investigator_graph.invoke(...)` — the full LangGraph pipeline +> 7. The result came back as an A2A artifact +> 8. The function's `message` action extracted the text and displayed it in Joule + +--- + +## Understanding What Just Happened + +### The Full Architecture + +You now have a complete end-to-end pipeline from Joule's chat UI to your LangGraph system: + +```text +User + │ + ā–¼ +SAP Joule (Chat UI) + │ + ā”œā”€ā”€ Scenario Match → investigate_scenario + │ │ + │ ā–¼ + │ investigate_function + │ │ + │ ā”œā”€ā”€ agent-request (A2A over HTTPS) + │ │ │ + │ │ ā–¼ + │ │ BTP Destination: INVESTIGATOR_AGENT_XX + │ │ │ + │ │ ā–¼ + │ │ CF App: investigator-graph- + │ │ │ + │ │ ā–¼ + │ │ InvestigatorExecutor → investigator_graph.invoke(...) + │ │ ā”œā”€ā”€ Appraiser Node (RPT-1) + │ │ ā”œā”€ā”€ Evidence Analyst Node (Grounding) + │ │ └── Lead Detective Node (GPT-4o) + │ │ │ + │ │ ā–¼ + │ │ A2A Response (artifacts[0].parts[0].text) + │ │ + │ ā”œā”€ā”€ message → Displays result to user + │ │ + ā–¼ ā–¼ +User sees investigation result in Joule chat +``` + +### How Each Layer Communicates + +| Step | Component | Protocol | What happens | +|---|---|---|---| +| 1 | User → Joule | Chat UI | User types a question in natural language | +| 2 | Joule → Scenario | Internal | Joule matches the question to the best scenario description | +| 3 | Scenario → Function | Internal | The matched scenario triggers the linked function | +| 4 | Function → Destination | HTTP | The `agent-request` action resolves the system alias to a BTP Destination | +| 5 | Destination → CF App | HTTPS | The destination routes to your Cloud Foundry app URL | +| 6 | CF App → LangGraph | Internal | The `InvestigatorExecutor` runs `investigator_graph.invoke(...)` | +| 7 | LangGraph → Response | A2A JSON-RPC | The result is returned as an A2A `TaskArtifactUpdateEvent` | +| 8 | Function → User | Chat UI | The `message` action extracts the text and shows it in Joule | + +--- + +## Key Takeaways + +- **Joule capabilities** are YAML-based packages that extend Joule with custom skills — no runtime code needed on the Joule side +- **`agent-request`** is the action type that bridges Joule to external agents via the A2A protocol +- **`agent_type: remote`** distinguishes code-based agents (your CF app) from content-based agents built inside Joule +- **System aliases** decouple the capability from the physical URL — the BTP Destination handles routing, so you can change the URL without redeploying the capability +- **`schema_version: 3.28.0`** is the minimum DTA version required for code-based agent support +- **The Joule Studio CLI** provides a complete workflow: `login` → `deploy` → `launch` → `update` +- **The A2A protocol** enables framework-agnostic integration — Joule doesn't care whether your agent uses LangGraph, CrewAI, or any other framework + +--- + +## Next Steps + +1. āœ… [Set up your development space](01-setup-dev-space.md) +2. āœ… [Build a basic agent](02-build-a-basic-agent.md) +3. āœ… [Add custom tools](03-add-your-first-tool.md) +4. āœ… [Build a multi-agent system](04-building-multi-agent-system.md) +5. āœ… [Add the Grounding Service](05-add-the-grounding-service.md) +6. āœ… [Solve the crime](06-solve-the-crime.md) +7. āœ… [Deploy your agent to CF with A2A](07-deploy-agent-to-cf.md) +8. āœ… [Integrate your agent into SAP Joule](08-integrate-agent-into-joule.md) (this exercise) + +šŸŽ‰ **Congratulations!** You've completed the full CodeJam. You built a multi-agent AI system from scratch using LangGraph, deployed it to Cloud Foundry as an A2A server, and integrated it into SAP Joule — making it accessible to business users through natural language. + +--- + +## Troubleshooting + +**Issue**: `joule: command not found` after installing + +- **Solution**: The global npm bin directory may not be in your PATH. Run `npm config get prefix` — the result + `/bin` should be in your PATH. On macOS/Linux, add it to your shell profile (e.g., `export PATH="$(npm config get prefix)/bin:$PATH"`). + +**Issue**: `joule login` fails with an authentication error + +- **Solution**: Double-check all credentials with your instructor. Common mistakes: + - Trailing spaces in the Auth URL or API URL + - Wrong Client ID / Client Secret pair + - Password with special characters that need escaping (`$` → `\$`) + - Expired or incorrect IAS credentials + +**Issue**: `joule deploy` fails with a schema version error + +- **Solution**: Ensure `schema_version: 3.28.0` in `capability.sapdas.yaml`. The `agent-request` action type requires this minimum version. Lower versions like `3.26.0` will not recognize the action. + +**Issue**: `joule deploy` fails with "User is not authorized to deploy sap capabilities" + +- **Solution**: Your `namespace` must be `joule.ext`. Any other namespace (e.g., `joule.custom`, `my.namespace`) is treated as an SAP-internal namespace and requires special permissions. + +**Issue**: Deployment fails with "destination not found" or agent-request returns an error + +- **Solution**: Verify that: + 1. The BTP Destination name (`INVESTIGATOR_AGENT_XX`) matches **exactly** the `destination` value in `capability.sapdas.yaml`'s `system_aliases` block + 2. The system alias key is `INVESTIGATOR_AGENT` (no XX) in both `capability.sapdas.yaml` and `investigate_function.yaml` + 3. The destination exists in the BTP cockpit under Connectivity → Destinations + 4. The destination URL is correct and your CF app is running (`cf apps`) + +**Issue**: Joule responds with an empty message or an error + +- **Solution**: Your CF app may have crashed or returned an unexpected response. Check: + - App status: `cf apps` + - App logs: `cf logs investigator-graph- --recent` + - Health endpoint: `curl https:///health` + - Ensure the Agent Card is accessible: `curl https:///.well-known/agent-card.json` + +**Issue**: "Scenario not matched" — Joule doesn't recognize your question + +- **Solution**: Joule's intent matching depends on the scenario description. Try rephrasing your question to include keywords from the description: "investigate", "art theft", "stolen items", "suspects", "evidence". If testing with a standalone assistant, ensure the capability was deployed successfully with `joule list`. + +**Issue**: Timeout error — the agent takes too long to respond + +- **Solution**: Joule expects a synchronous response within **60 seconds**. The full LangGraph pipeline may take longer. Options: + - Try a simpler request that requires less processing + - Check `cf logs investigator-graph- --recent` to see where time is spent + - Ensure your CF app has enough memory (1024M in `manifest.yml`) + - Consider reducing the number of grounding service queries in the Evidence Analyst's task + +**Issue**: YAML indentation or compile errors + +- **Solution**: YAML is whitespace-sensitive — use **2 spaces** for indentation, never tabs. Common mistakes: + - Mixed tabs and spaces + - Missing space after `:` in key-value pairs + - Incorrect nesting of `actions` under `action_groups` + - Run `joule lint` to check for errors before deploying + +--- + +## Resources + +- [Joule Development Guide](https://help.sap.com/docs/joule/joule-development-guide-ba88d1ec6a1b442098863d577c19b0c0/joule-development) +- [Code-Based Agents (Bring Your Own Agent)](https://help.sap.com/docs/joule/joule-development-guide-ba88d1ec6a1b442098863d577c19b0c0/code-based-agents-bring-your-own-agent) +- [Install and Update the Joule Studio CLI](https://help.sap.com/docs/joule/joule-development-guide-ba88d1ec6a1b442098863d577c19b0c0/install-and-update-joule-studio-cli) +- [A2A Protocol Specification](https://a2a-protocol.org/latest/) +- [SAP BTP Destinations Documentation](https://help.sap.com/docs/connectivity/sap-btp-connectivity-cf/create-http-destinations) +- [Blog: Joule A2A — Connect Code-Based Agents into Joule](https://community.sap.com/t5/technology-blog-posts-by-sap/joule-a2a-connect-code-based-agents-into-joule/ba-p/14329279) diff --git a/project/JavaScript/solution/joule/da.sapdas.yaml b/project/JavaScript/solution/joule/da.sapdas.yaml new file mode 100644 index 0000000..e642b74 --- /dev/null +++ b/project/JavaScript/solution/joule/da.sapdas.yaml @@ -0,0 +1,7 @@ +schema_version: 1.4.0 + +name: investigator_assistant + +capabilities: + - type: local + folder: ./investigator_capability diff --git a/project/JavaScript/solution/joule/investigator_capability/capability.sapdas.yaml b/project/JavaScript/solution/joule/investigator_capability/capability.sapdas.yaml new file mode 100644 index 0000000..2028a72 --- /dev/null +++ b/project/JavaScript/solution/joule/investigator_capability/capability.sapdas.yaml @@ -0,0 +1,12 @@ +schema_version: 3.28.0 + +metadata: + namespace: joule.ext + name: investigator_capability + version: 1.0.0 + display_name: Investigator_Capability + description: Capability containing the investigator graph agent for art theft investigation + +system_aliases: + INVESTIGATOR_AGENT: + destination: INVESTIGATOR_AGENT diff --git a/project/JavaScript/solution/joule/investigator_capability/functions/investigate_function.yaml b/project/JavaScript/solution/joule/investigator_capability/functions/investigate_function.yaml new file mode 100644 index 0000000..c5100fe --- /dev/null +++ b/project/JavaScript/solution/joule/investigator_capability/functions/investigate_function.yaml @@ -0,0 +1,13 @@ +action_groups: + - actions: + - type: status-update + message: Investigating... + - type: agent-request + system_alias: INVESTIGATOR_AGENT + agent_type: remote + result_variable: "apiResponse" + - type: message + message: + type: text + markdown: true + content: "" diff --git a/project/JavaScript/solution/joule/investigator_capability/scenarios/investigate_scenario.yaml b/project/JavaScript/solution/joule/investigator_capability/scenarios/investigate_scenario.yaml new file mode 100644 index 0000000..b553518 --- /dev/null +++ b/project/JavaScript/solution/joule/investigator_capability/scenarios/investigate_scenario.yaml @@ -0,0 +1,9 @@ +description: >- + This skill investigates art theft cases by appraising the value of stolen + items, analyzing evidence from security logs, bank records, phone records, + and criminal histories, and identifying the most likely suspect based on + available evidence. It coordinates multiple specialist agents to deliver + a comprehensive investigation report. +target: + name: investigate_function + type: function diff --git a/project/Python-LangGraph/solution/joule/da.sapdas.yaml b/project/Python-LangGraph/solution/joule/da.sapdas.yaml new file mode 100644 index 0000000..e642b74 --- /dev/null +++ b/project/Python-LangGraph/solution/joule/da.sapdas.yaml @@ -0,0 +1,7 @@ +schema_version: 1.4.0 + +name: investigator_assistant + +capabilities: + - type: local + folder: ./investigator_capability diff --git a/project/Python-LangGraph/solution/joule/investigator_capability/capability.sapdas.yaml b/project/Python-LangGraph/solution/joule/investigator_capability/capability.sapdas.yaml new file mode 100644 index 0000000..2028a72 --- /dev/null +++ b/project/Python-LangGraph/solution/joule/investigator_capability/capability.sapdas.yaml @@ -0,0 +1,12 @@ +schema_version: 3.28.0 + +metadata: + namespace: joule.ext + name: investigator_capability + version: 1.0.0 + display_name: Investigator_Capability + description: Capability containing the investigator graph agent for art theft investigation + +system_aliases: + INVESTIGATOR_AGENT: + destination: INVESTIGATOR_AGENT diff --git a/project/Python-LangGraph/solution/joule/investigator_capability/functions/investigate_function.yaml b/project/Python-LangGraph/solution/joule/investigator_capability/functions/investigate_function.yaml new file mode 100644 index 0000000..c5100fe --- /dev/null +++ b/project/Python-LangGraph/solution/joule/investigator_capability/functions/investigate_function.yaml @@ -0,0 +1,13 @@ +action_groups: + - actions: + - type: status-update + message: Investigating... + - type: agent-request + system_alias: INVESTIGATOR_AGENT + agent_type: remote + result_variable: "apiResponse" + - type: message + message: + type: text + markdown: true + content: "" diff --git a/project/Python-LangGraph/solution/joule/investigator_capability/scenarios/investigate_scenario.yaml b/project/Python-LangGraph/solution/joule/investigator_capability/scenarios/investigate_scenario.yaml new file mode 100644 index 0000000..b553518 --- /dev/null +++ b/project/Python-LangGraph/solution/joule/investigator_capability/scenarios/investigate_scenario.yaml @@ -0,0 +1,9 @@ +description: >- + This skill investigates art theft cases by appraising the value of stolen + items, analyzing evidence from security logs, bank records, phone records, + and criminal histories, and identifying the most likely suspect based on + available evidence. It coordinates multiple specialist agents to deliver + a comprehensive investigation report. +target: + name: investigate_function + type: function From e31c6551fd11eaaae092f490ac1750951f5a86c3 Mon Sep 17 00:00:00 2001 From: D062408 Date: Wed, 8 Jul 2026 12:21:32 +0200 Subject: [PATCH 6/6] add exercise 06 (Perplexity web search) across all three tracks Ports commits 73cc89a and c4cb077 (Maximilian Hoffmann) to the current branch, extended to the Python-LangGraph track which did not exist at the time of those commits. Exercises 06/07/08 are renumbered to 07/08/09 to make room. - New Intelligence Researcher agent/node in each track calls sap/sonar-pro via SAP AI Core to search the web for suspect backgrounds and criminal patterns; Lead Detective now synthesises three sources instead of two - New exercise 06-discover-connected-crimes.md for all three tracks - Solution code updated: investigator_crew.py, investigator_graph.py, tools.ts / types.ts / agentConfigs.ts / investigationWorkflow.ts - All cross-references in exercises/ and README updated (17 files) --- README.md | 17 +- .../JavaScript/02-build-a-basic-agent.md | 3 +- .../JavaScript/03-add-your-first-tool.md | 3 +- .../04-building-multi-agent-system.md | 7 +- .../05-add-the-grounding-service.md | 5 +- .../06-discover-connected-crimes.md | 381 +++++++++++++++ ...lve-the-crime.md => 07-solve-the-crime.md} | 2 +- ...o-cf-ts.md => 08-deploy-agent-to-cf-ts.md} | 5 +- ...le.md => 09-integrate-agent-into-joule.md} | 15 +- .../05-add-the-grounding-service.md | 5 +- .../06-discover-connected-crimes.md | 403 ++++++++++++++++ ...lve-the-crime.md => 07-solve-the-crime.md} | 4 +- ...gent-to-cf.md => 08-deploy-agent-to-cf.md} | 0 ...le.md => 09-integrate-agent-into-joule.md} | 15 +- .../Python/04-building-multi-agent-system.md | 5 +- .../Python/05-add-the-grounding-service.md | 5 +- .../Python/06-discover-connected-crimes.md | 446 ++++++++++++++++++ ...lve-the-crime.md => 07-solve-the-crime.md} | 2 +- ...gent-to-cf.md => 08-deploy-agent-to-cf.md} | 7 +- ...le.md => 09-integrate-agent-into-joule.md} | 15 +- exercises/readme.md | 17 +- .../JavaScript/solution/src/agentConfigs.ts | 34 +- .../solution/src/investigationWorkflow.ts | 54 ++- project/JavaScript/solution/src/tools.ts | 38 ++ project/JavaScript/solution/src/types.ts | 4 + .../solution/config/agents.py | 19 +- .../solution/investigator_graph.py | 69 ++- project/Python-LangGraph/solution/main.py | 6 + project/Python-LangGraph/solution/server.py | 1 + project/Python/solution/config/agents.yaml | 18 +- project/Python/solution/config/tasks.yaml | 34 +- project/Python/solution/investigator_crew.py | 59 ++- 32 files changed, 1625 insertions(+), 73 deletions(-) create mode 100644 exercises/JavaScript/06-discover-connected-crimes.md rename exercises/JavaScript/{06-solve-the-crime.md => 07-solve-the-crime.md} (99%) rename exercises/JavaScript/{07-deploy-agent-to-cf-ts.md => 08-deploy-agent-to-cf-ts.md} (99%) rename exercises/JavaScript/{08-integrate-agent-into-joule.md => 09-integrate-agent-into-joule.md} (98%) create mode 100644 exercises/Python-LangGraph/06-discover-connected-crimes.md rename exercises/Python-LangGraph/{06-solve-the-crime.md => 07-solve-the-crime.md} (98%) rename exercises/Python-LangGraph/{07-deploy-agent-to-cf.md => 08-deploy-agent-to-cf.md} (100%) rename exercises/Python-LangGraph/{08-integrate-agent-into-joule.md => 09-integrate-agent-into-joule.md} (98%) create mode 100644 exercises/Python/06-discover-connected-crimes.md rename exercises/Python/{06-solve-the-crime.md => 07-solve-the-crime.md} (99%) rename exercises/Python/{07-deploy-agent-to-cf.md => 08-deploy-agent-to-cf.md} (98%) rename exercises/Python/{08-integrate-agent-into-joule.md => 09-integrate-agent-into-joule.md} (98%) diff --git a/README.md b/README.md index 5293859..8e31811 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,10 @@ If you are unsure which path to choose, ask the instructor for guidance. - [Exercise 03 - Build your first agent tool](./exercises/Python/03-add-your-first-tool.md) - [Exercise 04 - Building a multi-agent system](./exercises/Python/04-building-multi-agent-system.md) - [Exercise 05 - Add the Grounding service](./exercises/Python/05-add-the-grounding-service.md) -- [Exercise 06 - Use your AI Agents to solve the crime](./exercises/Python/06-solve-the-crime.md) +- [Exercise 06 - Discover Connected Crimes with Web Search](./exercises/Python/06-discover-connected-crimes.md) +- [Exercise 07 - Use your AI Agents to solve the crime](./exercises/Python/07-solve-the-crime.md) +- [Exercise 08 - Deploy your agent to Cloud Foundry with A2A](./exercises/Python/08-deploy-agent-to-cf.md) +- [Exercise 09 - Integrate your agent into SAP Joule](./exercises/Python/09-integrate-agent-into-joule.md) ### Python (LangGraph + LiteLLM) @@ -60,8 +63,10 @@ If you are unsure which path to choose, ask the instructor for guidance. - [Exercise 03 - Build your first agent tool](./exercises/Python-LangGraph/03-add-your-first-tool.md) - [Exercise 04 - Building a multi-agent system](./exercises/Python-LangGraph/04-building-multi-agent-system.md) - [Exercise 05 - Add the Grounding service](./exercises/Python-LangGraph/05-add-the-grounding-service.md) -- [Exercise 06 - Use your AI Agents to solve the crime](./exercises/Python-LangGraph/06-solve-the-crime.md) -- [Exercise 07 - Deploy your agent to Cloud Foundry with A2A](./exercises/Python-LangGraph/07-deploy-agent-to-cf.md) +- [Exercise 06 - Discover Connected Crimes with Web Search](./exercises/Python-LangGraph/06-discover-connected-crimes.md) +- [Exercise 07 - Use your AI Agents to solve the crime](./exercises/Python-LangGraph/07-solve-the-crime.md) +- [Exercise 08 - Deploy your agent to Cloud Foundry with A2A](./exercises/Python-LangGraph/08-deploy-agent-to-cf.md) +- [Exercise 09 - Integrate your agent into SAP Joule](./exercises/Python-LangGraph/09-integrate-agent-into-joule.md) ### JavaScript (LangGraph + SAP Cloud SDK for AI) @@ -71,8 +76,10 @@ If you are unsure which path to choose, ask the instructor for guidance. - [Exercise 03 - Build your first agent tool](./exercises/JavaScript/03-add-your-first-tool.md) - [Exercise 04 - Building a multi-agent system](./exercises/JavaScript/04-building-multi-agent-system.md) - [Exercise 05 - Add the Grounding service](./exercises/JavaScript/05-add-the-grounding-service.md) -- [Exercise 06 - Use your AI Agents to solve the crime](./exercises/JavaScript/06-solve-the-crime.md) -- [Exercise 07 - Deploy your agent to Cloud Foundry with A2A](./exercises/JavaScript/07-deploy-agent-to-cf-ts.md) +- [Exercise 06 - Discover Connected Crimes with Web Search](./exercises/JavaScript/06-discover-connected-crimes.md) +- [Exercise 07 - Use your AI Agents to solve the crime](./exercises/JavaScript/07-solve-the-crime.md) +- [Exercise 08 - Deploy your agent to Cloud Foundry with A2A](./exercises/JavaScript/08-deploy-agent-to-cf-ts.md) +- [Exercise 09 - Integrate your agent into SAP Joule](./exercises/JavaScript/09-integrate-agent-into-joule.md) The instructor will start you on the first exercise. Proceed to the next exercise once the instructor tells you to. diff --git a/exercises/JavaScript/02-build-a-basic-agent.md b/exercises/JavaScript/02-build-a-basic-agent.md index ae4e12d..d7974d6 100644 --- a/exercises/JavaScript/02-build-a-basic-agent.md +++ b/exercises/JavaScript/02-build-a-basic-agent.md @@ -399,7 +399,8 @@ In the following exercises, you will: 4. šŸ“Œ [Add custom tools](03-add-your-first-tool.md) to your agents so they can access external data 5. šŸ“Œ [Build a multi-agent workflow](04-building-multi-agent-system.md) with LangGraph 6. šŸ“Œ [Integrate the Grounding Service](05-add-the-grounding-service.md) for evidence analysis -7. šŸ“Œ [Solve the museum art theft mystery](06-solve-the-crime.md) using your fully-featured agent team +7. šŸ“Œ [Discover Connected Crimes](06-discover-connected-crimes.md): Add web search with Perplexity sonar-pro +8. šŸ“Œ [Solve the museum art theft mystery](07-solve-the-crime.md) using your fully-featured agent team --- diff --git a/exercises/JavaScript/03-add-your-first-tool.md b/exercises/JavaScript/03-add-your-first-tool.md index f1b76ce..2bd83e0 100644 --- a/exercises/JavaScript/03-add-your-first-tool.md +++ b/exercises/JavaScript/03-add-your-first-tool.md @@ -496,7 +496,8 @@ In the following exercises, you will: 4. āœ… Add custom tools to your agents so they can access external data (this exercise) 5. šŸ“Œ [Build a multi-agent workflow](04-building-multi-agent-system.md) with LangGraph 6. šŸ“Œ [Integrate the Grounding Service](05-add-the-grounding-service.md) for evidence analysis -7. šŸ“Œ [Solve the museum art theft mystery](06-solve-the-crime.md) using your fully-featured agent team +7. šŸ“Œ [Discover Connected Crimes](06-discover-connected-crimes.md): Add web search with Perplexity sonar-pro +8. šŸ“Œ [Solve the museum art theft mystery](07-solve-the-crime.md) using your fully-featured agent team --- diff --git a/exercises/JavaScript/04-building-multi-agent-system.md b/exercises/JavaScript/04-building-multi-agent-system.md index 1578b17..0d2c102 100644 --- a/exercises/JavaScript/04-building-multi-agent-system.md +++ b/exercises/JavaScript/04-building-multi-agent-system.md @@ -355,7 +355,7 @@ npx tsx src/main.ts > šŸ’” **Note:** The Evidence Analyst currently produces placeholder output because it doesn't have access to real evidence documents yet. You'll connect it to the Grounding Service in Exercise 05. -> šŸ’” **Expected output:** The final report will say "Investigation completed but no conclusion was reached." — this is intentional. The Lead Detective node that produces `final_conclusion` hasn't been added yet; you'll implement it in Exercise 06. +> šŸ’” **Expected output:** The final report will say "Investigation completed but no conclusion was reached." — this is intentional. The Lead Detective node that produces `final_conclusion` hasn't been added yet; you'll implement it in Exercise 07. --- @@ -417,7 +417,7 @@ When you call `workflow.kickoff(inputs)`: - **Collaboration** — Agents can build upon each other's work - Sequential processing allows later nodes to use earlier results via shared state - - The `context` pattern (used in Exercise 06) enables explicit data sharing + - The `context` pattern (used in Exercise 07) enables explicit data sharing - **Maintainability** — Clear separation of concerns - Agent "personality" (goals, roles) lives in `agentConfigs.ts` @@ -464,7 +464,8 @@ The Evidence Analyst can't access actual evidence yet. In Exercise 05, you'll in 4. āœ… [Add custom tools](03-add-your-first-tool.md) (RPT-1 model integration) 5. āœ… [Build a multi-agent workflow](04-building-multi-agent-system.md) (this exercise) 6. šŸ“Œ [Add the Grounding Service](05-add-the-grounding-service.md): Give your Evidence Analyst access to real documents -7. šŸ“Œ [Solve the crime](06-solve-the-crime.md): Add a Lead Detective to combine findings and crack the case +7. šŸ“Œ [Discover Connected Crimes](06-discover-connected-crimes.md): Add web search with Perplexity sonar-pro +8. šŸ“Œ [Solve the crime](07-solve-the-crime.md): Add a Lead Detective to combine findings and crack the case --- diff --git a/exercises/JavaScript/05-add-the-grounding-service.md b/exercises/JavaScript/05-add-the-grounding-service.md index 705ac72..383398d 100644 --- a/exercises/JavaScript/05-add-the-grounding-service.md +++ b/exercises/JavaScript/05-add-the-grounding-service.md @@ -407,7 +407,8 @@ In the TypeScript approach, the orchestration pipeline handles the retrieval **a 4. āœ… [Add custom tools](03-add-your-first-tool.md) to your agents 5. āœ… [Build a multi-agent workflow](04-building-multi-agent-system.md) 6. āœ… Integrate the Grounding Service (this exercise) -7. šŸ“Œ [Solve the museum art theft mystery](06-solve-the-crime.md) using your fully-featured agent team +7. šŸ“Œ [Add web search capabilities](06-discover-connected-crimes.md) to gather external intelligence +8. šŸ“Œ [Solve the museum art theft mystery](07-solve-the-crime.md) using your fully-featured agent team --- @@ -441,4 +442,4 @@ In the TypeScript approach, the orchestration pipeline handles the retrieval **a - [SAP Cloud SDK for AI — Orchestration Package](https://github.com/SAP/ai-sdk-js/tree/main/packages/orchestration) - [LangGraph.js Documentation](https://langchain-ai.github.io/langgraphjs/) -[Next exercise](06-solve-the-crime.md) +[Next exercise](06-discover-connected-crimes.md) diff --git a/exercises/JavaScript/06-discover-connected-crimes.md b/exercises/JavaScript/06-discover-connected-crimes.md new file mode 100644 index 0000000..99d6fd6 --- /dev/null +++ b/exercises/JavaScript/06-discover-connected-crimes.md @@ -0,0 +1,381 @@ +# Discover Connected Crimes with Web Search (TypeScript/LangGraph) + +## Overview + +In Exercise 05, you learned to search internal documents using the Grounding Service. Your Evidence Analyst can now retrieve factual evidence from security logs, bank records, and termination letters without hallucination. + +But investigations don't happen in a vacuum. What if similar crimes happened elsewhere? What if your suspects have public criminal records? What if this heist is part of a larger criminal network? + +In this exercise, you'll add **web search capabilities** using Perplexity's **sonar-pro model** to gather external intelligence and discover patterns across the internet. + +--- + +## Understand Web Search with Sonar-Pro + +### Why Do We Need Web Search? + +Your current investigation system is powerful but limited to **internal data**: + +| **What You Can Do Now** | **What You Can't Do Yet** | +| ----------------------------------------------- | ---------------------------------------------------- | +| āœ… Search museum's internal evidence documents | āŒ Search public criminal databases | +| āœ… Retrieve bank records, security logs | āŒ Find similar crimes in other cities | +| āœ… Analyze suspects based on internal evidence | āŒ Check if suspects have public criminal records | +| āœ… Ground responses in factual documents | āŒ Discover criminal network connections | +| āœ… Avoid hallucination for internal data | āŒ Monitor online art markets for stolen items | + +**The Problem**: Real investigations combine **internal evidence** (what happened here) with **external intelligence** (what's happening elsewhere). Your agents need both! + +### Document Grounding vs. Web Search + +| Aspect | Document Grounding (Exercise 05) | Web Search (This Exercise) | +| ---------------------- | ---------------------------------------- | ----------------------------------------- | +| **Data Source** | Internal documents (pre-uploaded) | Real-time web information | +| **Coverage** | Organization-specific evidence | Global public information | +| **Freshness** | Historical documents (static) | Current information (updated constantly) | +| **Use Cases** | Internal logs, private records, policies | News, criminal records, pattern analysis | +| **Search Method** | Vector similarity (semantic) | Web search + LLM synthesis | +| **Tool Function** | `callGroundingServiceTool()` | `callSonarProSearchTool()` | +| **When to Use** | "What does our evidence say?" | "What do public records show?" | + +> šŸŽÆ **Key Insight**: These are **not alternatives** — they work **together**! + +### How Sonar-Pro Integrates with SAP Cloud SDK for AI + +Sonar-Pro is called through the **OrchestrationClient** using the same `promptTemplating` API you already use for GPT-4o and Claude: + +```typescript +import { OrchestrationClient } from "@sap-ai-sdk/orchestration"; + +const webSearchClient = new OrchestrationClient( + { + promptTemplating: { + model: { + name: "sonar-pro", // Perplexity's web search model + params: { + temperature: 0.2, // Lower for factual results + }, + }, + prompt: { + template: [ + { + role: "system", + content: "Search for factual information with citations", + }, + { role: "user", content: "{{?search_query}}" }, + ], + }, + }, + }, + { resourceGroup: process.env.RESOURCE_GROUP }, +); +``` + +**Key Difference**: +- `model.name: "gpt-4o"` → Regular reasoning LLM +- `model.name: "sonar-pro"` → Web search-enabled LLM +- Both use the same `OrchestrationClient` API! + +> āš ļø **Before continuing**: Verify that `sonar-pro` is available in your SAP AI Core resource group. Go to **SAP AI Launchpad → Generative AI Hub → Models** and confirm you can see a Perplexity sonar-pro deployment. If it is not listed, ask your workshop instructor to enable it. + +--- + +## Add The Web Search Tool + +### Step 1: Create the Sonar-Pro Search Tool + +šŸ‘‰ Open [`/project/JavaScript/starter-project/src/tools.ts`](/project/JavaScript/starter-project/src/tools.ts) + +šŸ‘‰ Add this tool **after** the `callGroundingServiceTool` function: + +```typescript +const webSearchClient = new OrchestrationClient( + { + promptTemplating: { + model: { + name: "sonar-pro", + params: { + temperature: 0.2, + }, + }, + prompt: { + template: [ + { + role: "system", + content: + "You are a web search assistant specializing in criminal intelligence. Search for accurate, recent information and always provide source citations with URLs and dates.", + }, + { role: "user", content: "{{?search_query}}" }, + ], + }, + }, + }, + { resourceGroup: process.env.RESOURCE_GROUP }, +); + +export async function callSonarProSearchTool(search_query: string): Promise { + try { + const response = await webSearchClient.chatCompletion({ + placeholderValues: { search_query }, + }); + return response.getContent() ?? "No search results found"; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + console.error("āŒ Sonar-pro web search failed:", errorMessage); + if (error instanceof Error && error.stack) console.error(error.stack); + return `Error calling sonar-pro web search: ${errorMessage}`; + } +} +``` + +> šŸ’” **Understanding the Web Search Tool:** +> +> - `model.name: "sonar-pro"` — Perplexity's web search-enabled model, called through SAP AI Core +> - `temperature: 0.2` — Lower temperature for factual, consistent results +> - `{{?search_query}}` — Template placeholder receives the search query +> - Returns synthesized answers with web source citations + +--- + +## Add The Intelligence Researcher Agent + +### Step 1: Update Type Definitions + +šŸ‘‰ Open [`/project/JavaScript/starter-project/src/types.ts`](/project/JavaScript/starter-project/src/types.ts) + +šŸ‘‰ Add the `intelligence_report` field to the `AgentState` annotation **after** `evidence_analysis` and **before** `final_conclusion`: + +```typescript + intelligence_report: Annotation({ + reducer: (_, update) => update, + default: () => undefined, + }), +``` + +### Step 2: Add Agent Configuration + +šŸ‘‰ Open [`/project/JavaScript/starter-project/src/agentConfigs.ts`](/project/JavaScript/starter-project/src/agentConfigs.ts) + +šŸ‘‰ Add this configuration **after** `evidenceAnalyst` and **before** `leadDetective`: + +```typescript + intelligenceResearcher: { + systemPrompt: (suspectNames: string) => `You are an Open-Source Intelligence (OSINT) Researcher. + You are an OSINT specialist who excels at finding patterns across multiple crime scenes. + You search public databases, news archives, and criminal records to connect seemingly isolated incidents. + Your expertise has uncovered several international art theft rings, and you know how to distinguish professional criminals from amateurs. + + Your goal: Search the web for similar art thefts, criminal patterns, and suspect backgrounds to determine if this heist is part of a larger criminal network + + Analyze the suspects: ${suspectNames} + + Search for: + 1. Public criminal records or prior convictions for each suspect + 2. Similar art theft incidents with the same modus operandi (insider job, no forced entry) + 3. Connections to known art theft rings or criminal networks + 4. News reports or public information about any of the suspects + 5. Recent museum heists in Europe with similar patterns`, + }, +``` + +šŸ‘‰ Also update the `leadDetective` system prompt to include `intelligenceReport` as a third parameter before `suspectNames`: + +```typescript + leadDetective: { + systemPrompt: ( + appraisalResult: string, + evidenceAnalysis: string, + intelligenceReport: string, + suspectNames: string, + ) => + `You are the lead detective on this high-profile art theft case... + + 1. INSURANCE APPRAISAL: ${appraisalResult} + 2. EVIDENCE ANALYSIS (Internal Documents): ${evidenceAnalysis} + 3. INTELLIGENCE REPORT (Web Search): ${intelligenceReport} + 4. SUSPECTS: ${suspectNames} + ...` + } +``` + +(Use the full prompt text from the solution — don't truncate it in the actual file.) + +### Step 3: Add the Intelligence Researcher Node + +šŸ‘‰ Open [`/project/JavaScript/starter-project/src/investigationWorkflow.ts`](/project/JavaScript/starter-project/src/investigationWorkflow.ts) + +šŸ‘‰ **First**, update the import to include the new tool: + +```typescript +import { callRPT1Tool, callGroundingServiceTool, callSonarProSearchTool } from "./tools.js"; +``` + +šŸ‘‰ **Second**, add the Intelligence Researcher node **after** `evidenceAnalystNode` and **before** `leadDetectiveNode`: + +```typescript + private async intelligenceResearcherNode(state: AgentStateType): Promise> { + console.log("\nšŸ” Intelligence Researcher starting web search..."); + + try { + const suspects = state.suspect_names.split(",").map((s) => s.trim()); + const intelligenceResults: string[] = []; + + for (const suspect of suspects) { + console.log(` Searching public records for: ${suspect}`); + const query = `${suspect} criminal record art theft security technician Europe background check`; + const result = await callSonarProSearchTool(query); + intelligenceResults.push(`Background check for ${suspect}:\n${result}`); + } + + console.log(" Searching for similar art theft incidents..."); + const patternQuery = + "museum art theft insider job no forced entry Europe similar incidents criminal network"; + const patternResult = await callSonarProSearchTool(patternQuery); + intelligenceResults.push(`Similar Art Theft Patterns:\n${patternResult}`); + + const intelligenceReport = `Intelligence Research Complete: ${intelligenceResults.join("\n\n")} + Summary: Conducted OSINT research on all suspects and identified similar crime patterns`; + + console.log("āœ… Intelligence research complete"); + + return { + intelligence_report: intelligenceReport, + messages: [...state.messages, { role: "assistant", content: intelligenceReport }], + }; + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + console.error("āŒ Intelligence research failed:", errorMsg); + if (error instanceof Error && error.stack) { + console.error(error.stack); + } + return { + intelligence_report: `Error during intelligence research: ${errorMsg}`, + messages: [ + ...state.messages, + { role: "assistant", content: `Error during intelligence research: ${errorMsg}` }, + ], + }; + } + } +``` + +šŸ‘‰ **Third**, update `leadDetectiveNode` to pass `intelligence_report` to the system prompt. Find the `AGENT_CONFIGS.leadDetective.systemPrompt(...)` call and update it to: + +```typescript + content: AGENT_CONFIGS.leadDetective.systemPrompt( + state.appraisal_result || "No appraisal result available", + state.evidence_analysis || "No evidence analysis available", + state.intelligence_report || "No intelligence report available", + state.suspect_names, + ), +``` + +šŸ‘‰ **Fourth**, update `buildGraph()` to include the intelligence researcher node and rewire edges: + +```typescript + workflow + .addNode("appraiser", this.appraiserNode.bind(this)) + .addNode("evidence_analyst", this.evidenceAnalystNode.bind(this)) + .addNode("intelligence_researcher", this.intelligenceResearcherNode.bind(this)) + .addNode("lead_detective", this.leadDetectiveNode.bind(this)) + .addEdge(START, "appraiser") + .addEdge("appraiser", "evidence_analyst") + .addEdge("evidence_analyst", "intelligence_researcher") + .addEdge("intelligence_researcher", "lead_detective") + .addEdge("lead_detective", END); +``` + +šŸ‘‰ **Fifth**, update the `kickoff` method's logging to include the intelligence report: + +```typescript + console.log("\n--- Intelligence Report ---"); + console.log(result.intelligence_report ?? "(not set)"); +``` + +(Add after the evidence analysis log line) + +### Step 4: Verify TypeScript compiles + +Run from `project/JavaScript/solution/`: +```bash +cd project/JavaScript/solution +npm run build +``` + +If it compiles without errors, all type changes are consistent. + +--- + +## Run Your Enhanced Investigation + +```bash +cd project/JavaScript/starter-project +npm install +npm run dev +``` + +> ā±ļø **This may take 3-6 minutes** as your agents run through all four steps. + +--- + +## Understanding the Enhanced Graph + +```mermaid +flowchart TD + A[Appraiser Node] --> B[Evidence Analyst Node] + B --> C[Intelligence Researcher Node] + C --> D[Lead Detective Node] + D --> E[Final Conclusion] +``` + +In LangGraph, each node receives the full `AgentState` and returns a **partial state update**. The `intelligence_report` field flows from the Intelligence Researcher node to the Lead Detective node through the state. + +--- + +## Key Takeaways + +- **Sonar-Pro** provides real-time web search through the same `OrchestrationClient` API you already use +- **LangGraph state** carries `intelligence_report` forward to the Lead Detective automatically +- **Deterministic orchestration**: the node calls the tool directly (no LLM tool-calling loop) — predictable and efficient +- **Multi-source intelligence**: grounding (internal) + web search (external) → detective synthesis + +--- + +## Next Steps + +You now have a complete intelligence gathering system with: +1. āœ… Structured data predictions (RPT-1) +2. āœ… Internal document search (Grounding Service) +3. āœ… External web intelligence (Sonar-Pro) + +In the next exercise, you'll add the **Lead Detective Agent** who will synthesize all three sources to [solve the museum art theft mystery](07-solve-the-crime.md). + +--- + +## Troubleshooting + +**Issue**: `Error: Model sonar-pro not found` or 404 from OrchestrationClient + +- **Solution**: Verify that sonar-pro is deployed in your SAP AI Core resource group. Check **SAP AI Launchpad → Generative AI Hub → Models**. + +**Issue**: `callSonarProSearchTool is not exported` + +- **Solution**: Ensure you exported the function: `export async function callSonarProSearchTool(...)` + +**Issue**: TypeScript error on `state.intelligence_report` + +- **Solution**: Ensure you added the `intelligence_report` field to `AgentState` in `types.ts` before the `Annotation.Root` closing brace. + +**Issue**: Web search takes too long + +- **Solution**: Sonar-pro queries can take 10-30 seconds per search; this is normal for real-time web crawling. + +--- + +## Resources + +- [Perplexity API Documentation](https://docs.perplexity.ai/) +- [SAP Cloud SDK for AI Documentation](https://sap.github.io/ai-sdk-js/) +- [LangGraph Documentation](https://langchain-ai.github.io/langgraphjs/) + +[Next exercise](07-solve-the-crime.md) diff --git a/exercises/JavaScript/06-solve-the-crime.md b/exercises/JavaScript/07-solve-the-crime.md similarity index 99% rename from exercises/JavaScript/06-solve-the-crime.md rename to exercises/JavaScript/07-solve-the-crime.md index 8cd2442..276fa4c 100644 --- a/exercises/JavaScript/06-solve-the-crime.md +++ b/exercises/JavaScript/07-solve-the-crime.md @@ -280,7 +280,7 @@ You've successfully built a sophisticated multi-agent AI investigation system in 4. āœ… [Add custom tools](03-add-your-first-tool.md) (RPT-1 model integration) 5. āœ… [Build a multi-agent workflow](04-building-multi-agent-system.md) 6. āœ… [Integrate the Grounding Service](05-add-the-grounding-service.md) -7. āœ… [Solve the museum art theft mystery](06-solve-the-crime.md) (this exercise) +7. āœ… [Solve the museum art theft mystery](07-solve-the-crime.md) (this exercise) --- diff --git a/exercises/JavaScript/07-deploy-agent-to-cf-ts.md b/exercises/JavaScript/08-deploy-agent-to-cf-ts.md similarity index 99% rename from exercises/JavaScript/07-deploy-agent-to-cf-ts.md rename to exercises/JavaScript/08-deploy-agent-to-cf-ts.md index c5cf97d..0836555 100644 --- a/exercises/JavaScript/07-deploy-agent-to-cf-ts.md +++ b/exercises/JavaScript/08-deploy-agent-to-cf-ts.md @@ -686,8 +686,9 @@ flowchart TD 3. āœ… [Add custom tools](03-add-your-first-tool.md) 4. āœ… [Build a multi-agent system](04-building-multi-agent-system.md) 5. āœ… [Add the Grounding Service](05-add-the-grounding-service.md) -6. āœ… [Solve the crime](06-solve-the-crime.md) -7. āœ… [Deploy your agent to CF with A2A](07-deploy-agent-to-cf-ts.md) (this exercise) +6. āœ… [Discover Connected Crimes](06-discover-connected-crimes.md) +7. āœ… [Solve the crime](07-solve-the-crime.md) +8. āœ… [Deploy your agent to CF with A2A](08-deploy-agent-to-cf-ts.md) (this exercise) --- diff --git a/exercises/JavaScript/08-integrate-agent-into-joule.md b/exercises/JavaScript/09-integrate-agent-into-joule.md similarity index 98% rename from exercises/JavaScript/08-integrate-agent-into-joule.md rename to exercises/JavaScript/09-integrate-agent-into-joule.md index 1d56df0..eb32f76 100644 --- a/exercises/JavaScript/08-integrate-agent-into-joule.md +++ b/exercises/JavaScript/09-integrate-agent-into-joule.md @@ -55,7 +55,7 @@ SAP Joule The function extracts the result and shows it to the user ``` -> šŸ’” **Key insight:** Joule doesn't need to know that your agent uses LangGraph, TypeScript, or any specific framework. It only speaks A2A — the same protocol you set up in Exercise 07. The capability YAML is just the bridge between Joule's chat UI and your A2A endpoint. +> šŸ’” **Key insight:** Joule doesn't need to know that your agent uses LangGraph, TypeScript, or any specific framework. It only speaks A2A — the same protocol you set up in Exercise 08. The capability YAML is just the bridge between Joule's chat UI and your A2A endpoint. --- @@ -170,7 +170,7 @@ The `agent-request` action in your Joule function doesn't call your CF app direc > āš ļø **Replace `XX` with your participant number** (e.g., `INVESTIGATOR_AGENT_01`, `INVESTIGATOR_AGENT_02`). This ensures each participant has a unique destination. You'll use this exact name in all YAML files below. -> āš ļø **Replace ``** with the actual route of your CF app from Exercise 07. You can find it by running `cf apps` or by checking the URL you noted at the end of Exercise 07. +> āš ļø **Replace ``** with the actual route of your CF app from Exercise 08. You can find it by running `cf apps` or by checking the URL you noted at the end of Exercise 08. šŸ‘‰ Click **Save**. @@ -183,7 +183,7 @@ The `agent-request` action in your Joule function doesn't call your CF app direc > 4. Reads the Agent Card to discover the communication endpoint and protocol > 5. Sends an A2A `message/send` request with the user's message > -> This is the same discovery flow you tested manually with `curl` in Exercise 07 — now Joule does it automatically. +> This is the same discovery flow you tested manually with `curl` in Exercise 08 — now Joule does it automatically. --- @@ -377,7 +377,7 @@ The expression `apiResponse.body.artifacts[0].parts[0].text` walks this JSON pat | `.parts[0]` | The first part of that artifact | | `.text` | The actual text content of the investigation result | -> šŸ’” **This maps directly to what you built in Exercise 07.** In `src/server.ts`, your `InvestigatorExecutor` emits a `TaskArtifactUpdateEvent` with `artifactId: "investigation_result"` and `parts: [{ kind: "text", text: result }]` — the output of `new InvestigationWorkflow(...).kickoff(...)`. That's exactly what Joule reads here through `artifacts[0].parts[0].text`. +> šŸ’” **This maps directly to what you built in Exercise 08.** In `src/server.ts`, your `InvestigatorExecutor` emits a `TaskArtifactUpdateEvent` with `artifactId: "investigation_result"` and `parts: [{ kind: "text", text: result }]` — the output of `new InvestigationWorkflow(...).kickoff(...)`. That's exactly what Joule reads here through `artifacts[0].parts[0].text`. ### Step 5: Create the Digital Assistant Descriptor @@ -582,9 +582,10 @@ User sees investigation result in Joule chat 3. āœ… [Add custom tools](03-add-your-first-tool.md) 4. āœ… [Build a multi-agent system](04-building-multi-agent-system.md) 5. āœ… [Add the Grounding Service](05-add-the-grounding-service.md) -6. āœ… [Solve the crime](06-solve-the-crime.md) -7. āœ… [Deploy your agent to CF with A2A](07-deploy-agent-to-cf-ts.md) -8. āœ… [Integrate your agent into SAP Joule](08-integrate-agent-into-joule.md) (this exercise) +6. āœ… [Discover Connected Crimes](06-discover-connected-crimes.md) +7. āœ… [Solve the crime](07-solve-the-crime.md) +8. āœ… [Deploy your agent to CF with A2A](08-deploy-agent-to-cf-ts.md) +9. āœ… [Integrate your agent into SAP Joule](09-integrate-agent-into-joule.md) (this exercise) šŸŽ‰ **Congratulations!** You've completed the full CodeJam. You built a multi-agent AI system from scratch using LangGraph (TypeScript), deployed it to Cloud Foundry as an A2A server, and integrated it into SAP Joule — making it accessible to business users through natural language. diff --git a/exercises/Python-LangGraph/05-add-the-grounding-service.md b/exercises/Python-LangGraph/05-add-the-grounding-service.md index 54ffe51..4fdd420 100644 --- a/exercises/Python-LangGraph/05-add-the-grounding-service.md +++ b/exercises/Python-LangGraph/05-add-the-grounding-service.md @@ -188,7 +188,8 @@ flowchart LR 2. āœ… Add the RPT-1 tool 3. āœ… Build a multi-agent graph with supervisor 4. āœ… Add the Grounding Service (this exercise) -5. šŸ“Œ [Solve the crime](06-solve-the-crime.md) — add a Lead Detective and wire up the full investigation +5. šŸ“Œ [Add web search capabilities](06-discover-connected-crimes.md) — add an Intelligence Researcher node with Sonar-Pro +6. šŸ“Œ [Solve the crime](07-solve-the-crime.md) — finalize the Lead Detective and solve the mystery 6. šŸ“Œ Deploy to Cloud Foundry with A2A --- @@ -214,4 +215,4 @@ flowchart LR - [SAP AI Core Grounding Management](https://help.sap.com/docs/sap-ai-core/sap-ai-core-service-guide/document-grounding) - [SAP Cloud SDK for AI Python Reference](https://help.sap.com/doc/generative-ai-hub-sdk/CLOUD/en-US/_reference/gen_ai_hub.html) -[Next exercise](06-solve-the-crime.md) \ No newline at end of file +[Next exercise](06-discover-connected-crimes.md) \ No newline at end of file diff --git a/exercises/Python-LangGraph/06-discover-connected-crimes.md b/exercises/Python-LangGraph/06-discover-connected-crimes.md new file mode 100644 index 0000000..e3388d8 --- /dev/null +++ b/exercises/Python-LangGraph/06-discover-connected-crimes.md @@ -0,0 +1,403 @@ +# Discover Connected Crimes with Web Search (Python/LangGraph) + +## Overview + +In Exercise 05, you learned to search internal documents using the Grounding Service. Your Evidence Analyst node can now retrieve factual evidence from security logs, bank records, and termination letters without hallucination. + +But investigations don't happen in a vacuum. What if similar crimes happened elsewhere? What if your suspects have public criminal records? What if this heist is part of a larger criminal network? + +In this exercise, you'll add an **Intelligence Researcher node** that uses Perplexity's **sonar-pro model** to gather external intelligence from the web. + +--- + +## Understand Web Search with Sonar-Pro + +### Why Do We Need Web Search? + +Your current investigation system is powerful but limited to **internal data**: + +| **What You Can Do Now** | **What You Can't Do Yet** | +| ----------------------------------------------- | ---------------------------------------------------- | +| āœ… Search museum's internal evidence documents | āŒ Search public criminal databases | +| āœ… Retrieve bank records, security logs | āŒ Find similar crimes in other cities | +| āœ… Analyze suspects based on internal evidence | āŒ Check if suspects have public criminal records | +| āœ… Ground responses in factual documents | āŒ Discover criminal network connections | +| āœ… Avoid hallucination for internal data | āŒ Monitor online art markets for stolen items | + +### Document Grounding vs. Web Search + +| Aspect | Document Grounding (Exercise 05) | Web Search (This Exercise) | +| ---------------------- | ---------------------------------------- | ----------------------------------------- | +| **Data Source** | Internal documents (pre-uploaded) | Real-time web information | +| **Coverage** | Organization-specific evidence | Global public information | +| **Freshness** | Historical documents (static) | Current information (updated constantly) | +| **Use Cases** | Internal logs, private records, policies | News, criminal records, pattern analysis | +| **Search Method** | Vector similarity (semantic) | Web search + LLM synthesis | +| **Tool Function** | `call_grounding_service()` | `call_sonar_pro_search()` | +| **When to Use** | "What does our evidence say?" | "What do public records show?" | + +> šŸŽÆ **Key Insight**: These are **not alternatives** — they work **together**! + +### How Sonar-Pro Works in LangGraph + +In the Python-LangGraph track, tools are **plain functions** that nodes call directly — there is no `@tool` decorator. This is **deterministic orchestration**: the graph decides when to call the tool, not the LLM. This is different from CrewAI where the agent autonomously decides to use tools. + +```python +# Plain function — no @tool decorator needed +def call_sonar_pro_search(query: str) -> str: + from litellm import completion + response = completion( + model="sap/sonar-pro", # Perplexity via SAP AI Core + messages=[...] + ) + return response.choices[0].message.content + +# Node calls it directly — the graph controls the flow +def intelligence_researcher_node(state: AgentState) -> dict: + result = call_sonar_pro_search("Marcus Chen criminal record") + return {"intelligence_report": result} +``` + +> āš ļø **Before continuing**: Verify that `sonar-pro` is available in your SAP AI Core resource group. Go to **SAP AI Launchpad → Generative AI Hub → Models** and confirm you can see a Perplexity sonar-pro deployment. If it is not listed, ask your workshop instructor to enable it. + +--- + +## Add The Web Search Tool + +### Step 1: Add the `call_sonar_pro_search` Function + +šŸ‘‰ Open [`/project/Python-LangGraph/starter-project/investigator_graph.py`](/project/Python-LangGraph/starter-project/investigator_graph.py) + +šŸ‘‰ Add this function **after** `call_grounding_service` and **before** the `model = ChatLiteLLM(...)` line: + +```python +def call_sonar_pro_search(query: str) -> str: + """Search the web using Perplexity's sonar-pro model for real-time intelligence.""" + from litellm import completion + try: + response = completion( + model="sap/sonar-pro", + messages=[ + { + "role": "system", + "content": "You are a web search assistant specializing in criminal intelligence. Search for accurate, recent information and always provide source citations with URLs and dates." + }, + { + "role": "user", + "content": query + } + ], + temperature=0.2, + ) + return response.choices[0].message.content + except Exception as e: + return f"Error calling sonar-pro web search: {str(e)}" +``` + +> šŸ’” **Why no `@tool` decorator?** +> +> In the LangGraph track, tool functions are called **directly by nodes** — the graph structure controls when tools are called, not the LLM. This is called **deterministic tool orchestration**. Compare this to CrewAI, where agents autonomously decide to call tools. + +--- + +## Extend the Agent State + +### Step 2: Add `intelligence_report` to `AgentState` + +The LangGraph `AgentState` is a `TypedDict` that defines all the data flowing through the graph. You need to add a field for the intelligence report. + +šŸ‘‰ Find the `AgentState` class in `investigator_graph.py` and add `intelligence_report`: + +```python +class AgentState(TypedDict): + payload: dict + suspect_names: str + appraisal_result: Optional[str] + evidence_analysis: Optional[str] + intelligence_report: Optional[str] # ← NEW + final_conclusion: Optional[str] + messages: list +``` + +> šŸ’” **How LangGraph state works**: Each node receives the full `AgentState` and returns a **partial dict** with only the fields it updated. LangGraph merges these partial updates into the shared state. This is why `intelligence_researcher_node` can return just `{"intelligence_report": ...}` — LangGraph carries the other fields forward automatically. + +--- + +## Add The Intelligence Researcher Node + +### Step 3: Add the Node Function + +šŸ‘‰ Add this function **after** `evidence_analyst_node` and **before** `lead_detective_node`: + +```python +def intelligence_researcher_node(state: AgentState) -> dict: + print("\nšŸ” Intelligence Researcher starting web search...") + + try: + suspects = [s.strip() for s in state["suspect_names"].split(",")] + intelligence_results = [] + + for suspect in suspects: + print(f" Searching public records for: {suspect}") + query = f"{suspect} criminal record art theft security technician Europe background check" + result = call_sonar_pro_search(query) + intelligence_results.append(f"Background check for {suspect}:\n{result}") + + print(" Searching for similar art theft incidents...") + pattern_query = "museum art theft insider job no forced entry Europe similar incidents criminal network" + pattern_result = call_sonar_pro_search(pattern_query) + intelligence_results.append(f"Similar Art Theft Patterns:\n{pattern_result}") + + intelligence_report = ( + "Intelligence Research Complete:\n\n" + "\n\n".join(intelligence_results) + + f"\n\nSummary: Conducted OSINT research on all suspects and identified similar crime patterns" + ) + + print("āœ… Intelligence research complete") + + return { + "intelligence_report": intelligence_report, + "messages": state["messages"] + [{"role": "assistant", "content": intelligence_report}], + } + except Exception as e: + error_msg = f"Error during intelligence research: {e}" + print(f"āŒ {error_msg}") + return { + "intelligence_report": error_msg, + "messages": state["messages"] + [{"role": "assistant", "content": error_msg}], + } +``` + +--- + +## Add the Agent Prompt + +### Step 4: Add `WEB_RESEARCHER_AGENT` to `config/agents.py` + +šŸ‘‰ Open [`/project/Python-LangGraph/starter-project/config/agents.py`](/project/Python-LangGraph/starter-project/config/agents.py) + +šŸ‘‰ Add this constant **after** `EVIDENCE_ANALYST_AGENT`: + +```python +WEB_RESEARCHER_AGENT = { + "name": "intelligence_researcher_agent", + "prompt": ( + "You are an Open-Source Intelligence (OSINT) specialist. " + "Search the web for criminal records, similar art thefts, and suspect backgrounds. " + "Focus on public information: news articles, court records, criminal databases. " + "Always provide source citations with URLs and dates." + ), +} +``` + +--- + +## Update the Lead Detective Prompt + +### Step 5: Extend `_lead_detective_prompt` + +The Lead Detective now synthesizes three sources instead of two. Update `_lead_detective_prompt` in `config/agents.py`: + +```python +def _lead_detective_prompt(appraisal_result: str, evidence_analysis: str, intelligence_report: str, suspect_names: str) -> str: + return ( + "You are the Lead Detective coordinating an art theft investigation. " + "You have received the following information from your team:\n\n" + f"1. INSURANCE APPRAISAL:\n{appraisal_result}\n\n" + f"2. EVIDENCE ANALYSIS (Internal Documents):\n{evidence_analysis}\n\n" + f"3. INTELLIGENCE REPORT (Web Search):\n{intelligence_report}\n\n" + f"4. SUSPECTS: {suspect_names}\n\n" + "Based on all the evidence and analysis, you MUST:\n" + "- Name the most likely thief and explain the evidence supporting that conclusion\n" + "- Consider both internal evidence and external criminal patterns\n" + "- Note any alibis or evidence that clears the other suspects\n" + "- State the total insured value of the stolen goods\n" + "- Assess whether this is an isolated incident or part of a larger criminal network\n" + "- Provide a comprehensive summary of the case." + ) +``` + +--- + +## Rewire the Graph + +### Step 6: Add the Node and Update Edges + +šŸ‘‰ Find the `build_graph()` function in `investigator_graph.py` and update it: + +```python +def build_graph(): + workflow = StateGraph(AgentState) + + workflow.add_node("appraiser", appraiser_node) + workflow.add_node("evidence_analyst", evidence_analyst_node) + workflow.add_node("intelligence_researcher", intelligence_researcher_node) # ← NEW + workflow.add_node("lead_detective", lead_detective_node) + workflow.add_edge(START, "appraiser") + workflow.add_edge("appraiser", "evidence_analyst") + workflow.add_edge("evidence_analyst", "intelligence_researcher") # ← CHANGED + workflow.add_edge("intelligence_researcher", "lead_detective") # ← NEW + workflow.add_edge("lead_detective", END) + + return workflow.compile() +``` + +The new graph structure: + +```mermaid +flowchart TD + START --> A[appraiser_node] + A --> B[evidence_analyst_node] + B --> C[intelligence_researcher_node] + C --> D[lead_detective_node] + D --> END +``` + +--- + +## Update Lead Detective Node Call + +### Step 7: Pass `intelligence_report` to the Prompt + +In `investigator_graph.py`, find `lead_detective_node` and update the `LEAD_DETECTIVE["prompt"]` call to include `intelligence_report`: + +```python +response = model.invoke([ + SystemMessage(content=LEAD_DETECTIVE["prompt"]( + state["appraisal_result"] or "No appraisal result available", + state["evidence_analysis"] or "No evidence analysis available", + state.get("intelligence_report") or "No intelligence report available", + state["suspect_names"], + )), + HumanMessage(content="Analyze all the evidence and identify the culprit. Provide a detailed conclusion."), +]) +``` + +--- + +## Update `main.py` and `server.py` + +### Step 8: Initialize the New State Field + +šŸ‘‰ Open [`/project/Python-LangGraph/starter-project/main.py`](/project/Python-LangGraph/starter-project/main.py) + +šŸ‘‰ Add `"intelligence_report": None` to the initial state dict: + +```python +result = investigator_graph.invoke({ + "payload": payload, + "suspect_names": "Sophie Dubois, Marcus Chen, Viktor Petrov", + "appraisal_result": None, + "evidence_analysis": None, + "intelligence_report": None, # ← NEW + "final_conclusion": None, + "messages": [], +}) +``` + +šŸ‘‰ Also add a print section for the intelligence report after the Evidence Analysis section: + +```python +print("\n" + "="*50) +print("Intelligence Report:") +print("="*50) +print(result["intelligence_report"] or "(not set)") +``` + +> šŸ’” **`server.py`** also constructs the initial state for `investigator_graph.ainvoke(...)`. Add `"intelligence_report": None` there too — find the dict and add the field. + +--- + +## Run Your Enhanced Investigation + +šŸ‘‰ Run the investigation from the starter-project folder: + +_macOS / Linux / BAS_ + +```bash +python3 ./project/Python-LangGraph/starter-project/main.py +``` + +_Windows (PowerShell)_ + +```powershell +python .\project\Python-LangGraph\starter-project\main.py +``` + +> ā±ļø **Expected output** — you should see all four nodes fire in order: +> ``` +> šŸ” Appraiser Agent starting... +> āœ… Appraisal complete +> +> šŸ” Evidence Analyst starting... +> āœ… Evidence analysis complete +> +> šŸ” Intelligence Researcher starting web search... +> Searching public records for: Sophie Dubois +> Searching public records for: Marcus Chen +> Searching public records for: Viktor Petrov +> Searching for similar art theft incidents... +> āœ… Intelligence research complete +> +> šŸ” Lead Detective analyzing all findings... +> āœ… Investigation complete +> ``` + +--- + +## Key Takeaways + +- **LangGraph tool functions are plain functions** — no `@tool` decorator needed. Nodes call them directly. +- **State carries data between nodes** — `intelligence_report` is set by the Intelligence Researcher node and automatically available to the Lead Detective node via `AgentState`. +- **Deterministic orchestration** — the graph structure controls the flow, not the LLM. Every run follows the same `appraiser → evidence_analyst → intelligence_researcher → lead_detective` path. +- **Sonar-Pro** is called through the same `litellm.completion()` API as other models, just with `model="sap/sonar-pro"`. + +--- + +## Next Steps + +You now have a complete intelligence gathering system with: +1. āœ… Structured data predictions (RPT-1) +2. āœ… Internal document search (Grounding Service) +3. āœ… External web intelligence (Sonar-Pro) + +In the next exercise, you'll finalize the **Lead Detective prompt** to synthesize all three sources and [solve the museum art theft mystery](07-solve-the-crime.md). + +--- + +## Troubleshooting + +**Issue**: `ModuleNotFoundError: No module named 'litellm'` + +- **Solution**: LiteLLM should already be installed from Exercise 02. If not: + + _macOS / Linux / BAS_ + ```bash + pip install litellm==1.82.6 + ``` + +**Issue**: `Error: Model sap/sonar-pro not found` or `404` + +- **Solution**: Verify that sonar-pro is deployed in your SAP AI Core resource group. Check **SAP AI Launchpad → Generative AI Hub → Models**. + +**Issue**: `KeyError: 'intelligence_report'` + +- **Solution**: Ensure you added `intelligence_report: Optional[str]` to the `AgentState` TypedDict AND initialized it to `None` in `main.py`'s initial state dict. + +**Issue**: `TypeError: _lead_detective_prompt() takes 3 positional arguments but 4 were given` + +- **Solution**: You updated the `lead_detective_node` call but forgot to update `_lead_detective_prompt`'s signature in `config/agents.py` (or vice versa). Make sure both accept `intelligence_report` as the third parameter. + +**Issue**: Intelligence Researcher fires but Lead Detective ignores the report + +- **Solution**: Check that `_lead_detective_prompt` actually interpolates `{intelligence_report}` in its return string, and that `lead_detective_node` passes `state.get("intelligence_report")` as the third argument. + +--- + +## Resources + +- [LangGraph Documentation](https://langchain-ai.github.io/langgraph/) +- [Perplexity API Documentation](https://docs.perplexity.ai/) +- [LiteLLM Documentation](https://docs.litellm.ai/) + +[Next exercise](07-solve-the-crime.md) diff --git a/exercises/Python-LangGraph/06-solve-the-crime.md b/exercises/Python-LangGraph/07-solve-the-crime.md similarity index 98% rename from exercises/Python-LangGraph/06-solve-the-crime.md rename to exercises/Python-LangGraph/07-solve-the-crime.md index b3b4900..ab435c4 100644 --- a/exercises/Python-LangGraph/06-solve-the-crime.md +++ b/exercises/Python-LangGraph/07-solve-the-crime.md @@ -126,7 +126,7 @@ The Lead Detective node receives `appraisal_result` and `evidence_analysis` from 3. āœ… Build a multi-agent graph with Lead Detective and specialist agents 4. āœ… Add the Grounding Service 5. āœ… Solve the crime (this exercise) -6. šŸ“Œ [Deploy to Cloud Foundry with A2A](07-deploy-agent-to-cf.md) +6. šŸ“Œ [Deploy to Cloud Foundry with A2A](08-deploy-agent-to-cf.md) --- @@ -144,4 +144,4 @@ The Lead Detective node receives `appraisal_result` and `evidence_analysis` from - **Solution**: The supervisor ran out of turns. This can happen with very long evidence reports. Try reducing `maxChunkCount` from 5 to 3 in `call_grounding_service`. -[Next exercise](07-deploy-agent-to-cf.md) \ No newline at end of file +[Next exercise](08-deploy-agent-to-cf.md) \ No newline at end of file diff --git a/exercises/Python-LangGraph/07-deploy-agent-to-cf.md b/exercises/Python-LangGraph/08-deploy-agent-to-cf.md similarity index 100% rename from exercises/Python-LangGraph/07-deploy-agent-to-cf.md rename to exercises/Python-LangGraph/08-deploy-agent-to-cf.md diff --git a/exercises/Python-LangGraph/08-integrate-agent-into-joule.md b/exercises/Python-LangGraph/09-integrate-agent-into-joule.md similarity index 98% rename from exercises/Python-LangGraph/08-integrate-agent-into-joule.md rename to exercises/Python-LangGraph/09-integrate-agent-into-joule.md index 0472d4e..b44ce6b 100644 --- a/exercises/Python-LangGraph/08-integrate-agent-into-joule.md +++ b/exercises/Python-LangGraph/09-integrate-agent-into-joule.md @@ -55,7 +55,7 @@ SAP Joule The function extracts the result and shows it to the user ``` -> šŸ’” **Key insight:** Joule doesn't need to know that your agent uses LangGraph, Python, or any specific framework. It only speaks A2A — the same protocol you set up in Exercise 07. The capability YAML is just the bridge between Joule's chat UI and your A2A endpoint. +> šŸ’” **Key insight:** Joule doesn't need to know that your agent uses LangGraph, Python, or any specific framework. It only speaks A2A — the same protocol you set up in Exercise 08. The capability YAML is just the bridge between Joule's chat UI and your A2A endpoint. --- @@ -170,7 +170,7 @@ The `agent-request` action in your Joule function doesn't call your CF app direc > āš ļø **Replace `XX` with your participant number** (e.g., `INVESTIGATOR_AGENT_01`, `INVESTIGATOR_AGENT_02`). This ensures each participant has a unique destination. You'll use this exact name in all YAML files below. -> āš ļø **Replace ``** with the actual route of your CF app from Exercise 07. You can find it by running `cf apps` or by checking the URL you noted at the end of Exercise 07. +> āš ļø **Replace ``** with the actual route of your CF app from Exercise 08. You can find it by running `cf apps` or by checking the URL you noted at the end of Exercise 08. šŸ‘‰ Click **Save**. @@ -183,7 +183,7 @@ The `agent-request` action in your Joule function doesn't call your CF app direc > 4. Reads the Agent Card to discover the communication endpoint and protocol > 5. Sends an A2A `message/send` request with the user's message > -> This is the same discovery flow you tested manually with `curl` in Exercise 07 — now Joule does it automatically. +> This is the same discovery flow you tested manually with `curl` in Exercise 08 — now Joule does it automatically. --- @@ -377,7 +377,7 @@ The expression `apiResponse.body.artifacts[0].parts[0].text` walks this JSON pat | `.parts[0]` | The first part of that artifact | | `.text` | The actual text content of the investigation result | -> šŸ’” **This maps directly to what you built in Exercise 07.** In `server.py`, your `InvestigatorExecutor` emits a `TaskArtifactUpdateEvent` with `artifactId="investigation_result"` and a `TextPart` — the output from `investigator_graph.invoke(...)`. That's exactly what Joule reads here through `artifacts[0].parts[0].text`. +> šŸ’” **This maps directly to what you built in Exercise 08.** In `server.py`, your `InvestigatorExecutor` emits a `TaskArtifactUpdateEvent` with `artifactId="investigation_result"` and a `TextPart` — the output from `investigator_graph.invoke(...)`. That's exactly what Joule reads here through `artifacts[0].parts[0].text`. ### Step 5: Create the Digital Assistant Descriptor @@ -582,9 +582,10 @@ User sees investigation result in Joule chat 3. āœ… [Add custom tools](03-add-your-first-tool.md) 4. āœ… [Build a multi-agent system](04-building-multi-agent-system.md) 5. āœ… [Add the Grounding Service](05-add-the-grounding-service.md) -6. āœ… [Solve the crime](06-solve-the-crime.md) -7. āœ… [Deploy your agent to CF with A2A](07-deploy-agent-to-cf.md) -8. āœ… [Integrate your agent into SAP Joule](08-integrate-agent-into-joule.md) (this exercise) +6. āœ… [Discover Connected Crimes](06-discover-connected-crimes.md) +7. āœ… [Solve the crime](07-solve-the-crime.md) +8. āœ… [Deploy your agent to CF with A2A](08-deploy-agent-to-cf.md) +9. āœ… [Integrate your agent into SAP Joule](09-integrate-agent-into-joule.md) (this exercise) šŸŽ‰ **Congratulations!** You've completed the full CodeJam. You built a multi-agent AI system from scratch using LangGraph, deployed it to Cloud Foundry as an A2A server, and integrated it into SAP Joule — making it accessible to business users through natural language. diff --git a/exercises/Python/04-building-multi-agent-system.md b/exercises/Python/04-building-multi-agent-system.md index d5405b7..d0b6d32 100644 --- a/exercises/Python/04-building-multi-agent-system.md +++ b/exercises/Python/04-building-multi-agent-system.md @@ -392,7 +392,7 @@ When you run `InvestigatorCrew().crew().kickoff(inputs={...})`: - **Collaboration** - Agents can build upon each other's work - Sequential processing allows later agents to use earlier results - - `context` parameter (coming in Exercise 06) enables explicit data sharing + - `context` parameter (coming in Exercise 07) enables explicit data sharing - **Maintainability** - Clear separation of concerns - Business logic (goals, roles) lives in YAML @@ -439,7 +439,8 @@ In the following exercises, you will: 3. āœ… [Add custom tools](03-add-your-first-tool.md) (RPT-1 model integration) 4. āœ… [Build a multi-agent system](04-building-multi-agent-system.md) (this exercise) 5. šŸ“Œ [Add the Grounding Service](05-add-the-grounding-service.md) - Give your Evidence Analyst access to real documents -6. šŸ“Œ [Solve the crime](06-solve-the-crime.md) - Add a Lead Detective Agent to combine findings and crack the case +6. šŸ“Œ [Discover Connected Crimes](06-discover-connected-crimes.md) - Add web search with Perplexity sonar-pro +7. šŸ“Œ [Solve the crime](07-solve-the-crime.md) - Add a Lead Detective Agent to combine findings and crack the case --- diff --git a/exercises/Python/05-add-the-grounding-service.md b/exercises/Python/05-add-the-grounding-service.md index b3206f7..d0bc025 100644 --- a/exercises/Python/05-add-the-grounding-service.md +++ b/exercises/Python/05-add-the-grounding-service.md @@ -468,7 +468,8 @@ In the following exercises, you will: 2. āœ… Add custom tools to your agents so they can access external data 3. āœ… Create a complete crew with multiple agents working together 4. āœ… Integrate the Grounding Service for better reasoning and fact-checking (this exercise) -5. šŸ“Œ [Solve the museum art theft mystery](06-solve-the-crime.md) using your fully-featured agent team +5. šŸ“Œ [Add web search capabilities](06-discover-connected-crimes.md) to gather external intelligence +6. šŸ“Œ [Solve the museum art theft mystery](07-solve-the-crime.md) using your fully-featured agent team --- @@ -511,4 +512,4 @@ In the following exercises, you will: - [SAP AI Core Grounding Management](https://help.sap.com/docs/sap-ai-core/sap-ai-core-service-guide/document-grounding) - [SAP Cloud SDK for AI (Python)](https://help.sap.com/doc/generative-ai-hub-sdk/CLOUD/en-US/_reference/README_sphynx.html) -[Next exercise](06-solve-the-crime.md) +[Next exercise](06-discover-connected-crimes.md) diff --git a/exercises/Python/06-discover-connected-crimes.md b/exercises/Python/06-discover-connected-crimes.md new file mode 100644 index 0000000..4337978 --- /dev/null +++ b/exercises/Python/06-discover-connected-crimes.md @@ -0,0 +1,446 @@ +# Discover Connected Crimes with Web Search + +## Overview + +In Exercise 05, you learned to search internal documents using the Grounding Service. Your Evidence Analyst can now retrieve factual evidence from security logs, bank records, and termination letters without hallucination. + +But investigations don't happen in a vacuum. What if similar crimes happened elsewhere? What if your suspects have public criminal records? What if this heist is part of a larger criminal network? + +In this exercise, you'll add **web search capabilities** using Perplexity's **sonar-pro model** to gather external intelligence and discover patterns across the internet. + +--- + +## Understand Web Search with Sonar-Pro + +### Why Do We Need Web Search? + +Your current investigation system is powerful but limited to **internal data**: + +| **What You Can Do Now** | **What You Can't Do Yet** | +| ----------------------------------------------- | ---------------------------------------------------- | +| āœ… Search museum's internal evidence documents | āŒ Search public criminal databases | +| āœ… Retrieve bank records, security logs | āŒ Find similar crimes in other cities | +| āœ… Analyze suspects based on internal evidence | āŒ Check if suspects have public criminal records | +| āœ… Ground responses in factual documents | āŒ Discover criminal network connections | +| āœ… Avoid hallucination for internal data | āŒ Monitor online art markets for stolen items | + +**The Problem**: Real investigations combine **internal evidence** (what happened here) with **external intelligence** (what's happening elsewhere). Your agents need both! + +### Document Grounding vs. Web Search + +You now have access to **two complementary search capabilities**: + +| Aspect | Document Grounding (Exercise 05) | Web Search (This Exercise) | +| ---------------------- | ---------------------------------------- | ----------------------------------------- | +| **Data Source** | Internal documents (pre-uploaded) | Real-time web information | +| **Coverage** | Organization-specific evidence | Global public information | +| **Freshness** | Historical documents (static) | Current information (updated constantly) | +| **Use Cases** | Internal logs, private records, policies | News, criminal records, pattern analysis | +| **Search Method** | Vector similarity (semantic) | Web search + LLM synthesis | +| **Source Control** | You control what documents exist | Public internet (no control) | +| **Tool** | `call_grounding_service` | `call_sonar_pro_search` | +| **Privacy** | Private, secure | Public information only | +| **When to Use** | "What does our evidence say?" | "What do public records show?" | + +> šŸŽÆ **Key Insight**: These are **not alternatives** — they work **together**! The best investigations use both internal evidence AND external intelligence. + +### What is Sonar-Pro? + +**Sonar-Pro** is Perplexity's AI model with built-in web search capabilities. Unlike regular LLMs: + +| Regular LLM (e.g., GPT-4, Claude) | Sonar-Pro (Perplexity) | +| --------------------------------------- | -------------------------------------------- | +| Knowledge cutoff (training data only) | Real-time web search | +| Can't access recent events | Finds current information | +| No source verification | Returns citations with URLs | +| "I think..." or "Based on my training" | "According to [source], dated [date]" | +| Static knowledge | Dynamic, up-to-date intelligence | + +**How Sonar-Pro Works**: +``` +Your Query → Sonar-Pro searches web → Retrieves relevant pages → +Synthesizes answer → Returns result with source citations +``` + +### How Sonar-Pro Integrates with SAP AI Core + +Sonar-Pro is called as a **model** through SAP's orchestration service, just like GPT-4 or Claude: + +```python +from litellm import completion + +response = completion( + model="sap/sonar-pro", # Perplexity via SAP orchestration + messages=[ + {"role": "system", "content": "Search for factual information"}, + {"role": "user", "content": "Your search query"} + ] +) +``` + +**Key Differences**: +- `sap/gpt-4o` → Regular reasoning LLM +- `sap/sonar-pro` → Web search-enabled LLM +- Both use the same `completion()` API! + +> āš ļø **Before continuing**: Verify that `sonar-pro` is available in your SAP AI Core resource group. Go to **SAP AI Launchpad → Generative AI Hub → Models** and confirm you can see a Perplexity sonar-pro deployment. If it is not listed, ask your workshop instructor to enable it. + +--- + +## Add The Web Search Tool + +### Step 1: Create the Sonar-Pro Search Tool + +You'll create a tool that enables your agents to search the web for criminal patterns, suspect backgrounds, and related incidents. + +šŸ‘‰ Open [`/project/Python/starter-project/investigator_crew.py`](/project/Python/starter-project/investigator_crew.py) + +šŸ‘‰ Add this tool **after** the `call_grounding_service` tool (around line 50): + +```python +@tool("call_sonar_pro_search") +def call_sonar_pro_search(search_query: str) -> str: + """Search the web using Perplexity's sonar-pro model for real-time information + about crimes, suspects, and criminal patterns. Use this to find similar incidents, + criminal networks, public records, or patterns that are not in internal documents. + + Args: + search_query: The search query about crimes, suspects, or criminal patterns + + Returns: + Search results with source citations from the web + """ + from litellm import completion + + try: + response = completion( + model="sap/sonar-pro", # Perplexity model with web search + messages=[ + { + "role": "system", + "content": "You are a web search assistant specializing in criminal intelligence. Search for accurate, recent information and always provide source citations with URLs and dates." + }, + { + "role": "user", + "content": search_query + } + ], + temperature=0.2, # Lower temperature for factual search + ) + + result = response.choices[0].message.content + return result + + except Exception as e: + return f"Error calling sonar-pro web search: {str(e)}" +``` + +> šŸ’” **Understanding the Web Search Tool:** +> +> **1. Model Selection** +> - `model="sap/sonar-pro"` - Uses Perplexity's web search-enabled model +> - Automatically searches the web and returns cited results +> - No additional configuration needed for basic web search +> +> **2. Search Configuration** +> - `temperature=0.2` - Lower temperature for factual, consistent results +> - System prompt guides the search focus (criminal intelligence) +> - User query contains the specific search (e.g., "Marcus Chen criminal history") +> +> **3. Response Format** +> - Sonar-pro returns synthesized answers with web sources +> - Includes URLs, publication dates, and source reliability +> - Agent receives structured information to inform investigation +> +> **4. Error Handling** +> - Returns error as string (agent can handle gracefully) +> - No exceptions raised (agent workflow continues) + +--- + +## Add The Intelligence Researcher Agent + +### Step 1: Add Agent Configuration + +šŸ‘‰ Open [`/project/Python/starter-project/config/agents.yaml`](/project/Python/starter-project/config/agents.yaml) + +šŸ‘‰ Add this configuration **after** the `evidence_analyst_agent` section and **before** the `lead_detective_agent` section: + +```yaml +intelligence_researcher_agent: + role: > + Open-Source Intelligence (OSINT) Researcher + goal: > + Search the web for similar art thefts, criminal patterns, and suspect backgrounds + to determine if this heist is part of a larger criminal network. Use the + call_sonar_pro_search tool to find recent incidents, news reports, and public + criminal records for all three suspects: {suspect_names}. + backstory: > + You are an OSINT specialist who excels at finding patterns across multiple + crime scenes. You search public databases, news archives, and criminal records + to connect seemingly isolated incidents. Your expertise has uncovered several + international art theft rings, and you know how to distinguish professional + criminals from amateurs. + llm: sap/gpt-4o +``` + +> šŸ’” **Why This Agent Design?** +> +> - **Role**: OSINT Researcher - Establishes expertise in public information gathering +> - **Goal**: Specific search objectives (patterns, backgrounds, networks) with explicit tool mention +> - **Backstory**: Provides context and authority for web-based investigations +> - **LLM**: Uses `sap/gpt-4o` for agent reasoning (the tool calls sonar-pro for searches) + +### Step 2: Add Task Configuration + +šŸ‘‰ Open [`/project/Python/starter-project/config/tasks.yaml`](/project/Python/starter-project/config/tasks.yaml) + +šŸ‘‰ Add this configuration **after** the `analyze_evidence_task` and **before** the `solve_crime` task: + +```yaml +research_criminal_network: + description: > + Search the web for intelligence about the three suspects ({suspect_names}) and + related crimes. Use the call_sonar_pro_search tool to find: + 1. Public criminal records or prior convictions for each suspect + 2. Similar art theft incidents with the same modus operandi (insider job, no forced entry) + 3. Connections to known art theft rings or criminal networks + 4. News reports or public information about any of the suspects + 5. Recent museum heists in Europe with similar patterns + + Cross-reference your web findings with the internal evidence analyzed by the + evidence analyst. Focus on discovering whether this is an isolated incident or + part of a larger criminal operation. + expected_output: > + A comprehensive intelligence report containing: + - Background checks for all three suspects with web sources + - List of similar art thefts found online (dates, locations, MO) + - Evidence of criminal network connections (if any) + - Assessment: isolated incident vs. organized crime ring + - All findings MUST include web sources with URLs and dates + agent: intelligence_researcher_agent +``` + +### Step 3: Add Agent and Task Methods to the Crew + +šŸ‘‰ Open [`/project/Python/starter-project/investigator_crew.py`](/project/Python/starter-project/investigator_crew.py) + +šŸ‘‰ Find the `InvestigatorCrew` class + +šŸ‘‰ Add these methods **after** the `analyze_evidence_task()` method and **BEFORE** the `lead_detective_agent()` method: + +```python + @agent + def intelligence_researcher_agent(self) -> Agent: + return Agent( + config=self.agents_config['intelligence_researcher_agent'], + verbose=True, + tools=[call_sonar_pro_search] # Web search tool + ) + + @task + def research_criminal_network(self) -> Task: + return Task( + config=self.tasks_config['research_criminal_network'], + context=[self.analyze_evidence_task()] # Uses internal evidence to inform web searches + ) +``` + +šŸ‘‰ Also update the `solve_crime` task to use all three sources: + +```python + @task + def solve_crime(self) -> Task: + return Task( + config=self.tasks_config['solve_crime'], + context=[self.appraise_loss_task(), self.analyze_evidence_task(), self.research_criminal_network()] + ) +``` + +> šŸ’” **Method Positioning Matters!** +> +> Your class should now have this order: +> 1. `appraiser_agent()` method +> 2. `appraise_loss_task()` method +> 3. `evidence_analyst_agent()` method +> 4. `analyze_evidence_task()` method +> 5. **šŸ‘ˆ `intelligence_researcher_agent()` method (NEW!)** +> 6. **šŸ‘ˆ `research_criminal_network()` method (NEW!)** +> 7. `lead_detective_agent()` method +> 8. `solve_crime()` method +> 9. `crew()` method (stays at the end) + +> šŸ’” **Understanding Task Context**: +> - `context=[self.analyze_evidence_task()]` means the Intelligence Researcher receives the Evidence Analyst's findings +> - This allows the researcher to use internal evidence to formulate better web searches + +--- + +## Run Your Enhanced Investigation + +šŸ‘‰ Run your crew to test the web search capability! + +**From repository root:** + +**macOS / Linux / BAS** + +```bash +python3 ./project/Python/starter-project/main.py +``` + +**Windows (PowerShell)** + +```powershell +python .\project\Python\starter-project\main.py +``` + +**From starter-project folder:** + +**macOS / Linux / BAS** + +```bash +python3 main.py +``` + +**Windows (PowerShell)** + +```powershell +python main.py +``` + +> ā±ļø **This may take 3-6 minutes** as your agents: +> +> 1. Predict stolen item values (Appraiser with RPT-1) +> 2. Search internal evidence documents (Evidence Analyst with Grounding) +> 3. **Search the web for criminal patterns (Intelligence Researcher with Sonar-Pro) ← NEW!** + +šŸ‘‰ Review the intelligence report from the web search: +- Did it find similar crimes? +- Do any suspects have public criminal records? +- Is there evidence of a criminal network? + +--- + +## Understanding Web Search Integration + +### What Just Happened? + +You created a complete multi-source intelligence gathering system that: + +1. **Searches Internal Documents** (Grounding Service) - Evidence from within the museum +2. **Searches External Web** (Sonar-Pro) - Public information from across the internet +3. **Combines Intelligence** - Both sources inform the investigation + +### The Enhanced Investigation Flow + +```mermaid +flowchart TD + A[Appraiser Agent] --> B[Predict Values
RPT-1 Tool] + B --> C[Evidence Analyst] + C --> D[Search Internal Docs
Grounding Service] + D --> E[Intelligence Researcher] + E --> F[Search Web
Sonar-Pro] + F --> G[Multi-Source Intelligence
Internal + External] + G --> H[Lead Detective
Solve Crime - Exercise 07] +``` + +### When to Use Each Search Type + +**Use Document Grounding** (`call_grounding_service`) when: +- āœ… Searching organization-specific documents +- āœ… Accessing private/confidential information +- āœ… Finding internal evidence (logs, records, policies) +- āœ… You control the document collection +- āœ… Need semantic search across your own data + +**Use Web Search** (`call_sonar_pro_search`) when: +- āœ… Searching public information +- āœ… Finding current events or recent news +- āœ… Checking criminal databases or public records +- āœ… Discovering patterns across multiple organizations +- āœ… Need real-time, up-to-date information + +--- + +## Key Takeaways + +- **Web Search** extends your investigation beyond internal documents to public intelligence +- **Sonar-Pro** provides real-time web search with source citations +- **Multi-Source Intelligence** combines internal evidence + external intelligence +- **Complementary Tools**: Document grounding and web search work together, not in competition +- **Complete Investigation**: Real detectives use both internal records AND external research +- **Pattern Discovery**: Web search reveals connections that internal documents can't show + +--- + +## Next Steps + +You now have a complete intelligence gathering system with: +1. āœ… Structured data predictions (RPT-1) +2. āœ… Internal document search (Grounding Service) +3. āœ… External web intelligence (Sonar-Pro) + +In the next exercise, you'll add the **Lead Detective Agent** who will synthesize findings from all three sources to [solve the museum art theft mystery](07-solve-the-crime.md). + +--- + +## Troubleshooting + +**Issue**: `ModuleNotFoundError: No module named 'litellm'` + +- **Solution**: LiteLLM should already be installed from Exercise 02. If not, run: + + **macOS / Linux / BAS** + ```bash + pip install litellm==1.82.6 + ``` + + **Windows (PowerShell)** + ```powershell + pip install litellm==1.82.6 + ``` + +**Issue**: `Error: Model sap/sonar-pro not found` + +- **Solution**: Verify that: + - Sonar-pro is available in your SAP AI Core Generative AI Hub model catalog + - Your resource group has access to Perplexity models + - Check SAP AI Launchpad → Generative AI Hub → Models for available models + +**Issue**: Web search returns no results or very generic information + +- **Solution**: Make your search queries more specific: + - āŒ Bad: "art theft" + - āœ… Good: "Marcus Chen security technician unauthorized access criminal record Europe" + - Include suspect names, locations, and specific details + +**Issue**: `AttributeError: 'NoneType' object has no attribute 'content'` + +- **Solution**: The sonar-pro API response structure might differ. Update error handling: + ```python + result = response.choices[0].message.content if response.choices else "No results" + ``` + +**Issue**: Agent doesn't use the web search tool + +- **Solution**: Ensure: + - The tool is assigned: `tools=[call_sonar_pro_search]` + - The task description explicitly mentions using `call_sonar_pro_search` + - The agent's goal references web search or OSINT + +**Issue**: Web search takes too long or times out + +- **Solution**: + - Sonar-pro queries can take 10-30 seconds per search + - This is normal for real-time web crawling + - If timeout occurs, increase LiteLLM timeout or retry + +--- + +## Resources + +- [Perplexity API Documentation](https://docs.perplexity.ai/) +- [SAP AI Core Orchestration Workflow](https://help.sap.com/docs/sap-ai-core/generative-ai/orchestration-workflow-v2) +- [LiteLLM Documentation](https://docs.litellm.ai/) + +[Next exercise](07-solve-the-crime.md) diff --git a/exercises/Python/06-solve-the-crime.md b/exercises/Python/07-solve-the-crime.md similarity index 99% rename from exercises/Python/06-solve-the-crime.md rename to exercises/Python/07-solve-the-crime.md index 7b3d73f..4c4bbba 100644 --- a/exercises/Python/06-solve-the-crime.md +++ b/exercises/Python/07-solve-the-crime.md @@ -98,7 +98,7 @@ Your crew configuration should already be set from Exercise 04, but let's verify ### Step 5: Verify main.py (No Changes Needed) -Your `main.py` from Exercise 04 should already be correct. It doesn't need any changes for Exercise 06! +Your `main.py` from Exercise 04 should already be correct. It doesn't need any changes for Exercise 07! > šŸ’” **What's happening:** The same `main.py` that ran 2 agents in Exercise 04 will now automatically run all 3 agents (including your new Lead Detective). CrewAI collects all `@agent` and `@task` decorated methods automatically. diff --git a/exercises/Python/07-deploy-agent-to-cf.md b/exercises/Python/08-deploy-agent-to-cf.md similarity index 98% rename from exercises/Python/07-deploy-agent-to-cf.md rename to exercises/Python/08-deploy-agent-to-cf.md index 11e8d20..6d9565f 100644 --- a/exercises/Python/07-deploy-agent-to-cf.md +++ b/exercises/Python/08-deploy-agent-to-cf.md @@ -542,9 +542,10 @@ flowchart TD 3. āœ… [Add custom tools](03-add-your-first-tool.md) 4. āœ… [Build a multi-agent system](04-building-multi-agent-system.md) 5. āœ… [Add the Grounding Service](05-add-the-grounding-service.md) -6. āœ… [Solve the crime](06-solve-the-crime.md) -7. āœ… [Deploy your agent to CF with A2A](07-deploy-agent-to-cf.md) (this exercise) -8. šŸ“Œ [Integrate your agent into SAP Joule](08-integrate-agent-into-joule.md) +6. āœ… [Discover Connected Crimes](06-discover-connected-crimes.md) +7. āœ… [Solve the crime](07-solve-the-crime.md) +8. āœ… [Deploy your agent to CF with A2A](08-deploy-agent-to-cf.md) (this exercise) +9. šŸ“Œ [Integrate your agent into SAP Joule](09-integrate-agent-into-joule.md) --- diff --git a/exercises/Python/08-integrate-agent-into-joule.md b/exercises/Python/09-integrate-agent-into-joule.md similarity index 98% rename from exercises/Python/08-integrate-agent-into-joule.md rename to exercises/Python/09-integrate-agent-into-joule.md index a32d30e..7ae68bf 100644 --- a/exercises/Python/08-integrate-agent-into-joule.md +++ b/exercises/Python/09-integrate-agent-into-joule.md @@ -55,7 +55,7 @@ SAP Joule The function extracts the result and shows it to the user ``` -> šŸ’” **Key insight:** Joule doesn't need to know that your agent uses CrewAI, Python, or any specific framework. It only speaks A2A — the same protocol you set up in Exercise 07. The capability YAML is just the bridge between Joule's chat UI and your A2A endpoint. +> šŸ’” **Key insight:** Joule doesn't need to know that your agent uses CrewAI, Python, or any specific framework. It only speaks A2A — the same protocol you set up in Exercise 08. The capability YAML is just the bridge between Joule's chat UI and your A2A endpoint. --- @@ -170,7 +170,7 @@ The `agent-request` action in your Joule function doesn't call your CF app direc > āš ļø **Replace `XX` with your participant number** (e.g., `INVESTIGATOR_AGENT_01`, `INVESTIGATOR_AGENT_02`). This ensures each participant has a unique destination. You'll use this exact name in all YAML files below. -> āš ļø **Replace ``** with the actual route of your CF app from Exercise 07. You can find it by running `cf app investigator-crew-a2a` or by checking the URL you noted at the end of Exercise 07. +> āš ļø **Replace ``** with the actual route of your CF app from Exercise 08. You can find it by running `cf app investigator-crew-a2a` or by checking the URL you noted at the end of Exercise 08. šŸ‘‰ Click **Save**. @@ -183,7 +183,7 @@ The `agent-request` action in your Joule function doesn't call your CF app direc > 4. Reads the Agent Card to discover the communication endpoint and protocol > 5. Sends an A2A `message/send` request with the user's message > -> This is the same discovery flow you tested manually with `curl` in Exercise 07 — now Joule does it automatically. +> This is the same discovery flow you tested manually with `curl` in Exercise 08 — now Joule does it automatically. --- @@ -377,7 +377,7 @@ The expression `apiResponse.body.artifacts[0].parts[0].text` walks this JSON pat | `.parts[0]` | The first part of that artifact | | `.text` | The actual text content of the investigation result | -> šŸ’” **This maps directly to what you built in Exercise 07.** In `server.py`, your `InvestigatorExecutor` emits a `TaskArtifactUpdateEvent` with `artifactId="investigation_result"` and a `TextPart`. That's exactly what Joule reads here through `artifacts[0].parts[0].text`. +> šŸ’” **This maps directly to what you built in Exercise 08.** In `server.py`, your `InvestigatorExecutor` emits a `TaskArtifactUpdateEvent` with `artifactId="investigation_result"` and a `TextPart`. That's exactly what Joule reads here through `artifacts[0].parts[0].text`. ### Step 5: Create the Digital Assistant Descriptor @@ -582,9 +582,10 @@ User sees investigation result in Joule chat 3. āœ… [Add custom tools](03-add-your-first-tool.md) 4. āœ… [Build a multi-agent system](04-building-multi-agent-system.md) 5. āœ… [Add the Grounding Service](05-add-the-grounding-service.md) -6. āœ… [Solve the crime](06-solve-the-crime.md) -7. āœ… [Deploy your agent to CF with A2A](07-deploy-agent-to-cf.md) -8. āœ… [Integrate your agent into SAP Joule](08-integrate-agent-into-joule.md) (this exercise) +6. āœ… [Discover Connected Crimes](06-discover-connected-crimes.md) +7. āœ… [Solve the crime](07-solve-the-crime.md) +8. āœ… [Deploy your agent to CF with A2A](08-deploy-agent-to-cf.md) +9. āœ… [Integrate your agent into SAP Joule](09-integrate-agent-into-joule.md) (this exercise) šŸŽ‰ **Congratulations!** You've completed the full CodeJam. You built a multi-agent AI system from scratch using CrewAI, deployed it to Cloud Foundry as an A2A server, and integrated it into SAP Joule — making it accessible to business users through natural language. diff --git a/exercises/readme.md b/exercises/readme.md index d3d3323..896f43b 100644 --- a/exercises/readme.md +++ b/exercises/readme.md @@ -24,7 +24,10 @@ Now that you understand the use case, choose your track and start the first exer - [Exercise 03 - Build your first agent tool](./Python/03-add-your-first-tool.md) - [Exercise 04 - Building a multi-agent system](./Python/04-building-multi-agent-system.md) - [Exercise 05 - Add the Grounding service](./Python/05-add-the-grounding-service.md) -- [Exercise 06 - Use your AI Agents to solve the crime](./Python/06-solve-the-crime.md) +- [Exercise 06 - Discover Connected Crimes with Web Search](./Python/06-discover-connected-crimes.md) +- [Exercise 07 - Use your AI Agents to solve the crime](./Python/07-solve-the-crime.md) +- [Exercise 08 - Deploy your agent to Cloud Foundry with A2A](./Python/08-deploy-agent-to-cf.md) +- [Exercise 09 - Integrate your agent into SAP Joule](./Python/09-integrate-agent-into-joule.md) ### Python (LangGraph + LiteLLM) @@ -34,8 +37,10 @@ Now that you understand the use case, choose your track and start the first exer - [Exercise 03 - Build your first agent tool](./Python-LangGraph/03-add-your-first-tool.md) - [Exercise 04 - Building a multi-agent system](./Python-LangGraph/04-building-multi-agent-system.md) - [Exercise 05 - Add the Grounding service](./Python-LangGraph/05-add-the-grounding-service.md) -- [Exercise 06 - Use your AI Agents to solve the crime](./Python-LangGraph/06-solve-the-crime.md) -- [Exercise 07 - Deploy your agent to Cloud Foundry with A2A](./Python-LangGraph/07-deploy-agent-to-cf.md) +- [Exercise 06 - Discover Connected Crimes with Web Search](./Python-LangGraph/06-discover-connected-crimes.md) +- [Exercise 07 - Use your AI Agents to solve the crime](./Python-LangGraph/07-solve-the-crime.md) +- [Exercise 08 - Deploy your agent to Cloud Foundry with A2A](./Python-LangGraph/08-deploy-agent-to-cf.md) +- [Exercise 09 - Integrate your agent into SAP Joule](./Python-LangGraph/09-integrate-agent-into-joule.md) ### JavaScript (LangGraph + SAP Cloud SDK for AI) @@ -45,5 +50,7 @@ Now that you understand the use case, choose your track and start the first exer - [Exercise 03 - Build your first agent tool](./JavaScript/03-add-your-first-tool.md) - [Exercise 04 - Building a multi-agent system](./JavaScript/04-building-multi-agent-system.md) - [Exercise 05 - Add the Grounding service](./JavaScript/05-add-the-grounding-service.md) -- [Exercise 06 - Use your AI Agents to solve the crime](./JavaScript/06-solve-the-crime.md) -- [Exercise 07 - Deploy your agent to Cloud Foundry with A2A](./JavaScript/07-deploy-agent-to-cf-ts.md) +- [Exercise 06 - Discover Connected Crimes with Web Search](./JavaScript/06-discover-connected-crimes.md) +- [Exercise 07 - Use your AI Agents to solve the crime](./JavaScript/07-solve-the-crime.md) +- [Exercise 08 - Deploy your agent to Cloud Foundry with A2A](./JavaScript/08-deploy-agent-to-cf-ts.md) +- [Exercise 09 - Integrate your agent into SAP Joule](./JavaScript/09-integrate-agent-into-joule.md) diff --git a/project/JavaScript/solution/src/agentConfigs.ts b/project/JavaScript/solution/src/agentConfigs.ts index 877b0b2..85c49a5 100644 --- a/project/JavaScript/solution/src/agentConfigs.ts +++ b/project/JavaScript/solution/src/agentConfigs.ts @@ -11,24 +11,48 @@ export const AGENT_CONFIGS = { Search for evidence related to each suspect and identify connections to the crime.`, }, + intelligenceResearcher: { + systemPrompt: (suspectNames: string) => `You are an Open-Source Intelligence (OSINT) Researcher. + You are an OSINT specialist who excels at finding patterns across multiple crime scenes. + You search public databases, news archives, and criminal records to connect seemingly isolated incidents. + Your expertise has uncovered several international art theft rings, and you know how to distinguish professional criminals from amateurs. + + Your goal: Search the web for similar art thefts, criminal patterns, and suspect backgrounds to determine if this heist is part of a larger criminal network + + Analyze the suspects: ${suspectNames} + + Search for: + 1. Public criminal records or prior convictions for each suspect + 2. Similar art theft incidents with the same modus operandi (insider job, no forced entry) + 3. Connections to known art theft rings or criminal networks + 4. News reports or public information about any of the suspects + 5. Recent museum heists in Europe with similar patterns`, + }, leadDetective: { - systemPrompt: (appraisalResult: string, evidenceAnalysis: string, suspectNames: string) => + systemPrompt: ( + appraisalResult: string, + evidenceAnalysis: string, + intelligenceReport: string, + suspectNames: string, + ) => `You are the lead detective on this high-profile art theft case. With years of experience solving complex crimes, you excel at synthesizing information from multiple sources and identifying the culprit based on evidence and expert analysis. - + Your goal: Synthesize all findings from the team to identify the most likely suspect and build a comprehensive case - + You have received the following information from your team: 1. INSURANCE APPRAISAL: ${appraisalResult} - 2. EVIDENCE ANALYSIS: ${evidenceAnalysis} - 3. SUSPECTS: ${suspectNames} + 2. EVIDENCE ANALYSIS (Internal Documents): ${evidenceAnalysis} + 3. INTELLIGENCE REPORT (Web Search): ${intelligenceReport} + 4. SUSPECTS: ${suspectNames} Based on all the evidence and analysis, determine: - Who is the most likely culprit? - What evidence supports this conclusion? - What was their motive and opportunity? + - Is this an isolated incident or part of a larger criminal network? - Summarise the insurance appraisal values of the stolen artworks. - Calculate the total estimated insurance value of the stolen items based on the appraisal results. - Provide a comprehensive summary of the case. diff --git a/project/JavaScript/solution/src/investigationWorkflow.ts b/project/JavaScript/solution/src/investigationWorkflow.ts index 53c55a3..f3c4fc2 100644 --- a/project/JavaScript/solution/src/investigationWorkflow.ts +++ b/project/JavaScript/solution/src/investigationWorkflow.ts @@ -2,7 +2,7 @@ import { StateGraph, END, START } from "@langchain/langgraph"; import { OrchestrationClient } from "@sap-ai-sdk/orchestration"; import { AgentState } from "./types.js"; import type { AgentStateType, RPT1Payload } from "./types.js"; -import { callRPT1Tool, callGroundingServiceTool } from "./tools.js"; +import { callRPT1Tool, callGroundingServiceTool, callSonarProSearchTool } from "./tools.js"; import { AGENT_CONFIGS } from "./agentConfigs.js"; export class InvestigationWorkflow { @@ -15,10 +15,12 @@ export class InvestigationWorkflow { workflow .addNode("appraiser", this.appraiserNode.bind(this)) .addNode("evidence_analyst", this.evidenceAnalystNode.bind(this)) + .addNode("intelligence_researcher", this.intelligenceResearcherNode.bind(this)) .addNode("lead_detective", this.leadDetectiveNode.bind(this)) .addEdge(START, "appraiser") .addEdge("appraiser", "evidence_analyst") - .addEdge("evidence_analyst", "lead_detective") + .addEdge("evidence_analyst", "intelligence_researcher") + .addEdge("intelligence_researcher", "lead_detective") .addEdge("lead_detective", END); return workflow; @@ -104,6 +106,51 @@ export class InvestigationWorkflow { } } + private async intelligenceResearcherNode(state: AgentStateType): Promise> { + console.log("\nšŸ” Intelligence Researcher starting web search..."); + + try { + const suspects = state.suspect_names.split(",").map((s) => s.trim()); + const intelligenceResults: string[] = []; + + for (const suspect of suspects) { + console.log(` Searching public records for: ${suspect}`); + const query = `${suspect} criminal record art theft security technician Europe background check`; + const result = await callSonarProSearchTool(query); + intelligenceResults.push(`Background check for ${suspect}:\n${result}`); + } + + console.log(" Searching for similar art theft incidents..."); + const patternQuery = + "museum art theft insider job no forced entry Europe similar incidents criminal network"; + const patternResult = await callSonarProSearchTool(patternQuery); + intelligenceResults.push(`Similar Art Theft Patterns:\n${patternResult}`); + + const intelligenceReport = `Intelligence Research Complete: ${intelligenceResults.join("\n\n")} + Summary: Conducted OSINT research on all suspects and identified similar crime patterns`; + + console.log("āœ… Intelligence research complete"); + + return { + intelligence_report: intelligenceReport, + messages: [...state.messages, { role: "assistant", content: intelligenceReport }], + }; + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + console.error("āŒ Intelligence research failed:", errorMsg); + if (error instanceof Error && error.stack) { + console.error(error.stack); + } + return { + intelligence_report: `Error during intelligence research: ${errorMsg}`, + messages: [ + ...state.messages, + { role: "assistant", content: `Error during intelligence research: ${errorMsg}` }, + ], + }; + } + } + private async leadDetectiveNode(state: AgentStateType): Promise> { console.log("\nšŸ” Lead Detective analyzing all findings..."); @@ -118,6 +165,7 @@ export class InvestigationWorkflow { content: AGENT_CONFIGS.leadDetective.systemPrompt( state.appraisal_result || "No appraisal result available", state.evidence_analysis || "No evidence analysis available", + state.intelligence_report || "No intelligence report available", state.suspect_names, ), }, @@ -156,6 +204,8 @@ export class InvestigationWorkflow { console.log(result.appraisal_result ?? "(not set)"); console.log("\n--- Evidence Analysis ---"); console.log(result.evidence_analysis ?? "(not set)"); + console.log("\n--- Intelligence Report ---"); + console.log(result.intelligence_report ?? "(not set)"); return result.final_conclusion || "Investigation completed but no conclusion was reached."; } diff --git a/project/JavaScript/solution/src/tools.ts b/project/JavaScript/solution/src/tools.ts index 3d1594b..df0e1fb 100644 --- a/project/JavaScript/solution/src/tools.ts +++ b/project/JavaScript/solution/src/tools.ts @@ -67,3 +67,41 @@ export async function callGroundingServiceTool(user_question: string): Promise { + try { + const response = await webSearchClient.chatCompletion({ + placeholderValues: { search_query }, + }); + return response.getContent() ?? "No search results found"; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + console.error("āŒ Sonar-pro web search failed:", errorMessage); + if (error instanceof Error && error.stack) console.error(error.stack); + return `Error calling sonar-pro web search: ${errorMessage}`; + } +} diff --git a/project/JavaScript/solution/src/types.ts b/project/JavaScript/solution/src/types.ts index 8df97c5..bbc4c8a 100644 --- a/project/JavaScript/solution/src/types.ts +++ b/project/JavaScript/solution/src/types.ts @@ -40,6 +40,10 @@ export const AgentState = Annotation.Root({ reducer: (_, update) => update, default: () => undefined, }), + intelligence_report: Annotation({ + reducer: (_, update) => update, + default: () => undefined, + }), final_conclusion: Annotation({ reducer: (_, update) => update, default: () => undefined, diff --git a/project/Python-LangGraph/solution/config/agents.py b/project/Python-LangGraph/solution/config/agents.py index 2806359..78920ea 100644 --- a/project/Python-LangGraph/solution/config/agents.py +++ b/project/Python-LangGraph/solution/config/agents.py @@ -19,18 +19,31 @@ ), } +WEB_RESEARCHER_AGENT = { + "name": "intelligence_researcher_agent", + "prompt": ( + "You are an Open-Source Intelligence (OSINT) specialist. " + "Search the web for criminal records, similar art thefts, and suspect backgrounds. " + "Focus on public information: news articles, court records, criminal databases. " + "Always provide source citations with URLs and dates." + ), +} + -def _lead_detective_prompt(appraisal_result: str, evidence_analysis: str, suspect_names: str) -> str: +def _lead_detective_prompt(appraisal_result: str, evidence_analysis: str, intelligence_report: str, suspect_names: str) -> str: return ( "You are the Lead Detective coordinating an art theft investigation. " "You have received the following information from your team:\n\n" f"1. INSURANCE APPRAISAL:\n{appraisal_result}\n\n" - f"2. EVIDENCE ANALYSIS:\n{evidence_analysis}\n\n" - f"3. SUSPECTS: {suspect_names}\n\n" + f"2. EVIDENCE ANALYSIS (Internal Documents):\n{evidence_analysis}\n\n" + f"3. INTELLIGENCE REPORT (Web Search):\n{intelligence_report}\n\n" + f"4. SUSPECTS: {suspect_names}\n\n" "Based on all the evidence and analysis, you MUST:\n" "- Name the most likely thief and explain the evidence supporting that conclusion\n" + "- Consider both internal evidence and external criminal patterns\n" "- Note any alibis or evidence that clears the other suspects\n" "- State the total insured value of the stolen goods\n" + "- Assess whether this is an isolated incident or part of a larger criminal network\n" "- Provide a comprehensive summary of the case." ) diff --git a/project/Python-LangGraph/solution/investigator_graph.py b/project/Python-LangGraph/solution/investigator_graph.py index f503b66..9099a2a 100644 --- a/project/Python-LangGraph/solution/investigator_graph.py +++ b/project/Python-LangGraph/solution/investigator_graph.py @@ -13,7 +13,7 @@ from gen_ai_hub.orchestration.models.document_grounding import DataRepositoryType import json -from config.agents import APPRAISER_AGENT, EVIDENCE_ANALYST_AGENT, LEAD_DETECTIVE +from config.agents import APPRAISER_AGENT, EVIDENCE_ANALYST_AGENT, LEAD_DETECTIVE, WEB_RESEARCHER_AGENT # Load .env from the same directory as this script env_path = Path(__file__).parent / '.env' @@ -28,6 +28,7 @@ class AgentState(TypedDict): suspect_names: str appraisal_result: Optional[str] evidence_analysis: Optional[str] + intelligence_report: Optional[str] final_conclusion: Optional[str] messages: list @@ -66,6 +67,29 @@ def call_grounding_service(user_question: str) -> str: return json.dumps(response.model_dump(), indent=2) +def call_sonar_pro_search(query: str) -> str: + """Search the web using Perplexity's sonar-pro model for real-time intelligence.""" + from litellm import completion + try: + response = completion( + model="sap/sonar-pro", + messages=[ + { + "role": "system", + "content": "You are a web search assistant specializing in criminal intelligence. Search for accurate, recent information and always provide source citations with URLs and dates." + }, + { + "role": "user", + "content": query + } + ], + temperature=0.2, + ) + return response.choices[0].message.content + except Exception as e: + return f"Error calling sonar-pro web search: {str(e)}" + + # Initialize the shared LLM model = ChatLiteLLM(model="sap/anthropic--claude-4.5-opus", temperature=0) @@ -131,6 +155,44 @@ def evidence_analyst_node(state: AgentState) -> dict: } +def intelligence_researcher_node(state: AgentState) -> dict: + print("\nšŸ” Intelligence Researcher starting web search...") + + try: + suspects = [s.strip() for s in state["suspect_names"].split(",")] + intelligence_results = [] + + for suspect in suspects: + print(f" Searching public records for: {suspect}") + query = f"{suspect} criminal record art theft security technician Europe background check" + result = call_sonar_pro_search(query) + intelligence_results.append(f"Background check for {suspect}:\n{result}") + + print(" Searching for similar art theft incidents...") + pattern_query = "museum art theft insider job no forced entry Europe similar incidents criminal network" + pattern_result = call_sonar_pro_search(pattern_query) + intelligence_results.append(f"Similar Art Theft Patterns:\n{pattern_result}") + + intelligence_report = ( + "Intelligence Research Complete:\n\n" + "\n\n".join(intelligence_results) + + f"\n\nSummary: Conducted OSINT research on all suspects and identified similar crime patterns" + ) + + print("āœ… Intelligence research complete") + + return { + "intelligence_report": intelligence_report, + "messages": state["messages"] + [{"role": "assistant", "content": intelligence_report}], + } + except Exception as e: + error_msg = f"Error during intelligence research: {e}" + print(f"āŒ {error_msg}") + return { + "intelligence_report": error_msg, + "messages": state["messages"] + [{"role": "assistant", "content": error_msg}], + } + + def lead_detective_node(state: AgentState) -> dict: print("\nšŸ” Lead Detective analyzing all findings...") @@ -139,6 +201,7 @@ def lead_detective_node(state: AgentState) -> dict: SystemMessage(content=LEAD_DETECTIVE["prompt"]( state["appraisal_result"] or "No appraisal result available", state["evidence_analysis"] or "No evidence analysis available", + state.get("intelligence_report") or "No intelligence report available", state["suspect_names"], )), HumanMessage(content="Analyze all the evidence and identify the culprit. Provide a detailed conclusion."), @@ -165,10 +228,12 @@ def build_graph(): workflow.add_node("appraiser", appraiser_node) workflow.add_node("evidence_analyst", evidence_analyst_node) + workflow.add_node("intelligence_researcher", intelligence_researcher_node) workflow.add_node("lead_detective", lead_detective_node) workflow.add_edge(START, "appraiser") workflow.add_edge("appraiser", "evidence_analyst") - workflow.add_edge("evidence_analyst", "lead_detective") + workflow.add_edge("evidence_analyst", "intelligence_researcher") + workflow.add_edge("intelligence_researcher", "lead_detective") workflow.add_edge("lead_detective", END) return workflow.compile() diff --git a/project/Python-LangGraph/solution/main.py b/project/Python-LangGraph/solution/main.py index ea0a475..ccb4b54 100644 --- a/project/Python-LangGraph/solution/main.py +++ b/project/Python-LangGraph/solution/main.py @@ -8,6 +8,7 @@ def main(): "suspect_names": "Sophie Dubois, Marcus Chen, Viktor Petrov", "appraisal_result": None, "evidence_analysis": None, + "intelligence_report": None, "final_conclusion": None, "messages": [], }) @@ -22,6 +23,11 @@ def main(): print("="*50) print(result["evidence_analysis"] or "(not set)") + print("\n" + "="*50) + print("Intelligence Report:") + print("="*50) + print(result["intelligence_report"] or "(not set)") + print("\n" + "="*50) print("Investigation Report:") print("="*50) diff --git a/project/Python-LangGraph/solution/server.py b/project/Python-LangGraph/solution/server.py index 192d32d..2926fff 100644 --- a/project/Python-LangGraph/solution/server.py +++ b/project/Python-LangGraph/solution/server.py @@ -57,6 +57,7 @@ async def execute(self, context: RequestContext, event_queue: EventQueue) -> Non "suspect_names": suspect_names, "appraisal_result": None, "evidence_analysis": None, + "intelligence_report": None, "final_conclusion": None, "messages": [], }), diff --git a/project/Python/solution/config/agents.yaml b/project/Python/solution/config/agents.yaml index 52c8281..721b2e7 100644 --- a/project/Python/solution/config/agents.yaml +++ b/project/Python/solution/config/agents.yaml @@ -12,13 +12,29 @@ evidence_analyst_agent: role: > Criminal Evidence Analyst goal: > - Retrieve and analyze evidence ONLY via the call_grounding_service tool. + Retrieve and analyze evidence ONLY via the call_grounding_service tool. Search for each suspect by name: Sophie Dubois, Marcus Chen, Viktor Petrov. Do NOT fabricate any evidence or alibis. Report only what the tool returns. backstory: > You are a methodical evidence analyst who bases conclusions strictly on retrieved documents. You never assume facts. llm: sap/anthropic--claude-4.5-opus +intelligence_researcher_agent: + role: > + Open-Source Intelligence (OSINT) Researcher + goal: > + Search the web for similar art thefts, criminal patterns, and suspect backgrounds + to determine if this heist is part of a larger criminal network. Use the + call_sonar_pro_search tool to find recent incidents, news reports, and public + criminal records for all three suspects: {suspect_names}. + backstory: > + You are an OSINT specialist who excels at finding patterns across multiple + crime scenes. You search public databases, news archives, and criminal records + to connect seemingly isolated incidents. Your expertise has uncovered several + international art theft rings, and you know how to distinguish professional + criminals from amateurs. + llm: sap/gpt-4o + lead_detective_agent: role: > Lead Detective and Case Manager diff --git a/project/Python/solution/config/tasks.yaml b/project/Python/solution/config/tasks.yaml index c30d997..90ba999 100644 --- a/project/Python/solution/config/tasks.yaml +++ b/project/Python/solution/config/tasks.yaml @@ -7,7 +7,7 @@ appraise_loss_task: analyze_evidence_task: description: > - Analyze the evidence of the theft that you can access via the grounding tool. + Analyze the evidence of the theft that you can access via the grounding tool. Provide any insights that can help in the investigation especially regarding alibis. Check the evidence for all three suspect names 1. Sophie Dubois, 2. Marcus Chen and 3. Viktor Petrov and provide an analysis for each of them. @@ -15,11 +15,37 @@ analyze_evidence_task: A detailed analysis of the evidence for each suspect, including any insights that can help in the investigation. agent: evidence_analyst_agent +research_criminal_network: + description: > + Search the web for intelligence about the three suspects ({suspect_names}) and + related crimes. Use the call_sonar_pro_search tool to find: + 1. Public criminal records or prior convictions for each suspect + 2. Similar art theft incidents with the same modus operandi (insider job, no forced entry) + 3. Connections to known art theft rings or criminal networks + 4. News reports or public information about any of the suspects + 5. Recent museum heists in Europe with similar patterns + + Cross-reference your web findings with the internal evidence analyzed by the + evidence analyst. Focus on discovering whether this is an isolated incident or + part of a larger criminal operation. + expected_output: > + A comprehensive intelligence report containing: + - Background checks for all three suspects with web sources + - List of similar art thefts found online (dates, locations, MO) + - Evidence of criminal network connections (if any) + - Assessment: isolated incident vs. organized crime ring + - All findings MUST include web sources with URLs and dates + agent: intelligence_researcher_agent + solve_crime: description: > - Find the thief from, the suspects by activating the evidence investigator agent and instructing him to look for the three suspects - using the grounding tool. He should find information on alibies and motives and return a report for you to analyze. And use the appraise_loss_task from - the appraiser agent to find the value of the stolen goods. + Find the thief among the suspects by reviewing: + 1. The insurance appraisal values from the appraiser agent + 2. The internal evidence analysis from the evidence analyst agent + 3. The web intelligence report from the intelligence researcher agent + + Synthesize all three sources to identify the culprit with high confidence. + Consider both internal evidence and external patterns/connections found online. expected_output: > The name of the thief and the total value of the stolen goods for the insurance. agent: lead_detective_agent \ No newline at end of file diff --git a/project/Python/solution/investigator_crew.py b/project/Python/solution/investigator_crew.py index 910ca7e..dc7d7bb 100644 --- a/project/Python/solution/investigator_crew.py +++ b/project/Python/solution/investigator_crew.py @@ -68,7 +68,45 @@ def call_grounding_service(user_question: str) -> str: response_dict = json.dumps(response.model_dump(), indent=2) # Convert to JSON string return response_dict # Return retrieved document chunks to the agent - + + +@tool("call_sonar_pro_search") +def call_sonar_pro_search(search_query: str) -> str: + """Search the web using Perplexity's sonar-pro model for real-time information + about crimes, suspects, and criminal patterns. Use this to find similar incidents, + criminal networks, public records, or patterns that are not in internal documents. + + Args: + search_query: The search query about crimes, suspects, or criminal patterns + + Returns: + Search results with source citations from the web + """ + from litellm import completion + + try: + response = completion( + model="sap/sonar-pro", # Perplexity model with web search + messages=[ + { + "role": "system", + "content": "You are a web search assistant specializing in criminal intelligence. Search for accurate, recent information and always provide source citations with URLs and dates." + }, + { + "role": "user", + "content": search_query + } + ], + temperature=0.2, # Lower temperature for factual search + ) + + result = response.choices[0].message.content + return result + + except Exception as e: + return f"Error calling sonar-pro web search: {str(e)}" + + @CrewBase class InvestigatorCrew(): """InvestigatorCrew crew""" @@ -103,7 +141,22 @@ def analyze_evidence_task(self) -> Task: return Task( config=self.tasks_config['analyze_evidence_task'] ) - + + @agent + def intelligence_researcher_agent(self) -> Agent: + return Agent( + config=self.agents_config['intelligence_researcher_agent'], + verbose=True, + tools=[call_sonar_pro_search] # Web search tool + ) + + @task + def research_criminal_network(self) -> Task: + return Task( + config=self.tasks_config['research_criminal_network'], + context=[self.analyze_evidence_task()] # Uses internal evidence to inform web searches + ) + @agent def lead_detective_agent(self) -> Agent: return Agent( @@ -115,7 +168,7 @@ def lead_detective_agent(self) -> Agent: def solve_crime(self) -> Task: return Task( config=self.tasks_config['solve_crime'], - context=[self.appraise_loss_task(), self.analyze_evidence_task()] # šŸ‘ˆ Lead detective uses results from other tasks + context=[self.appraise_loss_task(), self.analyze_evidence_task(), self.research_criminal_network()] ) @crew