Skip to main content

Python SDK

The Foil Python SDK provides automatic OpenAI instrumentation, tracing, logging, and feedback collection for Python applications.
Full examples: Browse runnable examples at github.com/getfoil/foil-examples.

Installation

Requirements: Python 3.8 or higher
pip install foil-sdk
Or with poetry:
poetry add foil-sdk

Configuration

Get your API key from the Foil Dashboard under Settings > API Keys.
Never hardcode API keys in your source code. Use environment variables instead.
# .env or shell
export FOIL_API_KEY=sk_live_xxx_yyy
import os
from foil import Foil

foil = Foil(api_key=os.environ['FOIL_API_KEY'])

Configuration Options

OptionTypeRequiredDefaultDescription
api_keystrYes-Your Foil API key
agent_namestrNo-Agent name (required for tracing)
debugboolNoFalseEnable debug logging

Quick Start: OpenAI Wrapper

The easiest way to get started is with the OpenAI wrapper, which automatically traces all your API calls:
from openai import OpenAI
from foil import Foil
import os

# Initialize clients
client = OpenAI()
foil = Foil(api_key=os.environ['FOIL_API_KEY'])

# Wrap the OpenAI client
wrapped_client = foil.wrap_openai(client)

# All calls are now automatically traced
response = wrapped_client.chat.completions.create(
    model='gpt-4o',
    messages=[{'role': 'user', 'content': 'Hello!'}]
)

print(response.choices[0].message.content)
Every call through wrapped_client is automatically logged to Foil with model name, input messages, output response, token usage, latency, and errors (if any).

Streaming

Streaming responses work seamlessly:
wrapped_client = foil.wrap_openai(client)

stream = wrapped_client.chat.completions.create(
    model='gpt-4o',
    messages=[{'role': 'user', 'content': 'Write a haiku'}],
    stream=True
)

for chunk in stream:
    if chunk.choices[0].delta.content:
        print(chunk.choices[0].delta.content, end='', flush=True)

# TTFT and full content are automatically captured

Tool Calls

Function/tool calls are automatically tracked:
tools = [
    {
        'type': 'function',
        'function': {
            'name': 'get_weather',
            'description': 'Get weather for a location',
            'parameters': {
                'type': 'object',
                'properties': {
                    'location': {'type': 'string'}
                },
                'required': ['location']
            }
        }
    }
]

wrapped_client = foil.wrap_openai(client)

response = wrapped_client.chat.completions.create(
    model='gpt-4o',
    messages=[{'role': 'user', 'content': 'What is the weather in Paris?'}],
    tools=tools
)

# Tool calls are captured in the trace
if response.choices[0].message.tool_calls:
    for tool_call in response.choices[0].message.tool_calls:
        print(f'Tool: {tool_call.function.name}')
        print(f'Args: {tool_call.function.arguments}')

Using Traces

For structured tracing with full span hierarchy, use Foil with agent_name and the trace() method:
from foil import Foil
import os

foil = Foil(
    api_key=os.environ['FOIL_API_KEY'],
    agent_name='my-agent',
)

def my_workflow(ctx):
    # LLM call with automatic span creation
    result = ctx.llm_call('gpt-4o', lambda _: openai.chat.completions.create(
        model='gpt-4o',
        messages=[{'role': 'user', 'content': 'Hello!'}],
    ))

    # Tool calls nest under the trace automatically
    data = ctx.tool('search', lambda _: search_api(query))

    return result.choices[0].message.content

output = foil.trace(my_workflow, name='my-trace', input='Hello!')
print(output)
This creates a trace with nested spans:
Trace: my-agent
├── LLM: gpt-4o
└── TOOL: search

Available Span Methods

MethodDescription
ctx.llm_call(model, fn)Wrap an LLM call in a span
ctx.tool(name, fn)Wrap a tool execution in a span
ctx.retriever(name, fn)Wrap a retriever operation
ctx.embedding(model, fn)Wrap an embedding operation
ctx.start_span(kind, name)Manual span for full control
ctx.record_feedback(positive)Record thumbs up/down
ctx.record_signal(name, value)Record a custom signal

Fire-and-Forget Logging

