rag-monitoring / notebook.ipynb
notebook.ipynb
Raw
%load_ext autoreload
%autoreload 2

query = "How does the agentic loop keep calling the model until it stops?"
from starter import rag_traced


answer = rag_traced.rag(query)
print(answer)
{
    "name": "search",
    "context": {
        "trace_id": "0x4354400e5794c32eb61dd9644dfc28e2",
        "span_id": "0xb7362fd7ef5dfea9",
        "trace_state": "[]"
    },
    "kind": "SpanKind.INTERNAL",
    "parent_id": "0xbbfa223ece42605a",
    "start_time": "2026-07-14T18:00:30.319890Z",
    "end_time": "2026-07-14T18:00:30.321468Z",
    "status": {
        "status_code": "UNSET"
    },
    "attributes": {},
    "events": [],
    "links": [],
    "resource": {
        "attributes": {
            "telemetry.sdk.language": "python",
            "telemetry.sdk.name": "opentelemetry",
            "telemetry.sdk.version": "1.43.0",
            "service.instance.id": "4029fd78-9ca0-4236-ad8e-d811854a361b",
            "service.name": "unknown_service"
        },
        "schema_url": ""
    }
}
{
    "name": "llm",
    "context": {
        "trace_id": "0x4354400e5794c32eb61dd9644dfc28e2",
        "span_id": "0xf4cb5c745265ef16",
        "trace_state": "[]"
    },
    "kind": "SpanKind.INTERNAL",
    "parent_id": "0xbbfa223ece42605a",
    "start_time": "2026-07-14T18:00:30.322370Z",
    "end_time": "2026-07-14T18:00:35.079096Z",
    "status": {
        "status_code": "UNSET"
    },
    "attributes": {},
    "events": [],
    "links": [],
    "resource": {
        "attributes": {
            "telemetry.sdk.language": "python",
            "telemetry.sdk.name": "opentelemetry",
            "telemetry.sdk.version": "1.43.0",
            "service.instance.id": "4029fd78-9ca0-4236-ad8e-d811854a361b",
            "service.name": "unknown_service"
        },
        "schema_url": ""
    }
}
{
    "name": "rag",
    "context": {
        "trace_id": "0x4354400e5794c32eb61dd9644dfc28e2",
        "span_id": "0xbbfa223ece42605a",
        "trace_state": "[]"
    },
    "kind": "SpanKind.INTERNAL",
    "parent_id": null,
    "start_time": "2026-07-14T18:00:30.319836Z",
    "end_time": "2026-07-14T18:00:35.079958Z",
    "status": {
        "status_code": "UNSET"
    },
    "attributes": {},
    "events": [],
    "links": [],
    "resource": {
        "attributes": {
            "telemetry.sdk.language": "python",
            "telemetry.sdk.name": "opentelemetry",
            "telemetry.sdk.version": "1.43.0",
            "service.instance.id": "4029fd78-9ca0-4236-ad8e-d811854a361b",
            "service.name": "unknown_service"
        },
        "schema_url": ""
    }
}
The agentic loop keeps calling the model until it stops by using a **`while True` loop** that repeatedly checks for function calls. Here’s how it works based on the provided context:

1. **Loop Condition**:
   - The loop runs indefinitely (`while True`) until the model stops generating function calls.
   - The loop breaks when `has_function_calls` is `False`, meaning the model has provided a final answer without further tool calls.

2. **Process Flow**:
   - The model generates a response, which may include function calls (e.g., `search`).
   - The code checks each item in the response:
     - If it’s a function call (e.g., `function_call`), it executes the function, appends the result to the conversation, and marks `has_function_calls` as `True`.
     - If it’s a message (e.g., the model’s final answer), it prints the answer and checks if `has_function_calls` is `False` to exit the loop.

3. **Termination**:
   - The loop exits when the model’s response contains no function calls (`has_function_calls == False`), indicating the agent has completed its task.

