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.
Installation
Requirements: Python 3.8 or higher
Or with poetry:
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
Option Type Required Default Description api_keystr Yes - Your Foil API key agent_namestr No - Agent name (required for tracing) debugbool No FalseEnable 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
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
Method Description 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
)
Multi-agent handoffs with create_child_context
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...