%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