Try it
Download notebook
In [ ]:
# Install required packages
!pip install --quiet ipywidgets 'pydantic-ai-slim[openrouter,tavily]' python-dotenv braintrust perplexityai fastmcp

print('✓ Packages installed!')

Pydantic: A million and one tool calls

Let's use Pydantic to do some investigating!

General setup

First we'll set up API keys, tracing, all of that fun stuff.

In [2]:
import os
from getpass import getpass

import braintrust
from dotenv import load_dotenv
from perplexity import Perplexity
from pydantic import BaseModel
from pydantic_ai import Agent, WebSearchTool, WebSearchUserLocation
from pydantic_ai.common_tools.tavily import tavily_search_tool

load_dotenv()

def require_key(name: str, label: str) -> str:
    if not os.getenv(name):
        os.environ[name] = getpass(f"{label}: ")
    return os.environ[name]

require_key("OPENROUTER_API_KEY", "OpenRouter API key")
tavily_key = require_key("TAVILY_API_KEY", "Tavily API key")
require_key("PERPLEXITY_API_KEY", "Perplexity API key")
require_key("BRAINTRUST_API_KEY", "Braintrust API key")

print("Keys initialized.")
Keys initialized.

Instrument Pydantic AI with Braintrust

In [2]:
from braintrust.wrappers.pydantic_ai import setup_pydantic_ai

if os.getenv("BRAINTRUST_API_KEY"):
    setup_pydantic_ai(project_name="dataharvest-2026")
    print("Braintrust connection established.")
else:
    print("Set BRAINTRUST_API_KEY to enable Braintrust tracing.")
Braintrust connection established.

Pydantic AI Agent setup

We'll add a couple tools: Tavily search, Perplexity search, and maybe even the built-in WebSearchTool.

In [ ]:
# Model list at https://ai.pydantic.dev/api/models/base/#pydantic_ai.models.KnownModelName
agent = Agent(
    'openrouter:anthropic/claude-haiku-4-5',
    tools=[tavily_search_tool(tavily_key)],
    # Should we add this?
    # builtin_tools=[WebSearchTool(location=WebSearchUserLocation.US)]
)

# Also set up the Perplexity tool
client = Perplexity()

# This is the other way to add a tool.
# Details at https://pydantic.dev/docs/ai/tools-toolsets/tools/
@agent.tool_plain
async def perplexity_research(query: str):
    """Do research on a topic using Perplexity.

    Args:
        query: what to search for
    """
    search = client.search.create(query=query, max_results=10)
    return search.results

# Define the instructions for the agent

instructions = f"""
You are an investigative journalism assistant that triages news tips.

Your task is to:
1. Evaluate the tip for credibility and newsworthiness (score 1-10)
2. Determine if it is valid (score >= 5) or spam/invalid (score < 5)
3. Categorize it into the most appropriate beat from this list: local news, crime, politics, business, health, technology, environment, education, culture, sports, international news
4. Write a concise 2-3 sentence summary
5. Use Perplexity and/or Tavily search to conduct background research on the topic, entities, and location mentioned
6. Research the tipster's name and email to assess credibility (look for public profiles, previous journalism involvement, potential biases)
7. Generate 3-5 specific follow-up questions for further investigation
8. Compose a professional email response:
   - If valid: Thank them, indicate you're reviewing it, and ask the follow-up questions
   - If invalid: Politely decline and thank them for their interest

Be thorough in your research but efficient. Focus on verifiable facts and credible sources. Understand that people don't really know how to write tips, and you should follow up on most of the ones that are not obviously crazy people.
"""

# Define the output from the agent
class ProcessedTip(BaseModel):
    is_valid: bool
    credibility: int
    newsworthiness: int
    validity: int
    beat: str
    summary: str
    research: str
    follow_up_questions: list[str]
    email_response: str

@braintrust.traced
async def triage_tip(name, email, tip):
    prompt = f"""
    Submitter: {name} - {email}
    Tip details: {tip}
    """
    result = await agent.run(
        prompt,
        instructions=instructions,
        output_type=ProcessedTip,
    )
    output = result.output.model_dump()

    return output