### Example Breakdown:
```python
while True:
    response = openai_client.responses.create(...)
    messages.extend(response.output)  # Add model's output to history

    for item in response.output:
        if item.type == "function_call":
            # Execute the function, append result, and continue looping
            ...
            has_function_calls = True
        elif item.type == "message":
            print("Final answer:", item.content[0].text)
            if not has_function_calls:
                break  # Exit loop
```

### Key Points:
- The loop **continues** as long as the model requests further tool calls.
- The model decides when to stop (e.g., after a final answer or when it’s satisfied).
- The loop relies on the `has_function_calls` flag to track whether the model needs more iterations.

This design ensures the agent dynamically adapts to the task, making multiple calls until it achieves its goal.
answer = rag_traced.rag(query)
print(answer)
{
    "name": "search",
    "context": {
        "trace_id": "0x6ce8d1a8c28847f58ddadf1f5d944397",
        "span_id": "0x5841abb3d5566bd8",
        "trace_state": "[]"
    },
    "kind": "SpanKind.INTERNAL",
    "parent_id": "0xd4236941298ca9a3",
    "start_time": "2026-07-14T18:15:12.381087Z",
    "end_time": "2026-07-14T18:15:12.383154Z",
    "status": {
        "status_code": "UNSET"
    },
    "attributes": {},
    "events": [],
    "links": [],
    "resource": {
        "attributes": {
            "telemetry.sdk.language": "python",
            "telemetry.sdk.name": "opentelemetry",
            "telemetry.sdk.version": "1.43.0",
            "service.instance.id": "4029fd78-9ca0-4236-ad8e-d811854a361b",
            "service.name": "unknown_service"
        },
        "schema_url": ""
    }
}
{
    "name": "llm",
    "context": {
        "trace_id": "0x6ce8d1a8c28847f58ddadf1f5d944397",
        "span_id": "0x007d6bd2145b8b1a",
        "trace_state": "[]"
    },
    "kind": "SpanKind.INTERNAL",
    "parent_id": "0xd4236941298ca9a3",
    "start_time": "2026-07-14T18:15:12.384449Z",
    "end_time": "2026-07-14T18:15:17.670019Z",
    "status": {
        "status_code": "UNSET"
    },
    "attributes": {
        "input_tokens": 7336,
        "output_tokens": 650
    },
    "events": [],
    "links": [],
    "resource": {
        "attributes": {
            "telemetry.sdk.language": "python",
            "telemetry.sdk.name": "opentelemetry",
            "telemetry.sdk.version": "1.43.0",
            "service.instance.id": "4029fd78-9ca0-4236-ad8e-d811854a361b",
            "service.name": "unknown_service"
        },
        "schema_url": ""
    }
}
{
    "name": "rag",
    "context": {
        "trace_id": "0x6ce8d1a8c28847f58ddadf1f5d944397",
        "span_id": "0xd4236941298ca9a3",
        "trace_state": "[]"
    },
    "kind": "SpanKind.INTERNAL",
    "parent_id": null,
    "start_time": "2026-07-14T18:15:12.381042Z",
    "end_time": "2026-07-14T18:15:17.674248Z",
    "status": {
        "status_code": "UNSET"
    },
    "attributes": {},
    "events": [],
    "links": [],
    "resource": {
        "attributes": {
            "telemetry.sdk.language": "python",
            "telemetry.sdk.name": "opentelemetry",
            "telemetry.sdk.version": "1.43.0",
            "service.instance.id": "4029fd78-9ca0-4236-ad8e-d811854a361b",
            "service.name": "unknown_service"
        },
        "schema_url": ""
    }
}
The agentic loop keeps calling the model until it stops by using a **`while True` loop** with a condition to check whether the model has requested further tool calls. Here’s how it works based on the provided context:

### Key Mechanism:
1. **Loop Continuation**:
   - The loop runs indefinitely (`while True`) until a stopping condition is met.
   - Inside the loop, the model’s response is processed to check if it contains a `function_call` (indicating the model wants to use a tool).

