Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.getfoil.ai/llms.txt

Use this file to discover all available pages before exploring further.

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...