name = "Jonathan Soma"
email = "jonathan.soma@gmail.com"
tip = """
The mayor of Fayetteville, NC has been embezzling. I'm the treasurer,
I know it. I have documents, I can prove it.
"""

result = await triage_tip(name, email, tip)

Now let's see the result.

In [4]:
result
Out[4]:
{'is_valid': True,
 'credibility': 7,
 'newsworthiness': 8,
 'validity': 7,
 'beat': 'crime',
 'summary': "A person identifying as the treasurer of Fayetteville, NC claims the mayor has been embezzling funds and states they have documentary evidence. Fayetteville's current treasurer is Charles Brioso, who has been in the role since April 2024. The tipster claims direct access to evidence of financial crimes.",
 'research': "TIPSTER CREDIBILITY: Jonathan Soma is the Knight Chair of Data Journalism at Columbia University's Journalism School, director of the Data Journalism MS program and the Lede Program, and a recognized expert in investigative journalism and AI tools for journalism. He has worked with ProPublica, The New York Times, and WNYC. His email (jonathan.soma@gmail.com) matches his publicly listed contact information on his personal website. However, there is no public record indicating he is or has been the treasurer of Fayetteville, NC. The current city treasurer is Charles Brioso (as of April 2024), who holds a CMA and CPA certification. POTENTIAL SCENARIOS: This could be (1) an inside source testing journalism outlets, (2) someone impersonating Soma, (3) Soma using a pseudonym intentionally, (4) Soma's email compromised, or (5) a test of editorial processes. FAYETTEVILLE CONTEXT: Mayor Mitch Colvin has served since December 2017. Public records show past controversies including: a $200,000 settlement regarding discrimination/disrespect allegations (2024), questions about a $1,000 donation from a contractor who later failed to complete $7.9 million in city projects, and transparency concerns regarding federal fund usage and fake bonding companies. However, NO current public allegations of mayoral embezzlement exist. The fraud scandal involved a former mayoral candidate (Franco Webb), not the sitting mayor.",
 'follow_up_questions': ["Can you provide your full name and verify your role as treasurer of Fayetteville? The city's current treasurer is listed as Charles Brioso.",
  'Can you share the specific documents you mention as proof? What time period do they cover and what specific transactions do they reference?',
  'Have you reported these allegations to law enforcement, city council, or other authorities? If so, which agencies and when?',
  "What specific actions or transactions constitute the embezzlement you're alleging? Do you have bank records, purchase orders, or other documentation?",
  'Are you aware of any ongoing investigations or audits related to mayoral finances?'],
 'email_response': "Subject: Re: Fayetteville Mayor Embezzlement Tip - Following Up\n\nDear Jonathan,\n\nThank you for submitting this tip regarding financial misconduct involving Fayetteville's city government. Allegations of embezzlement are serious matters that warrant investigation, and we take all credible leads seriously.\n\nBefore we proceed, we need to verify some details to ensure accuracy:\n\n1. Our records show that Charles Brioso is currently listed as the treasurer of the City of Fayetteville (in office since April 2024). Can you confirm your current role and how you're involved with city finances?\n\n2. To evaluate the strength of this tip, could you describe the nature of the alleged embezzlement and the time period it covers? What specific evidence do you have access to?\n\n3. Have you reported these allegations to law enforcement, city council, or an audit department? If so, which agencies have you contacted and when?\n\n4. Can you provide details about specific transactions, amounts, or accounts involved?\n\nWe understand that coming forward with allegations against city leadership can be complicated. However, we need to verify the sourcing and ensure we have sufficient documentation before proceeding with any investigation. Once we hear back from you with these details, we can assess next steps.\n\nPlease reply at your earliest convenience. You can reach us confidentially if needed.\n\nBest regards,\n[Newsroom]"}

If this was an investigative story, what resources would we want to add?