# Agent Building (TryHackMe)

[Challenge on TryHackMe: Agent Building](https://tryhackme.com/room/agentbuilding)

## Introduction

In the previous rooms, [Agent Discovery](https://tryhackme.com/room/agentdiscovery), [Agent Design](https://tryhackme.com/room/agentdesign), and [Agent Foundations](https://tryhackme.com/room/agentfoundations), you explored what AI agents are, identified where they can support a security workflow, and designed the NorthStar Fashion Security Investigation Agent.

You defined its purpose, selected the capabilities it needs, established its boundaries, and explored the core concepts behind tool use, state, and agent workflows.

![its time to build the agent system](https://cdn-images.tryhackme.com/user-uploads/5f5ed9259575d24307292950/room-content/5f5ed9259575d24307292950-1787344197505.png align="center")

**Now it is time to build it.**

In this room, you will progressively assemble the **NorthStar Fashion Security Investigation Agent** and connect it to the evidence sources required for alert investigations. You will begin with the agent’s instructions and behaviour, then add tools to retrieve alerts, search related SIEM logs, check IP reputation, and consult relevant organisational context. Finally, you will introduce conversation memory, so follow-up questions can continue from an existing investigation.

Each capability adds another step to the investigation workflow. The agent begins with a security alert, retrieves the relevant evidence, searches related SIEM logs, checks external context such as IP reputation, reviews organisational information, and then combines those findings to produce a supported verdict for the engineer to review.

## **Learning Objectives**

By the end of this room, you will be able to:

*   Build a Security Investigation Agent from a defined design
    
*   Connect tools that retrieve alerts and search SIEM logs
    
*   Correlate evidence across accounts, IP addresses, devices, events, and timestamps
    
*   Add external and organisational context to an investigation
    
*   Use conversation memory to support follow-up questions
    
*   Produce evidence-based verdicts while keeping final security decisions with the analyst
    

## **Prerequisites**

Before starting this room, you should understand the basic concepts introduced in:

*   [Agent Discovery](https://tryhackme.com/room/agentdiscovery)
    
*   [Agent Design](https://tryhackme.com/room/agentdesign)
    
*   [Agent Foundations](https://tryhackme.com/room/agentfoundations)
    

You should be familiar with AI agents, tools, prompts, structured outputs, state, and basic agent workflows.

## Follow the Evidence

Before giving the Security Investigation Agent access to SIEM logs, you will investigate one alert manually. This exercise shows why an analyst cannot simply search for a username or email and immediately reach a reliable conclusion.

## **Meet the SIEM**

A Security Information and Event Management system, or SIEM, collects security data so analysts can review activity and investigate alerts. The SIEM is already running on the lab machine.

Northstar Fashion uses Google Cloud Identity as its identity provider (IdP). The SIEM consumes its identity activity, normalises it into consistent logs, and generates alerts from those logs.

The SIEM has two main views:

*   `LOGS` contains normalised events collected from Google Cloud Identity.
    
*   `ALERTS` contains detections that may require investigation.
    

![An image of a SIEM](https://cdn-images.tryhackme.com/user-uploads/5f5ed9259575d24307292950/room-content/5f5ed9259575d24307292950-1787586990351.png align="center")

Open [`http://MACHINE_IP:8000/`](http://MACHINE_IP:8000/) and sign in with the following credentials:

*   Operator: analyst
    
*   Passphrase: analyst123
    

The timestamps shown in the screenshots may differ from those in your lab because the SIEM dynamically adjusts its alerts and logs to the current date and time.

**Note:** For the best experience, view the lab in full-screen mode. On smaller or split-screen layouts, some tables may require horizontal scrolling to see all fields, especially when searching by account.

## **Start With the Alert**

Open the **Alerts** tab and select `ALT-001`, named **Successful Office Sign-in**.

![SIEM Alert Queue](https://cdn-images.tryhackme.com/user-uploads/5f5ed9259575d24307292950/room-content/5f5ed9259575d24307292950-1787586990369.png align="center")

The alert says that [`maria.stow@northstar.fashion`](mailto:maria.stow@northstar.fashion) successfully authenticated from `198.51.100.24` on `NS-LT-002`. It also provides the activity time and event name, but it does not show what happened before or after the sign-in.

These alert fields give us several possible pivots:

*   Account: [`maria.stow@northstar.fashion`](mailto:maria.stow@northstar.fashion)
    
*   Source IP: `198.51.100.24`
    
*   Device: `NS-LT-002`
    
*   Event: `login_success`
    
*   Activity time: `07:52:04 UTC`
    

We will begin with the account because it identifies the person involved.

## **Search by Account**

Open the **Logs** tab, enter the following query, and select **Search**:

```text
actor.principal_email="maria.stow@northstar.fashion"
```

![Logs Matching Maria's Account](https://cdn-images.tryhackme.com/user-uploads/5f5ed9259575d24307292950/room-content/5f5ed9259575d24307292950-1787586990522.png align="center")

The query returns 49 logs. The results include successful logins, device activity, OAuth events, session changes, and other activity from different devices and times. Searching by account found relevant information, but it did not isolate the event being investigated.

An analyst could begin opening every result, but it is more efficient to return to the alert and add another entity.

## **Add the Source IP**

Combine the account with the source IP by using `AND`:

```text
actor.principal_email="maria.stow@northstar.fashion" AND network.source_ip="198.51.100.24"
```

![Logs Matching Maria's Account and IP](https://cdn-images.tryhackme.com/user-uploads/5f5ed9259575d24307292950/room-content/5f5ed9259575d24307292950-1787586990381.png align="center")

The result count falls from 49 to 44. This is only a small improvement because Maria performs many activities from the office IP. A familiar IP can appear in many unrelated events, so it cannot identify the alert activity by itself.

Return to the alert again and take the device name as the next pivot.

## **Add the Device**

Add `NS-LT-002` to the existing query:

```text
actor.principal_email="maria.stow@northstar.fashion" AND network.source_ip="198.51.100.24" AND device.device_name="NS-LT-002"
```

![Logs Matching Maria's Account IP and Device](https://cdn-images.tryhackme.com/user-uploads/5f5ed9259575d24307292950/room-content/5f5ed9259575d24307292950-1787586990364.png align="center")

The three-field query returns 21 logs. The results are more focused, but they still contain activity from different days and several event types. The analyst must now compare timestamps and event names with the alert rather than assuming every matching log belongs to the same activity.

## **Inspect the Matching Event**

The alert occurred at `07:52:04 UTC` and describes a `login_success`. Find the result with the same time and event, then select it to open the log details.

![Inspecting Matching Login Event](https://cdn-images.tryhackme.com/user-uploads/5f5ed9259575d24307292950/room-content/5f5ed9259575d24307292950-1787586990411.png align="center")

This log supports the event described by the alert, but one event still does not explain the surrounding timeline. Depending on the alert, the analyst may need to review earlier authentication attempts, later account changes, MFA activity, OAuth grants, or events from another source.

## **Why This Becomes Difficult**

The investigation required repeatedly moving between the alert and the logs, extracting relevant fields, rebuilding queries, checking result counts, and comparing individual events. Even after combining the account, IP address, and device, 21 logs remained. This process must also adapt to each alert type: an OAuth alert may require an application or client ID, while an administrative alert may depend on a target account, role, or group. The relevant fields and number of searches are therefore not known in advance.

This becomes difficult to scale when the analyst is the company’s only security engineer and must also support infrastructure, software, and other technical work. After completing this investigation, 50 more alerts remain in the queue, with a similar workload arriving every day.

Throughout this room, you will address this problem by building the **Security Investigation Agent**. You will give it tools to retrieve alerts, search and correlate relevant logs, check IP reputation, consult the engineer’s internal knowledge base, and retain conversational context so follow-up questions can continue without repeating the entire investigation.

### Answer the questions below

How many logs were returned when searching only Maria's account?

```python
actor.principal_email="maria.stow@northstar.fashion"
```

![](https://cdn.hashnode.com/uploads/covers/5f4a98085ee1ba597542e097/4625643c-cc92-41d8-b3cb-809ac0d86b33.png align="center")

How many logs remained after adding the source IP?

```python
actor.principal_email="maria.stow@northstar.fashion" AND network.source_ip="198.51.100.24"
```

![](https://cdn.hashnode.com/uploads/covers/5f4a98085ee1ba597542e097/b13f70f5-39ee-485f-b506-6ea9245e3ec6.png align="center")

How many logs remained after adding the device?

```python
actor.principal_email="maria.stow@northstar.fashion" AND network.source_ip="198.51.100.24" AND device.device_name="NS-LT-002"
```

![](https://cdn.hashnode.com/uploads/covers/5f4a98085ee1ba597542e097/2abe9721-3c3b-4402-ae1c-6cd8fdb5431e.png align="center")

## Build the Security Investigation Agent

In the previous task, you investigated a security alert manually by extracting relevant entities, refining SIEM searches, and comparing related activity.

Now you will begin building the **NorthStar Fashion Security Investigation Agent**.

As the room progresses, you will add the capabilities it needs to retrieve alerts, search related logs, check external reputation information, consult organisational context, and support follow-up questions.

For now, you will create the first version of the agent and observe how it behaves when asked to investigate an alert **without access to any security tools**.

## **Meet the TryHackMe AI Service**

The lab provides access to an AI model through the TryHackMe AI service. A helper module named `thm_`[`ai.py`](http://ai.py) is already included in the project to handle communication with the service and securely use the temporary AI token provided to the lab environment.

You do not need to manage the model provider, API credentials, or token directly. Throughout this room, `thm_`[`ai.py`](http://ai.py) will act as the interface between your investigation code and the TryHackMe AI service, keeping the agent logic separate from the underlying model infrastructure.

Navigate to the `agent-building` directory and open `agent_`[`task3.py`](http://task3.py).

Terminal

```powershell
user@machine$ nano agent_task3.py
```

In the starter file, you will find the following import:

```python
from thm_ai import THMAgentClient, THMAgentError
```

`THMAgentClient` sends messages to the TryHackMe AI service, while `THMAgentError` allows the application to handle errors returned by the service.

The AI service used by this room also has **platform-level system instructions** configured by TryHackMe.

Those instructions define the tool-routing contract used throughout the lab. In particular, the model can request an investigation capability by returning structured JSON similar to:

```json
{
  "type": "tool_call",
  "name": "get_alert",
  "arguments": {
    "alert_id": "ALT-051"
  }
}
```

Your Python code does not replace those system instructions. Instead, it extends them with the investigation-specific behaviour, verdict rules, output expectations, and security boundaries required by the NorthStar Fashion Security Investigation Agent.

## **Create the THM AI Client**

Now locate the first `TODO` in `agent_`[`task3.py`](http://task3.py):

```python
# TODO: Create the THM AI client
client = ...
```

Create an instance of `THMAgentClient`:

```python
client = THMAgentClient()
```

The application can now communicate with the model, but it has not yet defined what kind of agent the model should behave as.

## **Define the Agent's Role**

An AI model becomes useful in an application when the surrounding application gives it a clear purpose and boundaries.

The Security Investigation Agent needs to know:

*   What role it performs
    
*   What evidence it may use
    
*   Which verdicts it may return
    
*   Which actions are outside its authority
    

Find the next `TODO` and replace it with:

```python
AGENT_INSTRUCTIONS = (
    "You are the Security Investigation Agent for NorthStar Fashion, a SOC "
    "assistant. Analyse only the alert and evidence supplied in this "
    "conversation - never invent SIEM data. "
    "Use only these verdicts: TruePositive, BenignPositive, FalsePositive, "
    "or InsufficientEvidence. Respond with a Verdict, Key Evidence, a "
    "one-sentence Reason, and a Recommendation. "
    "You cannot close alerts, change SIEM state, perform containment, block "
    "IP addresses, disable accounts, or run commands - you only support the "
    "human investigation."
)
```

Notice that these instructions define both **capability** and **authority**. The agent may analyse evidence and recommend a verdict, but it must not:

*   Close alerts
    
*   Block IP addresses
    
*   Disable accounts
    
*   Perform containment actions
    

These actions remain outside the agent’s authority and require human involvement.

The agent can only return one of four controlled verdicts:

*   `TruePositive`
    
*   `BenignPositive`
    
*   `FalsePositive`
    
*   `InsufficientEvidence`
    

Constraining the verdict vocabulary makes later investigations easier to validate, compare, and evaluate.

## **Send an Investigation Request**

The next step is to ask the agent to investigate a real NorthStar Fashion alert:

```python
investigation_request = "Investigate alert ALT-051."
```

At this point, however, `ALT-051` is only a string; the application has not yet retrieved the corresponding alert from the SIEM.

Find the final `TODO` and complete it with:

```python
response = client.send_message(
    f"{AGENT_INSTRUCTIONS}\n\nUser request: {investigation_request}"
)
```

The application sends the agent instructions together with the user request to the TryHackMe AI service, and the generated assistant response is then returned in `response["message"]["content"]`.

## **Run Your First Agent**

Run:

```python
user@machine$ python3 agent_task3.py
```

The program sends `Investigate alert ALT-051.` to the model. Look closely at the response: the model understands that this is an investigation request and that `ALT-051` appears to be an alert identifier, but it does **not** know what happened in the alert because the application currently sends only the user request to the AI model. There is still no connection to SIEM, logs, IP reputation, and organisation context.

A secure agent should not invent missing information. Instead, it should recognise that additional evidence is required and return `InsufficientEvidence`.

This first implementation demonstrates an important agent engineering principle: **Reasoning about a capability does not grant access to that capability.**

The model may understand how to investigate suspicious authentication activity, but the application still has no mechanism for retrieving the alert itself. In the next task, you will extend the architecture by adding the first approved investigation capabilities, `list_alerts()` and `get_alert()`, allowing the application to retrieve real SIEM evidence before asking the model to analyse it.

### Answer the questions below

Which method sends a message to the AI service? `send_message`

Which response field contains the generated assistant text? `Content`

Can the agent retrieve `ALT-051` from the SIEM at this stage? (Yea/Nay) `Nay`

## Give the Agent Investigation Tools

In the previous task, the Security Investigation Agent received:

```plaintext
Investigate alert ALT-051.
```

The model recognised that it needed more information and returned a structured capability request similar to:

```json
{
  "type": "tool_call",
  "name": "get_alert",
  "arguments": {
    "alert_id": "ALT-051"
  }
}
```

However, the application could not act on that request. Although the model could request a capability, there was no execution layer to verify that the capability was allowed, call the corresponding Python function, collect the result, and return the evidence to the model.

In this task, you will build that missing execution layer by adding the first two approved investigation capabilities:

```plaintext
list_alerts()
get_alert()
```

Then, you’ll connect the model's tool requests to real data from the NorthStar Fashion SIEM.

## **Read the API Documentation**

Before giving the agent access to the SIEM, it helps to understand the system you are connecting.

The SIEM publishes an API reference at:

```plaintext
http://MACHINE_IP:8000/api/docs
```

Keep the API reference open while completing this task, as it documents the available endpoints, authentication headers, accepted parameters, and response schemas. For this task, we will use:

| **Method and path** | **Purpose** |
| --- | --- |
| `GET /services/siem/alerts` | List a page of alerts |
| `GET /services/siem/alerts/{id}` | Retrieve one complete alert |

Both endpoints require the API key in the `X-SIEM-API-Key` header.

Before connecting these endpoints to the agent, let's test them directly.

## **Test the API With Curl**

Request the first ten alerts:

```python
curl 
--header "X-SIEM-API-Key: sk_live_demo_9f3a21c4b77d4d3e" 
"http://127.0.0.1:8000/services/siem/alerts?count=10" 
| python3 -m json.tool
```

The `count` parameter controls how many alerts are returned.

To retrieve the next ten alerts, use `offset=10`:

```python
curl 
--header "X-SIEM-API-Key: sk_live_demo_9f3a21c4b77d4d3e" 
"http://127.0.0.1:8000/services/siem/alerts?count=10&offset=10" 
| python3 -m json.tool
```

`offset` controls how many alerts are skipped before the results are returned.

Now retrieve one complete alert:

```python
curl 
--header "X-SIEM-API-Key: sk_live_demo_9f3a21c4b77d4d3e" 
"http://127.0.0.1:8000/services/siem/alerts/ALT-006" 
| python3 -m json.tool
```

These requests confirm that the SIEM API and authentication are working before the AI is involved.

### What Is an Agent Capability?

In this room, an investigation capability is a normal Python function that performs one approved operation.

For example:

`list_alerts()`

Retrieves a small page of SIEM alerts, while:

`get_alert()`

Retrieves one complete alert.

The AI does not execute these Python functions directly.

Instead, it requests a capability using structured JSON:

```python
{
  "type": "tool_call",
  "name": "get_alert",
  "arguments": {
    "alert_id": "ALT-051"
  }
}
```

The application then decides whether the requested capability is allowed.

This creates an important boundary: **The AI requests capabilities. The application controls execution.**

Only explicitly approved functions will be available to the agent.

### List Alerts

Open `agent_task4.py`. The first investigation capability is already implemented:

```plaintext
def list_alerts(count: int = 10, offset: int = 0) -> list:
    """List security alerts in small pages."""
    response = requests.get(
        "http://127.0.0.1:8000/services/siem/alerts",
        headers={"X-SIEM-API-Key": siem_api_key},
        params={"count": count, "offset": offset},
        timeout=5,
    )
    response.raise_for_status()
    data = response.json()

    return [
        {
            "id": alert["id"],
            "name": alert["name"],
            "severity": alert["severity"],
            "status": alert["status"],
        }
        for alert in data["value"]
    ]
```

The function accepts `count` and `offset` so the application can retrieve alerts in small pages. Rather than returning the entire SIEM response, it keeps only the `id`, `name`, `severity`, and `status` fields, making the result easier for both the analyst and the model to process.

Passing unnecessary fields to an AI model consumes context and can obscure the evidence that actually matters, so reducing data before it reaches the model is both an efficiency and an engineering consideration.

Alert summaries are useful for browsing, but investigating a specific alert requires its complete details. Find the first `TODO`:

```plaintext
  def get_alert(alert_id: str) -> dict:
    """Retrieve a security alert by its ID, for example ALT-006."""

    # TODO 1: Retrieve a specific alert by ID
    response = ...

    response.raise_for_status()
    return response.json()
```

Complete the request:

```python
 response = requests.get(
    f"http://127.0.0.1:8000/services/siem/alerts/{alert_id}",
    headers={"X-SIEM-API-Key": siem_api_key},
    timeout=5,
)
```

The completed function becomes:

```plaintext
def get_alert(alert_id: str) -> dict:
    """Retrieve a security alert by its ID, for example ALT-006."""

    response = requests.get(
        f"http://127.0.0.1:8000/services/siem/alerts/{alert_id}",
        headers={"X-SIEM-API-Key": siem_api_key},
        timeout=5,
    )

    response.raise_for_status()
    return response.json()
```

Unlike `list_alerts()`, this function returns the complete selected alert because the investigation requires its full context. At this point, the Python application can retrieve alert evidence; the next step is to control whether the AI is allowed to request these capabilities.

## **Approve the Investigation Capabilities**

The application defines its capability boundary using:

```python
APPROVED_CAPABILITIES = {
    "list_alerts": list_alerts,
    "get_alert": get_alert,
}
```

This dictionary acts as an allowlist: its keys define the capability names the AI is allowed to request, while its values map those names to the Python functions the application is permitted to execute.

For example, when the model requests the `get_alert` capability with an argument such as `alert_id: "ALT-051"`, the application looks up `"get_alert"` in the allowlist, maps it to the approved `get_alert()` Python function, and executes that function using the supplied argument.

If the model requests the `get_alert` capability with `alert_id: "ALT-051"`, the application can find a matching entry in `APPROVED_CAPABILITIES` and execute the approved `get_alert()` function. If it instead requests an unapproved capability such as `disable_account`, no matching entry exists, so the application refuses the request. This allowlist creates a much stronger security boundary than allowing the model to execute arbitrary Python functions.

### Parse the Tool Request

The AI returns either a normal response or a tool request encoded as JSON text. The application must distinguish between these two cases before deciding what to do next. The provided `parse_tool_call()` function performs this check:

```plaintext
def parse_tool_call(message_text: str):
    stripped = message_text.strip()

    if not stripped.startswith("{"):
        return None

    try:
        data, _ = json.JSONDecoder().raw_decode(stripped)
    except (TypeError, ValueError):
        return None

    if not isinstance(data, dict):
        return None

    if data.get("type") == "tool_call":
        return data

    if (
        data.get("name") in APPROVED_CAPABILITIES
        and isinstance(data.get("arguments"), dict)
    ):
        return data

    return None
```

If a valid tool request is found, `parse_tool_call()` returns the parsed dictionary; otherwise, it returns `None`. The investigation loop can then make a simple decision: execute an approved capability when a tool request is present, or return the model response as the final answer when no tool request is detected.

### Execute Only Approved Capabilities

The next helper, `run_tool_call()`, extracts the requested capability name and arguments, then checks whether that name exists in `APPROVED_CAPABILITIES`. If no approved capability is found, the function returns an error and executes nothing. If the capability is approved, the application calls the corresponding Python function with the supplied arguments using `capability(**arguments)`.

For this request, the model asks to use get\_alert with `alert_id="ALT-051"`. The application then verifies that `get_alert` is approved and, only if it is allowed, executes `get_alert(alert_id="ALT-051")`. The distinction is important: the AI requests the capability, but the application controls and executes it.

### Build the Investigation Loop

The application now has all the pieces required for a basic agent loop: it sends the investigation request to the model, checks whether the response contains a tool request, verifies that the requested capability is approved, executes it, and collects the result.

Now connect those pieces together. Find the second `TODO` inside `investigate()`:

```plaintext
for _ in range(MAX_TOOL_CALLS_PER_TURN):

    # TODO 2: Send the current message to the THM AI service
    response = ...

    content = response["message"]["content"]
```

Complete it with:

```python
response = client.send_message(message)
```

The application sends the current message to the AI service and extracts the returned content. It then checks whether that content contains a tool request. If no tool request is found, the model has produced its final answer and the application returns it. If a valid tool request is present, the application executes the approved capability and stores the result.

However, that Python return value is not automatically visible to the model. The application must explicitly send the retrieved evidence back to the AI service so the investigation can continue.

### Return the Tool Result

The tool-routing contract expects evidence in this format:

```python
TOOL_RESULT: <json>
```

Find the third TODO:

```python
# TODO 3: Report the tool's result back to the AI, in the
# "TOOL_RESULT: <json>" format its system prompt expects, so it can
# continue the investigation or give a final answer
message = ...
```

Complete it with:

```python
message = "TOOL_RESULT: " + json.dumps(result)
```

For example, after `get_alert("ALT-051")` executes, the application can send the result back to the model as `TOOL_RESULT: {"id": "ALT-051", ...}`. The model can then use that evidence to either request another approved capability or produce its final response. To prevent the loop from continuing indefinitely, the task limits each turn to `MAX_TOOL_CALLS_PER_TURN = 5`.

### The Agent Loop

The workflow now follows a controlled loop: the user request is sent to the AI model, which may either return a final response or request a capability. If a capability is requested, the application parses the request, checks the allowlist, executes the approved function, and sends the result back to the model. This process repeats until the model produces a final response or the tool-call limit is reached.

This is the first complete agent execution loop in the room. The **AI decides what evidence it wants, while the application decides what it is allowed to access.**

### Run the Agent

Run:

```python
user@machine$ python3 agent_task4.py
```

The program should display:

`Security Investigation Agent ready. Try: Investigate alert ALT-051.`

Enter:

`Investigate alert ALT-051.`

You should now see an investigation step similar to:

```python
Investigation steps:
  - get_alert
```

In Task 3, the raw `tool_call` was the final output because the application did not yet know how to execute it. Now, the application receives the request, checks`APPROVED_CAPABILITIES`, executes`get_alert()`, collects the SIEM response, returns it as`TOOL_RESULT`, and sends that evidence back to the model. The agent can now analyse real SIEM data rather than stopping at the tool request.

### Controlled Agency

The architecture you just built demonstrates an important security principle: Agency should be mediated by explicit application controls.

The model never executes `get_alert()` directly. Instead, it produces structured data describing the capability it wants to use, including the capability name and arguments. The application then checks whether that capability exists in `APPROVED_CAPABILITIES` and executes it only if it is allowed.

This gives the model access to approved investigation capabilities without granting unrestricted access to Python, the SIEM, or the operating system.

**The model proposes. The application enforces.**

### Answer the questions below

Which query parameter controls how many alerts are skipped? `offset`

Which tool retrieves one complete alert by its ID? `get_alert`

## Correlate Alerts With Logs

The Security Investigation Agent can now list alerts, retrieve a complete alert, execute approved capabilities, and return the results to the AI. However, an alert only explains **why a detection was created;** understanding what actually happened requires the agent to pivot into the underlying security logs.

In this task, you will add `search_logs()` to the agent’s approved capabilities. This allows the model to use entities extracted from an alert - such as an account, source IP, device, or event - to retrieve related SIEM evidence.

### Why Search the Logs?

Consider an alert containing:

```python
Account: david.james@northstar.fashion
Source IP: 192.0.2.46
Event: login_success
```

The alert tells us which activity triggered the detection.

However, an investigation may need to answer additional questions:

*   Did the same account generate other authentication events?
    
*   Did the same IP appear before or after the alert?
    
*   Was the activity successful or failed?
    
*   Was another device involved?
    
*   Does the sequence of events support the alert?
    

These details live in the SIEM logs, so the alert provides investigation pivots that the agent can use to search for related evidence.

### Normalised SIEM Fields

The NorthStar Fashion SIEM stores normalised logs, which map information from different event types into consistent fields so the same query structure can be used across multiple sources. Some useful fields include:

| **Field** | **What it identifies** |
| --- | --- |
| `actor.principal_email` | Account that performed the activity |
| `target.principal_email` | Account affected by the activity |
| `network.source_ip` | Source IP address |
| `device.device_name` | Device involved |
| `normalized_event` | Normalised event type |
| `native_event_name` | Original event name |
| `outcome` | Whether the activity succeeded or failed |

Fields such as `actor.principal_email`and `network.source_ip` are nested fields, where the dot notation identifies a value stored inside a larger section of the normalised event.

## **Test the Search API**

The SIEM API documentation is available at [`http://MACHINE_IP:8000/api/docs`](http://MACHINE_IP:8000/api/docs). For this task, we will use:

<table style="min-width: 50px;"><colgroup><col style="min-width: 25px;"><col style="min-width: 25px;"></colgroup><tbody><tr><td colspan="1" rowspan="1"><p>Method and path</p></td><td colspan="1" rowspan="1"><p>Purpose</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>POST /services/siem/search</code></p></td><td colspan="1" rowspan="1"><p>Search normalised SIEM logs</p></td></tr></tbody></table>

Unlike the alert endpoints from the previous task, the search endpoint receives its parameters in a JSON body:

<table style="min-width: 50px;"><colgroup><col style="min-width: 25px;"><col style="min-width: 25px;"></colgroup><tbody><tr><td colspan="1" rowspan="1"><p>Property</p></td><td colspan="1" rowspan="1"><p>Purpose</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>search</code></p></td><td colspan="1" rowspan="1"><p>Field-aware SIEM query</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>count</code></p></td><td colspan="1" rowspan="1"><p>Maximum number of results to return</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>offset</code></p></td><td colspan="1" rowspan="1"><p>Number of matching results to skip</p></td></tr></tbody></table>

Start with a simple query:

```plaintext
curl \
  --request POST \
  --header "X-SIEM-API-Key: sk_live_demo_9f3a21c4b77d4d3e" \
  --header "Content-Type: application/json" \
  --data '{"search":"actor.principal_email=\"david.james@northstar.fashion\"","count":1,"offset":0}' \
  "http://127.0.0.1:8000/services/siem/search" \
  | python3 -m json.tool
```

The query follows the `field="value"` format and requests only one matching result, keeping the response small and easy to inspect.

## **Narrow Searches With AND**

A single account may appear across many logs, so the search can be narrowed by combining multiple fields:

```plaintext
curl \
  --request POST \
  --header "X-SIEM-API-Key: sk_live_demo_9f3a21c4b77d4d3e" \
  --header "Content-Type: application/json" \
  --data '{"search":"actor.principal_email=\"david.james@northstar.fashion\" AND network.source_ip=\"192.0.2.46\"","count":1,"offset":0}' \
  "http://127.0.0.1:8000/services/siem/search" \
  | python3 -m json.tool
```

`AND` narrows the search by requiring all conditions to match within the same log, while `OR` broadens it by allowing either condition to produce a result. For investigations, starting with a focused `AND` query often reduces irrelevant evidence; if no results are found, the model can then broaden the search by removing conditions or using `OR`.

## **Add** `search_logs()`

Open `agent_`[`task5.py`](http://task5.py). The code from Task 4 is already included, and you will now complete the new `search_logs()` capability. The function accepts a SIEM `query`, a `count` that limits the number of logs returned per page, and an `offset` for requesting later pages without retrieving the same records again.

Find the first `TODO`:

```python
# TODO 1: Search the SIEM with the AI's query, count, and offset.
response = 
```

Complete it with:

```python
response = requests.post(
    "http://127.0.0.1:8000/services/siem/search",
    headers={"X-SIEM-API-Key": siem_api_key},
    json={
        "search": query,
        "count": count,
        "offset": offset,
    },
    timeout=5,
)
```

The completed request sends the SIEM search expression, result limit, and pagination offset as JSON. The application then checks the HTTP response with `response.raise_for_status()` before decoding the returned data with `response.json()`.

## **Reduce Unnecessary Log Data**

A single SIEM event can contain a large amount of metadata, but not every field is useful for the investigation. Some fields exist primarily for transporting, storing, or indexing the event and can be removed before the data reaches the model.

The starter code already defines:

```python
FIELDS_TO_SKIP = {
    "schema_version",
    "receive_timestamp",
    "source_format",
    "customer_id",
    "organization_id",
    "log_name",
    "log_type",
    "raw_log",
}
```

For example, `raw_log` may duplicate information already present in the normalised event, so sending it to the model would consume context without adding useful evidence.

The starter code also provides `remove_empty_fields()`, which recursively cleans dictionaries and lists. It skips fields listed in `FIELDS_TO_SKIP`, removes empty values such as `None`, empty strings, empty lists, and empty dictionaries, and preserves useful populated fields from nested structures.

Importantly, this function does **not** enforce a fixed authentication-only schema. Relevant fields remain available whether the event relates to authentication, devices, OAuth, group changes, administration, or another supported event type.

## **Return Evidence and Pagination**

After cleaning the logs, `search_logs()` returns:

```python
return {
    "total": data["totalResultCount"],
    "count": len(results),
    "offset": data["offset"],
    "truncated": data["truncated"],
    "results": results,
}
```

This gives the agent two types of information: investigation evidence in `results`, which contains the cleaned matching logs, and search state in `total`, `count`, `offset`, and `truncated`, which describes the current page. When `truncated` is `True`, additional results are available, and the agent can request the next page by increasing `offset` - for example, from `count=10`, `offset=0` to `count=10`, `offset=10`. This allows the investigation to gather evidence progressively instead of retrieving every matching event at once.

### Keep Tool Results Within the AI Message Limit

Log searches introduce a challenge that is less significant when retrieving a single alert: tool results can become too large for the model context.

The starter code therefore includes`build_tool_result_message()`, which serialises the tool result before sending it back to the THM AI service. In this workflow, messages are limited to `4096` characters, so the function checks whether the normal `TOOL_RESULT: message` fits within that limit.

If it does, the complete evidence is returned. If a log search is too large, the function progressively removes entries from `results` and marks the response with `"truncated_for_message_limit": true`. If the evidence still cannot fit, the model receives a short error requesting a more focused search.

This introduces an important agent-building principle: **A useful tool result must fit within the context available to the model.**

Retrieving more evidence is not always better. Focused evidence is usually easier for the model to process than a large dump of unrelated logs.

### Approve the Search Capability

The Python function now exists, but the Security Investigation Agent still cannot use it. Remember the capability boundary introduced in Task 4: **implementing a function does not automatically make it available to the model.**

```python
APPROVED_CAPABILITIES = {
    "list_alerts": list_alerts,
    "get_alert": get_alert,
}
```

Find the second `TODO`:

```plaintext
# TODO 2: Approve search_logs by adding it here
```

Add:

```plaintext
"search_logs": search_logs,
```

The completed allowlist becomes:

```python
APPROVED_CAPABILITIES = {
    "list_alerts": list_alerts,
    "get_alert": get_alert,
    "search_logs": search_logs,
}
```

The model may now request `search_logs` with a query, result count, and offset. The existing execution layer handles the `rest: parse_tool_call()` identifies the request, `APPROVED_CAPABILITIES` confirms that the capability is allowed, run\_tool\_call() executes `search_logs(...)`, and `build_tool_result_message()` returns the evidence to the model as `TOOL_RESULT`.

No changes to the execution loop are required. You are extending the agent simply by registering another approved capability.

### From Alert to Investigation Pivot

The workflow is now more powerful. After retrieving an alert with get\_alert, the model can extract useful investigation pivots such as the account, source IP, device, or event, then use those values to build a focused search\_logs request. For example, it may combine `actor.principal_email="david.james@northstar.fashion"` with `network.source_ip="192.0.2.46"` to retrieve related SIEM activity.

The investigation now progresses from the alert itself to extracting relevant pivots, searching related logs, and correlating the resulting evidence. This is much closer to how an analyst investigates a real detection.

**Run the Agent Run:**

```python
user@machine$ python3 agent_task5.py 
```

The program should display:

`Security Investigation Agent ready. Try: Investigate alert ALT-049.`

Enter:

`Investigate alert ALT-049.`

The investigation steps should now include both alert retrieval and log searching, for example:

```python
Investigation steps:
- get_alert
- search_logs
```

The exact query may vary, but it should use relevant entities extracted from the alert. The final response should therefore combine alert evidence with related SIEM activity, rather than relying on the alert alone.

### Test the Search Capability

You can also test `search_logs` directly with:

**Search for logs involving david.james@northstar.fashion and source IP 192.0.2.46.**

The model should translate this into a structured `search_logs` request. To test pagination, try:

`Show the first 20 logs for david.james@northstar.fashion in pages of 10.`

The agent can request the first page with `count=10`, `offset=0` and, if more results are needed, continue with `count=10`, `offset=10`.

### Correlation, Not Collection

Adding `search_logs` introduces an important principle: a useful investigation agent should retrieve evidence that helps answer a specific question rather than collect every available log. For example, a broad query such as `actor.principal_email="david.james@northstar.fashion"` may return many unrelated events, while adding `network.source_ip="192.0.2.46"` narrows the search to activity more directly related to the alert. Focused searches therefore improve investigation relevance, context efficiency, response quality, and explainability.

**Agent capabilities should retrieve the evidence needed for the decision, not every piece of data available.**

### Answer the questions below

Which logical operator requires both search conditions to match the same log? `AND`

Which HTTP method does the SIEM search endpoint use? `POST`

## Add External and Organisation Context

The Security Investigation Agent can now retrieve alerts and correlate them with related SIEM logs, giving it a strong view of what happened inside NorthStar Fashion. However, internal telemetry alone may not provide enough context to interpret an event correctly.

An analyst may also need to determine whether a source IP has been associated with abusive activity elsewhere, whether suspicious behaviour matches an approved internal exercise, or whether a network, device, or activity is expected within the organisation.

In this task, you will add two new investigation capabilities:

*   `check_ip_abuse()`
    
*   `search_org_details()`
    

These capabilities extend the investigation beyond raw SIEM telemetry by adding external reputation and internal organisation context.

### Why Add More Context?

Two alerts can show similar authentication patterns - multiple failed logins, a successful authentication, an external source IP, and an unusual device - yet represent very different situations. SIEM telemetry may show **what happened**, but additional context can help explain **what it means**.

For example, external reputation indicating that the source IP has recent credential-attack reports may strengthen a malicious interpretation. In contrast, organisation context showing that the same account, IP, device, and activity were part of an approved security test may explain why the behaviour was expected.

Neither source should be trusted in isolation. The Security Investigation Agent should correlate **alert evidence, SIEM logs, external reputation, and organisation context before producing a supported verdict**.

### Check External IP Reputation

The lab provides a local IP reputation service for checking whether an IP address has been reported for abusive activity:

*   Method and path Purpose `GET /api/v2/check` Check one IP address for reported abusive activity
    

Unlike the `SIEM API`, this service authenticates requests using the Key header instead of `X-SIEM-API-Key`.

Test an IP address with known reports:

```python
 curl \
  --request GET \
  --header "Key: demo-key" \
  --header "Accept: application/json" \
  "http://127.0.0.1:8000/api/v2/check?ipAddress=203.0.113.47&maxAgeInDays=90&verbose=" \
  | python3 -m json.tool
```

The request accepts three parameters:

### Parameter Purpose

*   `ipAddress` IP address to check
    
*   `maxAgeInDays` Maximum age of returned reports
    
*   `verbose` Include individual report details
    

The response may include fields such as `abuseConfidenceScore`, `totalReports`, `numDistinctUsers`, `lastReportedAt`, and `reports`.

Now test an IP address with no reported abuse:

```python
curl 
--request GET 
--header "Key: demo-key" 
--header "Accept: application/json" 
"http://127.0.0.1:8000/api/v2/check?ipAddress=192.0.2.46&maxAgeInDays=90&verbose=" 
| python3 -m json.tool
```

A result such as `abuseConfidenceScore: 0` and `totalReports: 0` does not prove that the IP or its activity is safe. It only means that this reputation service found no abuse reports for that address within the requested time period.

### Build check\_ip\_abuse()

Open `agent_task6.py`. The code from Task 5 is already included.

Find the first `TODO`:

```plaintext
def check_ip_abuse(ip_address: str) -> dict:
    """Check an IP address for previously reported abusive activity."""

    # TODO 1: Check the IP address for abuse history
    response = ...
```

Complete the request with:

```python
response = requests.get(
    "http://127.0.0.1:8000/api/v2/check",
    headers={
        "Key": abuseipdb_api_key,
        "Accept": "application/json",
    },
    params={
        "ipAddress": ip_address,
        "maxAgeInDays": 90,
        "verbose": "",
    },
    timeout=5,
)
```

The function then checks the HTTP response with `response.raise_for_status()` and extracts the nested data from `response.json()["data"]`. Rather than returning the entire API response, the application keeps only the fields that provide useful evidence for the investigation.

### Keep the Reputation Result Compact

A verbose reputation response may include many reports and metadata fields, so the starter code keeps only the first three report examples and the fields most useful for investigation. This preserves the IP address, abuse confidence score, total number of reports, number of distinct reporters, most recent report time, and a small sample of recent report comments without sending the entire raw response to the model.

This follows the same principle introduced in Task 5: **Send enough evidence to support reasoning, but avoid unnecessary context.**

### Add Organisation Context

External reputation can show what other sources have observed, but it cannot answer organisation-specific questions such as whether an IP belongs to NorthStar Fashion, whether a device is expected, or whether suspicious activity was part of an approved security test.

For that, the project includes a small collection of internal Markdown documents in `org_details/`. Because the knowledge base is small, this room uses deterministic keyword matching instead of embeddings or a vector database, making retrieval easier to inspect, control, and debug.

The starter code loads each Markdown file while preserving both its `source` and `text`, so any retrieved context still indicates where the information came from.

The provided `search_org_details(query: str)` capability extracts meaningful words from the query, ignores common stopwords, and compares those terms with each organisation document. The document with the highest overlap is selected when it reaches`MIN_MATCH_SCORE = 2`; otherwise, the function returns No matching organisation context found.

This approach is deliberately simple and deterministic, but the core agent-building principle remains the same: The agent retrieves relevant organisation context only when it needs it.

### Approve the New Capabilities

The new Python functions are now implemented, but the application still cannot execute them until they are added to the approved capability allowlist.

Find the second `TODO` and add to the dict:

```python
"check_ip_abuse": check_ip_abuse,
"search_org_details": search_org_details,
```

The Security Investigation Agent now has five approved capabilities:

*   `list_alerts`
    
*   `get_alert`
    
*   `search_logs`
    
*   `check_ip_abuse`
    
*   `search_org_details`
    

Because these capabilities use the same allowlist and execution layer introduced earlier, the core orchestration logic does not need to change. The model requests a capability, the application verifies that it is approved, executes the corresponding Python function, and returns the result to the model.

### Investigate with External Context

Run:

```python
user@machine$ python3 agent_task6.py 
```

Then try:

`Investigate alert ALT-007.`

Depending on the evidence requested by the model, the investigation may use `get_alert`, `search_logs`, and `check_ip_abuse`. The reputation result can provide supporting context about whether the source IP has recently been associated with abusive activity, but it should not determine the verdict on its own.

A high abuse score combined with related suspicious authentication activity is stronger evidence than a high abuse score by itself.

### Investigate with Organisation Context

Now try:

`Investigate alert ALT-049.`

The model may also request `search_org_details` when organisation context could help explain the activity. For example, an internal approval may indicate that a suspicious-looking account, IP, device, or action was part of an authorised security test.

The agent should still correlate that context with the alert and SIEM logs rather than trust it automatically. A supported authorisation should match the current investigation closely enough to explain the observed activity.

### Evidence Can Change Interpretation

This demonstrates an important difference between **detection** and **investigation**. The telemetry may show dozens of failed authentication attempts followed by a successful login, correctly triggering a suspicious authentication alert. Those events really occurred, but additional organisation context may reveal that the same account, source IP, device, and activity were part of an approved validation exercise.

The alert was still useful because it correctly detected the configured condition. What changes is the interpretation of that activity.

This is why the agent supports four verdicts:

*   `TruePositive`
    
*   `BenignPositive`
    
*   `FalsePositive`
    
*   `InsufficientEvidence`
    

A **BenignPositive**, for example, means the detected activity occurred, but the available evidence shows that it was legitimate, expected, or authorised.

### The Investigation So Far

The agent can now combine several evidence sources, each answering a different question:

| **Evidence source** | **Question** |
| --- | --- |
| Alert | Why was the detection created? |
| SIEM logs | What activity actually occurred? |
| IP reputation | Has the source IP been reported elsewhere? |
| Organisation context | Is there internal context that changes the interpretation? |

The value comes from correlating these sources rather than treating any single one as definitive.

### Answer the questions below

Which capability checks an IP address for previously reported abusive activity? `check_ip_abuse`

Which capability searches NorthStar Fashion's internal organisation documents? `search_org_details`

## Remember the Investigation

The Security Investigation Agent can now retrieve alerts, correlate SIEM logs, check external IP reputation, search organisation context, and produce a supported verdict. However, real investigations rarely end after a single answer.

An analyst may immediately ask:

What evidence led you to that verdict? Which source IP was involved? Was there any approved activity related to this alert? If the previous investigation remains available in the conversation, the agent should be able to answer these follow-up questions without requiring the analyst to repeat the alert ID or retrieve the same evidence again.

In this task, you will examine how the AI service preserves conversation history and use that context to continue an investigation across follow-up questions.

What Is Conversation Memory? Conversation memory keeps information from earlier turns available to later ones. For the Security Investigation Agent, this may include analyst requests, tool calls, tool results, investigation evidence, and previous assistant responses.

This differs from the organisation context introduced in the previous task. Organisation context provides reusable internal knowledge, such as whether an activity was approved, a device is expected, or an IP belongs to the organisation. Conversation memory instead preserves what has already happened during the current investigation.

Context type Purpose Organisation context Provides reusable internal knowledge Conversation history Preserves what happened during the current investigation Memory Is Already Provided by the TryHackMe AI Service Unlike the previous implementation of this room, the current architecture does not require a LangGraph checkpointer or separate thread ID for short-term memory. THMAgentClient communicates with a TryHackMe AI service that already preserves conversation history on the server side.

As a result, consecutive calls to client.send\_message(...) remain part of the same conversation, allowing follow-up questions to use earlier requests, tool results, and assistant responses without manually rebuilding the message history.

The application can inspect this stored conversation using client.get\_messages(), which is exposed by the thm\_ai.py helper for retrieving the history maintained by the platform.

### Follow-Up Questions Need No New Capability

The `investigate(user_message: str)` function already supports both new investigations and follow-up questions. A new investigation may require capabilities such as `get_alert`, `search_logs`, check\_ip\_abuse, or search\_org\_details, while a follow-up such as What evidence led you to that verdict? may be answered directly from the existing conversation history.

Because the function simply sends the current `user_message` through`client.send_message(...)`, no special orchestration logic is needed to identify follow-ups. If the required evidence is already present in the conversation, the model can respond immediately without requesting another tool.

This keeps the workflow simple: new investigations may require tools, while follow-up questions may reuse existing conversation history.

### Inspect the Shared History

Open `agent_task7.py`. The completed investigation workflow from Task 6 is already included.

The program also supports a special `history` command. When the analyst enters`history`, the application should retrieve and display the conversation history maintained by the TryHackMe AI service.

Find the `TODO` and complete it with:

```python
history = client.get_messages()
```

The surrounding code is already provided:

```python
if human_msg.strip().lower() == "history":
    try:
        history = client.get_messages()
    except THMAgentError as error:
        print(f"AI request failed: {error}")
        continue

    for entry in history.get("messages", []):
        print(f"[{entry['role']}] {entry['content']}")

    continue
```

`get_messages()` returns the conversation history maintained by the TryHackMe AI service. Each entry includes fields such as `role` and `content`, allowing the application to display the conversation in a readable format, for example:

```python
[user] Investigate alert ALT-049.
[assistant] ...
[user] TOOL_RESULT: ...
[assistant] ...
```

The exact history depends on the investigation and which capabilities the model requested.

## **Why Inspect the History?**

The `history` command is not required for the agent to remember the investigation. Memory already works through the shared conversation used by `client.send_message()`.

Instead, `history` makes that conversation visible. It demonstrates that the application does not maintain its own Python list of previous messages or require an additional memory framework; the TryHackMe AI service maintains the conversation.

This distinction is important:

> **Using memory and inspecting memory are separate operations.**

`send_message()` continues the existing conversation, while `get_messages()` allows the application to inspect it.

## **Run the Agent**

Run:

```python
 user@machine$ python3 agent_task7.py
    
```

The program should display:

`Security Investigation Agent ready. Try: Investigate alert ALT-049.`

You can also type `history` to see the shared conversation TryHackMe AI is remembering.

Begin with:

`Investigate alert ALT-049.`

The agent may request several capabilities before producing a verdict, such as `get_alert`, `search_logs`, `check_ip_abuse,` and `search_org_details`. The exact sequence may vary depending on the evidence the model determines is useful.

Keep the program running and then ask:

`What evidence led you to that verdict?`

The agent should be able to explain its earlier verdict using the alert, related logs, IP reputation result, and organisation context already available in the conversation. You do not need to repeat `ALT-049` or resend the evidence manually because the previous investigation provides the context needed to understand what that verdict refers to.

### Inspect What the Agent Remembers

Now enter:

`history`

The application calls `client.get_messages()` and displays the conversation maintained by the AI service.

The conversation history captures the full investigation flow: the analyst submits an investigation request, the AI requests approved capabilities as needed, and each result is returned as a `TOOL_RESULT`. The model uses that evidence to continue the investigation until it produces a final response. If the analyst then asks a follow-up question, the agent can use the existing conversation history to answer in the context of the same investigation.

The tool results are especially important because the conversation contains not only the final verdict, but also the evidence gathered during the investigation. This allows later questions to refer back to what the agent previously observed.

### Memory Does Not Mean Re-Running the Investigation

Suppose the first request was:

`Investigate alert ALT-049.`

The agent may already have retrieved the alert details, related logs, IP reputation, and relevant organisation context. If the analyst then asks:

`Which organisation evidence supported the verdict?`

the agent may be able to answer directly from conversation history without calling `search_org_details()` again.

This can make follow-up interactions more efficient, but memory should not prevent the agent from retrieving fresh evidence when new or updated information is required.

### Conversation Memory Has Boundaries

Conversation history improves continuity, but it must be scoped carefully. Consider what happens if the same conversation is used for several unrelated alerts:

```python
Investigate ALT-007.
Investigate ALT-049.
What source IP was suspicious?
```

The final question is ambiguous because evidence from multiple investigations is now present in the same context.

In a production system, conversation state should therefore be scoped to an appropriate boundary, such as an analyst session, case, incident, or investigation. Separate investigations may require separate conversation contexts to prevent evidence from crossing case boundaries.

This lab uses the conversation maintained by the TryHackMe AI service so you can focus on how conversational state affects agent behaviour.

**Memory improves continuity, but useful memory still needs clear boundaries.**

### Do Not Clear the Shared History

During Normal Use `THMAgentClient` also provides `client.clear_messages()`, but you do not need it in this task.

The Python client and browser AI panel share the same conversation history, so calling `client.clear_messages()` would remove that shared state. A reset should therefore only be performed when an exercise or application explicitly requires a fresh conversation.

**Exception: recovering from a stuck conversation.** Conversation history is tied to your account for this room, not to the current VM. If you notice the agent repeatedly returning a malformed or unreadable response instead of investigating (for example, raw JSON with no recognisable tool request), the model has likely locked onto a bad pattern from earlier in the conversation. In that case, `client.clear_messages()` is the appropriate fix, even though it also clears the browser AI panel's history - a working agent is worth losing that shared history for.

### The Complete Security Investigation Agent

The Security Investigation Agent can now combine AI reasoning with approved capabilities, SIEM alerts, log correlation, external IP reputation, organisation context, and conversation history.

Across the room, you progressively added these capabilities:

```python
Task 3 → Define the agent behaviour and connect to the AI
Task 4 → Execute approved alert capabilities
Task 5 → Correlate alerts with SIEM logs
Task 6 → Add external and organisation context
Task 7 → Continue investigations across follow-up questions
```

The completed workflow allows the model to request evidence, while the application controls which capabilities may execute. Retrieved evidence is returned to the model, correlated into a supported verdict, and preserved in the shared conversation so later questions can continue from the same investigation.

A follow-up such as:

`What evidence led you to that verdict?`

can therefore be answered using the alert, related logs, IP reputation result, and organisation context already available in the conversation. This works because each send\_message() call continues the server-side conversation maintained by the TryHackMe AI service; no separate checkpointer or thread ID is required.

**The Security Investigation Agent is now complete.**

Across the room, you progressed from an AI model with no access to current security evidence to an agent that can retrieve alerts, correlate SIEM logs, check IP abuse history, consult organisation knowledge, enforce an explicit capability allowlist, and preserve investigation context across follow-up questions.

The completed implementation is available in `agent_complete.py.` It contains the same components added step by step throughout the room, including the five approved investigation capabilities - l`ist_alerts`, `get_alert`, `search_logs`, `check_ip_abuse`, and `search_org_details` - along with the tool-request execution loop, compact evidence handling, TryHackMe AI client integration, and conversation-aware follow-up behaviour.

You can use `agent_complete.py` as the final reference implementation of the NorthStar Fashion Security Investigation Agent.

### Answer the questions below

Which method retrieves the conversation history stored by the TryHackMe AI service? `get_messages`

Does the agent need a separate LangGraph checkpointer to remember this conversation? (Yea/Nay) `Nay`

Which file contains the completed Security Investigation Agent built throughout the room? `agent_complete.py`

## Conclusion

Throughout this room, you moved from manually investigating an alert to building the NorthStar Fashion Security Investigation Agent in Python with LangChain. You configured a chat model, placed it inside an agent runtime, connected tools for alerts, logs, and IP abuse history, grounded investigations in organisation knowledge with RAG, and added short-term memory for follow-up questions. The result is a controlled Security Investigation Agent that can gather and correlate approved evidence, produce supported verdicts and recommendations, and preserve the investigation within a conversation while the engineer retains responsibility for the final decision and all containment actions.

![The agent is now assembled](https://cdn-images.tryhackme.com/user-uploads/5f5ed9259575d24307292950/room-content/5f5ed9259575d24307292950-1787344245903.png align="center")

## **Key Takeaways**

*   Build agent capabilities incrementally; testing each function before exposing it as a tool makes API requests, returned data, and failures easier to understand before the model begins deciding when to use them.
    
*   Treat every source as evidence with limits; alerts explain detections, logs show related activity, IP reputation adds external context, and organisation documents may establish authorisation, but no single source should be treated as proof without corroboration.
    
*   Keep investigation support and security action separate; the Security Investigation Agent may gather evidence and recommend a verdict, but the engineer remains responsible for the final decision, alert handling, and containment actions
