> This page is for version v5.0 (default).
> For other versions, use one of these documentation indexes:
> - v5.0 (default): https://humanloop.com/docs/v5/llms.txt
> - v4.0: https://humanloop.com/docs/v4/llms.txt

> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://humanloop.com/docs/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://humanloop.com/docs/_mcp/server.

> Add logging your AI project using the Humanloop API.

Our SDK offers high-level utilities for integrating Humanloop in your project. You can use the API to the same effect with any language you use or if you prefer more control.

This guide revisits our [logging quickstart tutorial](/docs/v5/quickstart/set-up-logging): we'll use API actions instead of the SDK decorators, showing you how Humanloop instrumentation works step-by-step.

By the end, we'll have a chat agent project integrated with Humanloop logging. The example uses the Python SDK, but the verbs map directly [to our API](/docs/v5/api-reference/sdks).

## Prerequisites

#### Account setup

Create a Humanloop Account

If you haven't already, [create an account](https://app.humanloop.com/signup) or [log in](https://app.humanloop.com/login) to Humanloop

Add an OpenAI API Key

If you're the first person in your organization, you'll need to add an API key to a model provider.

1. Go to OpenAI and [grab an API key](https://platform.openai.com/api-keys).
2. In Humanloop [Organization Settings](https://app.humanloop.com/account/api-keys) set up OpenAI as a model provider.

> **Info**
>
> Using the Prompt Editor will use your OpenAI credits in the same way that the
> OpenAI playground does. Keep your API keys for Humanloop and the model
> providers private.

#### Install dependencies

```bash
pip install humanloop openai
```

Humanloop SDK requires Python 3.9 or higher. Optionally, create a virtual environment to keep dependencies tidy.

## Create the chat agent

We start with a simple chat agent that answers math and science questions.

#### Python

Create an `agent.py` file and add the following:

**`agent.py`**

```python title="agent.py" maxLines=100
import os
import json
import datetime

from humanloop import Humanloop
from openai import OpenAI

openai = OpenAI(api_key="YOUR_OPENAI_KEY")
humanloop = Humanloop(api_key="YOUR_HUMANLOOP_KEY")


def calculator(operation: str, num1: int, num2: int) -> str:
    """Do arithmetic operations on two numbers."""
    if operation == "add":
        return num1 + num2
    elif operation == "subtract":
        return num1 - num2
    elif operation == "multiply":
        return num1 * num2
    elif operation == "divide":
        return num1 / num2
    else:
        return "Invalid operation"


TOOL_JSON_SCHEMA = {
    "name": "calculator",
    "description": "Do arithmetic operations on two numbers.",
    "parameters": {
        "type": "object",
        "required": ["operation", "num1", "num2"],
        "properties": {
            "operation": {"type": "string"},
            "num1": {"type": "integer"},
            "num2": {"type": "integer"},
        },
        "additionalProperties": False,
    },
}


def call_model(messages: list[str]) -> str:
    output = openai.chat.completions.create(
        messages=messages,
        model="gpt-4o",
        tools=[
            {
                "type": "function",
                "function": TOOL_JSON_SCHEMA,
            }
        ],
        temperature=0.7,
    )

    # Check if model asked for a tool call
    if output.choices[0].message.tool_calls:
        for tool_call in output.choices[0].message.tool_calls:
            arguments = json.loads(tool_call.function.arguments)
            if tool_call.function.name == "calculator":
                result = calculator(**arguments)
                return f"[TOOL CALL] {result}"

    # Otherwise, return the LLM response
    return output.choices[0].message.content


def conversation():
    messages = [
        {
            "role": "system",
            "content": "You are a a groovy 80s surfer dude "
            "helping with math and science.",
        },
    ]
    while True:
        user_input = input("You: ")
        if user_input == "exit":
            break
        messages.append({"role": "user", "content": user_input})
        response = call_model(messages=messages)
        messages.append({"role": "assistant", "content": response})
        print(f"Agent: {response}")


if __name__ == "__main__":
    conversation()
```

#### TypeScript

Create an `agent.ts` file and add the following:

**`agent.ts`**

```typescript title="agent.ts" maxLines=100
import { OpenAI } from "openai";
import * as readline from 'readline/promises';
import { HumanloopClient } from "humanloop";
import { ChatCompletionMessageParam } from "openai/resources";

const openai = new OpenAI({ apiKey: "YOUR_OPENAI_KEY" });
const humanloop = new HumanloopClient({apiKey: "YOUR_HUMANLOOP_KEY"});

function calculator(operation: string, num1: number, num2: number): string | number {
    /** Do arithmetic operations on two numbers. */
    switch (operation) {
        case "add":
            return num1 + num2;
        case "subtract":
            return num1 - num2;
        case "multiply":
            return num1 * num2;
        case "divide":
            return num1 / num2;
        default:
            return "Invalid operation";
    }
}

const TOOL_JSON_SCHEMA = {
    name: "calculator",
    description: "Do arithmetic operations on two numbers.",
    parameters: {
        type: "object",
        required: ["operation", "num1", "num2"],
        properties: {
            operation: { type: "string" },
            num1: { type: "integer" },
            num2: { type: "integer" },
        },
        additionalProperties: false,
    },
};

async function callModel(messages: ChatCompletionMessageParam[]): Promise<string> {
    const output = await openai.chat.completions.create({
        messages: messages,
        model: "gpt-4o",
        tools: [
            {
                type: "function",
                function: TOOL_JSON_SCHEMA,
            },
        ],
        temperature: 0.7,
    });

    // Check if model asked for a tool call
    const toolCalls = output.choices[0]?.message?.tool_calls;
    if (toolCalls) {
        for (const toolCall of toolCalls) {
            const toolArguments = JSON.parse(toolCall.function.arguments);
            if (toolCall.function.name === "calculator") {
                const toolStartTime = new Date();
                const result = calculator(toolArguments.operation, toolArguments.num1, toolArguments.num2);
                return `[TOOL CALL] ${result}`;
            }
        }
    }

    // Otherwise, return the LLM response
    return output.choices[0]?.message?.content || "";
}

async function conversation() {
    const messages: ChatCompletionMessageParam[] = [
        {
            role: "system",
            content: "You are a groovy 80s surfer dude helping with math and science.",
        },
    ];

    const rl = readline.createInterface({
        input: process.stdin,
        output: process.stdout,
    });

    while (true) {
        let userInput = await rl.question("You: ")
        if (userInput === "exit") {
            rl.close();
            break;
        }

        messages.push({ role: "user", content: userInput });
        const response = await callModel(traceId, messages);
        messages.push({ role: "assistant", content: response });
        console.log(`Agent: ${response}`);
    }
}

conversation();
```

## Log to Humanloop

The agent works and is capable of function calling. However, we rely on inputs and outputs to reason about the behavior. Humanloop logging allows you to observe the steps taken by the agent, which we will demonstrate below.

### Initialize the trace

Modify `call_model` to accept a `trace_id` argument. It will be used to associate [Logs](/docs/v5/explanations/logs) to the logging trace.

The trace of the conversation will be associated with a [Flow](/docs/v5/explanations/flows). Initialize the trace at the start of the conversation.

#### Python

**`agent.py`**

```python title="agent.py" highlight={1,5-10,23} maxLines=30
def call_model(trace_id: str, messages: list[str]) -> str:
    ...

def conversation():
    trace_id = humanloop.flows.log(
        path="Logging Quickstart/QA Agent",
        flow={
            "attributes": {},
        },
    ).id
    messages = [
        {
            "role": "system",
            "content": "You are a a groovy 80s surfer dude "
            "helping with math and science.",
        },
    ]
    while True:
        user_input = input("You: ")
        if user_input == "exit":
            break
        messages.append({"role": "user", "content": user_input})
        response = call_model(trace_id=trace_id, messages=messages)
        messages.append({"role": "assistant", "content": response})
        print(f"Agent: {response}")
```

#### Typescript

**`agent.ts`**

```typescript title="agent.ts" highlight={1,6-13,35} maxLines=30
async function callModel(traceId: string, messages: ChatCompletionMessageParam[]): Promise<string> {
    ...
}

async function conversation() {
    const flowLog = await humanloop.flows.log({
        path="Logging Quickstart/QA Agent",
        flow={
            "attributes": {},
        },
    });

    const traceId = flowLog.id;

    const messages: ChatCompletionMessageParam[] = [
        {
            role: "system",
            content: "You are a groovy 80s surfer dude helping with math and science.",
        },
    ];

    const rl = readline.createInterface({
        input: process.stdin,
        output: process.stdout,
    });

    while (true) {
        let userInput = await rl.question("You: ")
        if (userInput === "exit") { 
            rl.close();
            break;
        }

        messages.push({ role: "user", content: userInput });
        const response = await callModel(traceId, messages);
        messages.push({ role: "assistant", content: response });
        console.log(`Agent: ${response}`);
    }
}
```

### Add logging

We add log statements that will create the Logs contained in the trace.

#### Python

**`agent.py`**

```python title="agent.py" highlight={2,12-23,30,32-44}
def call_model(trace_id: str, messages: list[str]) -> str:
    prompt_start_time = datetime.datetime.now()
    output = openai.chat.completions.create(
        messages=messages,
        model="gpt-4o",
        tools=[{
            "type": "function",
            "function": TOOL_JSON_SCHEMA,
        }],
        temperature=0.7,
    )
    prompt_log_id = humanloop.prompts.log(
        path="Logging Quickstart/QA Prompt",
        prompt={
            "model": "gpt-4o",
            "tools": [TOOL_JSON_SCHEMA],
            "temperature": 0.7,
        },
        output=output.choices[0].message.content,
        trace_parent_id=trace_id,
        start_time=prompt_start_time,
        end_time=datetime.datetime.now(),
    ).id

    # Check if model asked for a tool call
    if output.choices[0].message.tool_calls:
        for tool_call in output.choices[0].message.tool_calls:
            arguments = json.loads(tool_call.function.arguments)
            if tool_call.function.name == "calculator":
                tool_start_time = datetime.datetime.now()
                result = calculator(**arguments)
                humanloop.tools.log(
                    path="Logging Quickstart/Calculator",
                    tool={
                        "name": "calculator",
                        "description": "Do arithmetic operations on two numbers.",
                        "function": TOOL_JSON_SCHEMA,
                    },
                    inputs=arguments,
                    output=result,
                    trace_parent_id=prompt_log_id,
                    start_time=tool_start_time,
                    end_time=datetime.datetime.now(),
                )
                return f"[TOOL CALL] {result}"

    # Otherwise, return the LLM response
    return output.choices[0].message.content
```

#### Typescript

**`agent.ts`**

```typescript title="agent.ts" highlight={2,15-28,39-53}
async function callModel(traceId: string, messages: ChatCompletionMessageParam[]): Promise<string> {
    const promptStartTime = new Date();
    const output = await openai.chat.completions.create({
        messages: messages,
        model: "gpt-4o",
        tools: [
            {
                type: "function",
                function: TOOL_JSON_SCHEMA,
            },
        ],
        temperature: 0.7,
    });

    const promptLog = await humanloop.prompts.log({
        path: "Logging Quickstart/QA Prompt",
        prompt: {
            model: "gpt-4o",
            tools: [TOOL_JSON_SCHEMA],
            temperature: 0.7,
        },
        output: output.choices[0]?.message?.content || "",
        traceParentId: traceId,
        startTime: promptStartTime,
        endTime: new Date(),
    });

    const promptLogId = promptLog.id;

    // Check if model asked for a tool call
    const toolCalls = output.choices[0]?.message?.tool_calls;
    if (toolCalls) {
        for (const toolCall of toolCalls) {
            const toolArguments = JSON.parse(toolCall.function.arguments);
            if (toolCall.function.name === "calculator") {
                const toolStartTime = new Date();
                const result = calculator(toolArguments.operation, toolArguments.num1, toolArguments.num2);

                await humanloop.tools.log({
                    path: "Logging Quickstart/Calculator",
                    tool: {
                        "function": {
                            "name": "calculator",
                            "description": "Do arithmetic operations on two numbers.",
                            "parameters": TOOL_JSON_SCHEMA
                        },
                    },
                    inputs: toolArguments,
                    output: JSON.stringify(result),
                    traceParentId: promptLogId,
                    startTime: toolStartTime,
                    endTime: new Date(),
                });
                return `[TOOL CALL] ${result}`;
            }
        }
    }

    // Otherwise, return the LLM response
    return output.choices[0]?.message?.content || "";
}

async function conversation() {
    const traceLog = await humanloop.flows.log({
        path: "Logging Quickstart/QA Agent",
        flow: {
            attributes: {},
        },
    });

    const traceId = traceLog.id;

    const messages: ChatCompletionMessageParam[] = [
        {
            role: "system",
            content: "You are a groovy 80s surfer dude helping with math and science.",
        },
    ];

    const rl = readline.createInterface({
        input: process.stdin,
        output: process.stdout,
    });

    while (true) {
        let userInput = await rl.question("You: ")
        if (userInput === "exit") {
            rl.close();
            break;
        }

        messages.push({ role: "user", content: userInput });
        const response = await callModel(traceId, messages);
        messages.push({ role: "assistant", content: response });
        console.log(`Agent: ${response}`);
    }

    await humanloop.flows.updateLog(traceId, {
        "output": "",
        "traceStatus": "complete",
    });
}
```

### Complete the trace

When the conversation is finished, we mark the trace as complete, signalling no more logs will be added.

#### Python

**`agent.py`**

```python title="agent.py" highlight={24-28} maxLines=30
def conversation():
    trace_id = humanloop.flows.log(
        path="Logging Quickstart/QA Agent",
        flow={
            "attributes": {},
        },
    ).id
    messages = [
        {
            "role": "system",
            "content": "You are a a groovy 80s surfer dude "
            "helping with math and science.",
        },
    ]
    while True:
        user_input = input("You: ")
        if user_input == "exit":
            break
        messages.append({"role": "user", "content": user_input})
        response = call_model(trace_id=trace_id, messages=messages)
        messages.append({"role": "assistant", "content": response})
        print(f"Agent: {response}")

    humanloop.flows.update_log(
        log_id=trace_id,
        output="",
        status="complete",
    )
```

#### Typescript

**`agent.ts`**

```typescript title="agent.ts" highlight={36-39}
async function conversation() {
    const traceLog = await humanloop.flows.log({
        path: "Logging Quickstart/QA Agent",
        flow: {
            attributes: {},
        },
    });

    const traceId = traceLog.id;

    const messages: ChatCompletionMessageParam[] = [
        {
            role: "system",
            content: "You are a groovy 80s surfer dude helping with math and science.",
        },
    ];

    const rl = readline.createInterface({
        input: process.stdin,
        output: process.stdout,
    });

    while (true) {
        let userInput = await rl.question("You: ")
        if (userInput === "exit") {
            rl.close();
            break;
        }

        messages.push({ role: "user", content: userInput });
        const response = await callModel(traceId, messages);
        messages.push({ role: "assistant", content: response });
        console.log(`Agent: ${response}`);
    }

    await humanloop.flows.updateLog(traceId, {
        "output": "",
        "traceStatus": "complete",
    });
}
```

## Run the code

Have a conversation with the agent. When you're done, type `exit` to close the program.

#### Python

**`Terminal`**

```bash curl title="Terminal"
python agent.py
You: Hi dude!
Agent: Tubular! I am here to help with math and science, what is groovin?
You: How does flying work?
Agent: ...
You: What is 5678 * 456?
Agent: [TOOL CALL] 2587968
You: exit
```

#### TypeScript

**`Terminal`**

```bash curl title="Terminal"
npx tsc && node dist/index.js
You: Hi dude!
Agent: Tubular! I am here to help with math and science, what is groovin?
You: How does flying work?
Agent: ...
You: What is 5678 * 456?
Agent: [TOOL CALL] 2587968
You: exit
```

## Check your workspace

Navigate to [your workspace](https://app.humanloop.com) to see the logged conversation.

Inside the **Logging Quickstart** directory on the left, click the **QA Agent** [Flow](/docs/v5/explanation/flows). Select the **Logs** tab from the top of the page and click the Log inside the table.

You will see the conversation's trace, containing Logs corresponding to the [Tool](/docs/v5/explanation/tools) and the [Prompt](/docs/v5/explanation/prompts).

![](/docs/_fern-img/5cda056d966bab0861d9d8d5a22f524426023a24052b8ee419da0a0c6fa6c035.webp)

## Change the agent and rerun

Modify the `call_model` function to use a different model and temperature.

#### Python

**`agent.py`**

```python title="agent.py" highlight={5,10,15,17} maxLines=30
def call_model(trace_id: str, messages: list[str]) -> str:
    prompt_start_time = datetime.datetime.now()
    output = openai.chat.completions.create(
        messages=messages,
        model="gpt-4o-mini",
        tools=[{
            "type": "function",
            **TOOL_JSON_SCHEMA,
        }],
        temperature=0.2,
    )
    prompt_log_id = humanloop.prompts.log(
        path="Logging Quickstart/QA Prompt",
        prompt={
            "model": "gpt-4o-mini",
            "tools": [TOOL_JSON_SCHEMA],
            "temperature": 0.2,
        }
        output=output.choices[0].message.content,
        trace_parent_id=trace_id,
        start_time=prompt_start_time,
        end_time=datetime.datetime.now(),
    ).id

    ...
```

#### Typescript

**`agent.ts`**

```typescript title="agent.ts" highlight={5,12,18,20} maxLines=30
async function callModel(traceId: string, messages: ChatCompletionMessageParam[]): Promise<string> {
    const promptStartTime = new Date();
    const output = await openai.chat.completions.create({
        messages: messages,
        model: "gpt-4o-mini",
        tools: [
            {
                type: "function",
                function: TOOL_JSON_SCHEMA,
            },
        ],
        temperature: 0.2,
    });

    const promptLog = await humanloop.prompts.log({
        path: "Logging Quickstart/QA Prompt",
        prompt: {
            model: "gpt-4o-mini",
            tools: [TOOL_JSON_SCHEMA],
            temperature: 0.2,
        },
        output: output.choices[0]?.message?.content || "",
        traceParentId: traceId,
        startTime: promptStartTime,
        endTime: new Date(),
    });

    ...
```

Run the agent again, then head back to your workspace.

Click the **QA Prompt** [Prompt](/docs/v5/explanation/prompts), select the **Dashboard** tab from the top of the page
and you should find a new version of the Prompt at the top of the list.

By changing the hyperparameters of the OpenAI call, you have tagged a new version of the Prompt.

![](/docs/_fern-img/2293bc119e4fc55e04565a772bdcee9b47a2471003f72bf0f69fae4db064052d.webp)

## Next steps

Logging is the first step to observing your AI product. Read these guides to learn more about evals on Humanloop:

* Add [monitoring Evaluators](/docs/v5/guides/observability/monitoring) to evaluate Logs as they're made against a File.

* See evals in action in our [tutorial on evaluating an agent](/docs/v5/tutorials/agent-evaluation).