We are going to build a sales intelligence agent that researches a prospect company and drafts a personalized outreach email. It runs entirely through the OpenAI SDK against Oxlo.ai, using function calling, a multi-turn reasoning loop, and structured JSON output. If you are building agentic workflows and want predictable costs per request instead of ballooning token bills, this stack is worth evaluating.
What you'll need
- Python 3.10 or newer
- The OpenAI SDK:
pip install openai - An Oxlo.ai API key from https://portal.oxlo.ai
Step 1: Set up the client and verify connectivity
First, I initialize the OpenAI client pointing at Oxlo.ai and make a quick health check. I use llama-3.3-70b here because it is a reliable general-purpose model for a quick ping.
from openai import OpenAI
import json
client = OpenAI(base_url="https://api.oxlo.ai/v1", api_key="YOUR_OXLO_API_KEY")
def verify_connection():
response = client.chat.completions.create(
model="llama-3.3-70b",
messages=[
{"role": "user", "content": "Say 'Oxlo.ai connection active' and nothing else."}
],
max_tokens=20,
)
print(response.choices[0].message.content)
verify_connection()
Step 2: Define the system prompt and tool schemas
Next, I lock down the agent's behavior with a strict system prompt and declare two tools, get_company_news and get_decision_maker. The model will request these when it needs context.
SYSTEM_PROMPT = """You are a sales intelligence agent. Your job is to research a target company and draft a personalized outreach email to a specific decision maker.
Rules:
1. Always call the available tools to gather context before writing.
2. Once you have enough information, output a valid JSON object with keys: recipient_email, subject, body, cited_sources.
3. Do not make up data. Only use facts returned by tools.
4. Keep the email under 150 words.
"""
tools = [
{
"type": "function",
"function": {
"name": "get_company_news",
"description": "Retrieve recent news about a company.",
"parameters": {
"type": "object",
"properties": {
"company_name": {"type": "string"}
},
"required": ["company_name"]
}
}
},
{
"type": "function",
"function": {
"name": "get_decision_maker",
"description": "Find a decision maker at a company.",
"parameters": {
"type": "object",
"properties": {
"company_name": {"type": "string"}
},
"required": ["company_name"]
}
}
}
]
Step 3: Implement the tool execution logic
Since this is a self-contained tutorial, I will mock the external APIs with simple Python functions that return realistic JSON. In production, these would hit your CRM or news service.
def get_company_news(company_name: str):
if "acme" in company_name.lower():
return {
"headlines": [
"Acme Corp expands into AI logistics",
"Acme Corp hires new CTO from major cloud provider"
]
}
return {"headlines": [f"{company_name} announces Q3 earnings beat"]}
def get_decision_maker(company_name: str):
if "acme" in company_name.lower():
return {
"name": "Sarah Chen",
"title": "VP of Engineering",
"email": "s.chen@acmecorp.example"
}
return {
"name": "Alex Smith",
"title": "Director of Operations",
"email": "alex.smith@example.com"
}
def dispatch_tool_call(name: str, arguments: str):
args = json.loads(arguments)
if name == "get_company_news":
return get_company_news(args["company_name"])
if name == "get_decision_maker":
return get_decision_maker(args["company_name"])
raise ValueError(f"Unknown tool: {name}")
Step 4: Build the research loop with function calling
Now I wire up the conversation loop. I send the user request to qwen-3-32b, which excels at agent workflows on Oxlo.ai, and handle any tool calls until the model stops requesting tools.
def research_company(company_name: str):
messages = [
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": f"Research {company_name} and identify a decision maker. Call all relevant tools."}
]
while True:
response = client.chat.completions.create(
model="qwen-3-32b",
messages=messages,
tools=tools,
tool_choice="auto",
)
message = response.choices[0].message
messages.append(message.model_dump())
if message.tool_calls:
for tc in message.tool_calls:
result = dispatch_tool_call(tc.function.name, tc.function.arguments)
messages.append({
"role": "tool",
"tool_call_id": tc.id,
"name": tc.function.name,
"content": json.dumps(result),
})
else:
return messages
Step 5: Enforce structured JSON output for the final deliverable
Relying on the model to guess the format is brittle. After the research phase, I make one final call with JSON mode to guarantee valid structured output. I use deepseek-v3.2 here because it handles coding and reasoning tasks cleanly, and it is available on the free tier for experimentation.
def draft_email(research_messages: list):
research_messages.append({
"role": "user",
"content": "Now draft the final outreach email as a JSON object with keys: recipient_email, subject, body, cited_sources."
})
response = client.chat.completions.create(
model="deepseek-v3.2",
messages=research_messages,
response_format={"type": "json_object"},
)
return json.loads(response.choices[0].message.content)
def generate_outreach(company_name: str):
research_messages = research_company(company_name)
return draft_email(research_messages)
Step 6: Stream the final response for real-time feedback
For production UIs, waiting for the entire JSON blob is slow. I add an optional streaming step so the final structured response arrives token by token. Oxlo.ai supports streaming with no cold starts on popular models.
def draft_email_streamed(research_messages: list):
research_messages.append({
"role": "user",
"content": "Now draft the final outreach email as a JSON object with keys: recipient_email, subject, body, cited_sources."
})
stream = client.chat.completions.create(
model="deepseek-v3.2",
messages=research_messages,
response_format={"type": "json_object"},
stream=True,
)
print("Streaming JSON output:")
for chunk in stream:
delta = chunk.choices[0].delta.content
if delta:
print(delta, end="", flush=True)
print()
# Example usage
research_messages = research_company("Acme Corp")
draft_email_streamed(research_messages)
Run it
Putting it all together, I call the non-streaming pipeline and inspect the result.
if __name__ == "__main__":
result = generate_outreach("Acme Corp")
print(json.dumps(result, indent=2))
Example output:
{
"recipient_email": "s.chen@acmecorp.example",
"subject": "Quick question about AI logistics at Acme Corp",
"body": "Hi Sarah,\n\nI saw that Acme Corp recently expanded into AI logistics and hired a new CTO from a major cloud provider. I help engineering teams cut infrastructure costs by 30% during rapid scaling.\n\nWorth a brief conversation?\n\nBest,\n[Your Name]",
"cited_sources": [
"Acme Corp expands into AI logistics",
"Acme Corp hires new CTO from major cloud provider"
]
}
Wrap-up
From here, you could wire the tool functions to real APIs like Clearbit or Crunchbase, or swap in kimi-k2.6 for longer context if you are researching enterprise prospects with hundreds of news items. Because Oxlo.ai charges a flat rate per request, running multi-turn agent loops with long system prompts does not inflate your bill the way token-based pricing does. Check the details at https://oxlo.ai/pricing and scale the loop without watching input tokens.
Top comments (0)