2. **Condition to Stop**:
   - The loop breaks when the model’s response contains **no function calls** (`has_function_calls == False`).
   - This means the model has decided it has enough information to answer without invoking additional tools.

3. **Iteration Tracking**:
   - An iteration counter (`it`) is incremented to track how many round-trips (model → tool → model) have occurred.
   - This helps debug or limit the loop (e.g., to prevent infinite loops).

---

### How It Works in Code:
```python
while True:
    print(f"iteration #{it}...")
    has_function_calls = False  # Reset flag for this iteration

    # Step 1: Send the current conversation history to the model
    response = openai_client.responses.create(
        model="gpt-5.4-mini",
        input=messages,  # Current conversation history
        tools=[search_tool],
    )

    # Step 2: Process the model's response
    messages.extend(response.output)  # Append model's output to history

    for item in response.output:
        if item.type == "function_call":
            print("function_call:", item.name, item.arguments)
            call_output = make_call(item)  # Execute the tool and get results
            messages.append(call_output)  # Add tool output to history
            has_function_calls = True  # Flag that the model wants more

        elif item.type == "message":
            print("ASSISTANT:", item.content[0].text)

    it += 1  # Increment iteration counter

    # Step 3: Stop if no function calls were requested
    if not has_function_calls:
        break
```

---

### Why It Stops:
- The loop **only exits** when the model’s final response contains no `function_call` entries. This signals the model has answered the question or reached a conclusion without needing further tool calls.
- The instructions (e.g., `instructions` variable) guide the model to explicitly request tools (e.g., `search`) when needed, but it remains flexible to decide when to stop.

---

### Example:
If the model asks for a tool call (e.g., `search`), the loop:
1. Executes the tool (e.g., searches for "Ollama").
2. Appends the tool’s result to the conversation history.
3. Sends the updated history back to the model.
4. Repeats until the model’s next response has no function calls.

This continues until the model provides a final answer without further tool requests.
from datetime import datetime

start = datetime.fromisoformat("2026-07-14T18:15:12.384449Z")
end = datetime.fromisoformat("2026-07-14T18:15:17.670019Z")

duration = (end - start).total_seconds()

print(duration)
print(duration * 1000)
5.28557
5285.57
from starter import rag_traced

answer = rag_traced.rag(query)
print(answer)
The agentic loop keeps calling the model until it stops by using a **`while True` loop** with a condition to check whether the model has requested further tool calls. Here’s how it works based on the provided context:

### Key Mechanism:
1. **Loop Continuation**:
   - The loop (`while True`) runs indefinitely until a specific condition is met.
   - Inside the loop, the model’s response is processed to check if it contains any `function_call` entries.

2. **Condition to Stop**:
   - The loop breaks when `has_function_calls` is `False`. This flag is set to `True` each time the model requests a tool call (e.g., `search`) and `False` when the model provides a final answer without further tool calls.

3. **Iteration Tracking**:
   - The iteration counter (`it`) is incremented each time through the loop to show progress (e.g., `iteration #1`, `iteration #2`).

### Example Breakdown:
```python
while True:
    print(f"iteration #{it}...")
    has_function_calls = False  # Reset flag for this iteration

    response = openai_client.responses.create(...)

    for item in response.output:
        if item.type == "function_call":
            print("function_call:", item.name, item.arguments)
            call_output = make_call(item)
            messages.append(call_output)
            has_function_calls = True  # Model wants more tool calls

        elif item.type == "message":
            print("ASSISTANT:", item.content[0].text)

    it += 1
    if not has_function_calls:  # Exit condition: no more tool calls
        break
```

### Why It Stops:
- The loop stops when the model’s response contains no `function_call` entries. This means the model has decided to provide a final answer without needing additional tool calls.
- The instructions (e.g., `instructions` variable) guide the model to make multiple searches and only stop when it has sufficient information.

### Summary:
The loop persists until the model explicitly signals it’s done with tool calls, leveraging the `has_function_calls` flag to enforce this condition. This dynamic approach allows the agent to adapt to the model’s decisions, such as retrying searches for typos or expanding queries.
import sqlite3
import pandas as pd