For manual logging without the OpenAI wrapper:
foil.log({
    'model': 'gpt-4o',
    'input': messages,
    'output': response,
    'latency': 1200,
    'tokens': {
        'prompt': 100,
        'completion': 50,
        'total': 150
    },
    'status': 'completed'
})
This is non-blocking and doesn’t wait for a response.

Complete Example

from openai import OpenAI
from foil import Foil
import os

def main():
    # Initialize clients
    client = OpenAI()
    foil = Foil(api_key=os.environ['FOIL_API_KEY'])

    # Wrap OpenAI client
    wrapped_client = foil.wrap_openai(client)

    # Chat application
    messages = [
        {'role': 'system', 'content': 'You are a helpful assistant.'}
    ]

    print('Chat with GPT-4o (type "quit" to exit)')

    while True:
        user_input = input('You: ')
        if user_input.lower() == 'quit':
            break

        messages.append({'role': 'user', 'content': user_input})

        # Automatically traced
        response = wrapped_client.chat.completions.create(
            model='gpt-4o',
            messages=messages,
            stream=True
        )

        print('Assistant: ', end='')
        full_response = ''
        for chunk in response:
            if chunk.choices[0].delta.content:
                content = chunk.choices[0].delta.content
                print(content, end='', flush=True)
                full_response += content
        print()

        messages.append({'role': 'assistant', 'content': full_response})

if __name__ == '__main__':
    main()

Error Handling

Errors are automatically captured:
wrapped_client = foil.wrap_openai(client)

try:
    response = wrapped_client.chat.completions.create(
        model='gpt-4o',
        messages=[{'role': 'user', 'content': 'Hello'}]
    )
except Exception as e:
    # Error is already recorded in Foil
    print(f'Error: {e}')
    raise

Advanced Patterns

The wrapper works with the async OpenAI client:
from openai import AsyncOpenAI
from foil import Foil
import asyncio

async def main():
    client = AsyncOpenAI()
    foil = Foil(api_key=os.environ['FOIL_API_KEY'])

    wrapped_client = foil.wrap_openai(client)

    response = await wrapped_client.chat.completions.create(
        model='gpt-4o',
        messages=[{'role': 'user', 'content': 'Hello!'}]
    )

    print(response.choices[0].message.content)

asyncio.run(main())
Each call through the wrapped client is logged separately:
wrapped_client = foil.wrap_openai(client)
messages = []

messages.append({'role': 'user', 'content': 'My name is Alice'})
response = wrapped_client.chat.completions.create(
    model='gpt-4o',
    messages=messages
)
messages.append({'role': 'assistant', 'content': response.choices[0].message.content})

messages.append({'role': 'user', 'content': 'What is my name?'})
response = wrapped_client.chat.completions.create(
    model='gpt-4o',
    messages=messages
)
When one agent delegates to another, use span.create_child_context() to create nested agent spans:
def agent_fn(ctx):
    coordinator_span = ctx.start_span(
        SpanKind.AGENT, 'coordinator',
        {'input': 'Plan a trip to Tokyo'},
    )

    child_ctx = coordinator_span.create_child_context()

    flight_span = child_ctx.start_span(
        SpanKind.AGENT, 'flight-searcher',
        {'input': 'Find flights to Tokyo'},
    )
    # ... sub-agent work ...
    flight_span.end({'output': 'Flight options compiled'})

    hotel_span = child_ctx.start_span(
        SpanKind.AGENT, 'hotel-searcher',
        {'input': 'Find hotels in Tokyo'},
    )
    # ... sub-agent work ...
    hotel_span.end({'output': 'Hotel options compiled'})

    coordinator_span.end({'output': 'Trip plan complete'})

foil.trace(agent_fn, name='trip-planner')
Trace input and output guardrail checks as separate spans:
def agent_fn(ctx):
    guard_span = ctx.start_span(
        SpanKind.CHAIN, 'input-guardrail',
        {'input': user_input},
    )
    is_safe = check_input_guardrail(user_input)
    guard_span.end({
        'output': {'safe': is_safe},
        'properties': {'guardrail_type': 'input'},
    })

    if not is_safe:
        return 'Request blocked by guardrail'

    # Continue with LLM call...