conn = sqlite3.connect("traces.db")

df = pd.read_sql(
    "SELECT * FROM spans",
    conn
)

print(df)
     name           start_time             end_time  input_tokens  \
0  search  1784056376194982017  1784056376197035791           NaN   
1     llm  1784056376204200104  1784056379820000468        7336.0   
2     rag  1784056376194915754  1784056379825301546           NaN   

   output_tokens  cost  
0            NaN  None  
1          484.0  None  
2            NaN  None  
df["duration_ms"] = (
    df["end_time"] -
    df["start_time"]
) / 1e6
(
    df[df.name != "rag"]
    .groupby("name")["duration_ms"]
    .sum()
)
name
llm       3615.800364
search       2.053774
Name: duration_ms, dtype: float64

answer = rag_traced.rag(query)
print(answer)
The agentic loop keeps calling the model until it stops because of the **`while True` loop structure** combined with the **`has_function_calls` flag**.

### How It Works:
1. **Infinite Loop (`while True`)**:
   The loop runs indefinitely until explicitly broken out of.
   ```python
   while True:
       # Model call and processing
       if has_function_calls == False:
           break  # Exit condition
   ```

2. **`has_function_calls` Flag**:
   After each model response, the code checks if the model requested any function calls (e.g., `search`).
   - If **no function calls** are present, the loop breaks, and the final answer is returned.
   - If **function calls** exist, the loop continues, processing the tool results and re-sending them to the model.

3. **Iteration Logic**:
   - The model decides whether to call tools (e.g., `search`) based on its reasoning.
   - The loop keeps iterating until the model provides a final answer without further tool requests.

### Why It Stops:
The loop stops when the model’s response contains **no function calls** (`has_function_calls == False`), indicating it has completed its task and provided a final answer.

### Example Flow:
- **User Query**: *"How do I run Olama locally?"*
- **Model’s First Attempt**: Searches for "Olama" → Returns no results → Asks for a tool call.
- **Tool Call**: Executes `search("Olama")` → Returns empty results.
- **Model’s Second Attempt**: Searches for "Ollama" → Returns useful results → Provides an answer.
- **Final Response**: No further function calls → Loop breaks, and the answer is returned.

This design ensures the agent dynamically adapts to incomplete or incorrect initial queries by repeatedly querying tools until it finds a satisfactory answer.
import sqlite3
import pandas as pd

conn = sqlite3.connect("traces.db")

df = pd.read_sql(
    "SELECT * FROM spans",
    conn
)

print(df)
      name           start_time             end_time  input_tokens  \
0   search  1784056376194982017  1784056376197035791           NaN   
1      llm  1784056376204200104  1784056379820000468        7336.0   
2      rag  1784056376194915754  1784056379825301546           NaN   
3   search  1784056605627375896  1784056605629557648           NaN   
4      llm  1784056605637611721  1784056608796712115        7336.0   
5      rag  1784056605627301767  1784056608800542395           NaN   
6   search  1784056611544953865  1784056611546858029           NaN   
7      llm  1784056611553761475  1784056615061661678        7336.0   
8      rag  1784056611544911917  1784056615065781962           NaN   
9   search  1784056616258031330  1784056616259944932           NaN   
10     llm  1784056616265641879  1784056618620603018        7336.0   
11     rag  1784056616257988741  1784056618629422079           NaN   

    output_tokens  cost  
0             NaN  None  
1           484.0  None  
2             NaN  None  
3             NaN  None  
4           508.0  None  
5             NaN  None  
6             NaN  None  
7           512.0  None  
8             NaN  None  
9             NaN  None  
10          398.0  None  
11            NaN  None  
llm_df = df[df.name == "llm"]

print(
    llm_df["input_tokens"]
)
1     7336.0
4     7336.0
7     7336.0
10    7336.0
Name: input_tokens, dtype: float64