🌟Building a Poetry Analysis System with OpenAI SDK Concepts: Tool Calling + Agent Handoffs

Table of contents
- 🚀Introduction
- 🔧What We'll Build
- Prerequisites
- Project Setup
- Core Architecture
- ✍🏻Step 1: Base Agent Class
- 🤖Step 2: AI-Powered Analysis System
- 🎶Step 3: Poetry Classification System
- 🌟Step 4: Specialized Agents
- 📊Step 5: Triage Agent (Handoffs Orchestrator)
- 🔮Step 6: Modern Web UI with Streamlit
- 🪑Step 7: UI Components and User Interaction
- 📱Step 8: Results Display
- Running the Application
- Key Concepts Demonstrated
- Testing the System
- Conclusion
- Next Steps

🚀Introduction
In this tutorial, we'll build a sophisticated poetry analysis system that demonstrates core OpenAI SDK concepts like Tool Calling and Agent Handoffs. This project showcases how to create a multi-agent architecture where specialized agents work together to analyze different types of poetry.
🔧What We'll Build
Our system will:
Classify poems into three types: Lyric, Narrative, and Dramatic
Analyze poetry using AI-powered detailed interpretations
Demonstrate tool calling between agents
Show agent handoffs for specialized processing
Provide a modern web UI with Streamlit
Prerequisites
Before we start, make sure you have:
Python 3.11+ installed
A Gemini API key (free from Google AI Studio)
Basic understanding of Python classes and functions
Project Setup
1. Create Project Structure
First, let's set up our project:
mkdir poetry-analysis-system
cd poetry-analysis-system
2. Install Dependencies
Create a requirements.txt
file:
streamlit
python-dotenv
google-generativeai
Install using uv (recommended) or pip:
# Using uv (faster)
uv add -r requirements.txt
# Or using pip
pip install -r requirements.txt
3. Environment Setup
Create a .env
file in your project root:
GEMINI_API_KEY=your_gemini_api_key_here
Core Architecture
Our system uses a multi-agent architecture with the following components:
Base Agent Class: Foundation for all agents
Specialized Analysts: Each for different poetry types
Triage Agent: Orchestrates handoffs between agents
Tool Calling System: Enables agents to call specific functions
✍🏻Step 1: Base Agent Class
Let's start with the foundation - our base agent class that demonstrates tool calling:
import os
from dotenv import load_dotenv
import google.generativeai as genai
from typing import Dict, List, Tuple, Any
# Load environment variables for API keys
load_dotenv()
GEMINI_API_KEY = os.getenv('GEMINI_API_KEY')
genai.configure(api_key=GEMINI_API_KEY)
# Base agent class demonstrating OpenAI SDK concepts like tool calling
class PoetryAgent:
"""Base agent class demonstrating OpenAI SDK concepts"""
def __init__(self, name: str, tools: List[Dict] = None):
self.name = name # Agent identifier
self.tools = tools or [] # Available tools for this agent
def call_tool(self, tool_name: str, **kwargs) -> Any:
"""Simulate tool calling functionality - core OpenAI SDK concept"""
if tool_name == "analyze_poem":
return self.analyze_poem(**kwargs)
elif tool_name == "classify_poem":
return self.classify_poem(**kwargs)
return None
def analyze_poem(self, poem: str, poem_type: str) -> str:
"""Tool for poem analysis - demonstrates tool calling"""
return self._generate_analysis(poem, poem_type)
def classify_poem(self, poem: str) -> str:
"""Tool for poem classification - demonstrates tool calling"""
return self._classify_poem_type(poem)
Key Concepts Explained:
Tool Calling: The
call_tool()
method simulates how agents can call specific functionsModular Design: Each tool has a single responsibility
Extensibility: Easy to add new tools by extending the base class
🤖Step 2: AI-Powered Analysis System
Now let's add the analysis capabilities using Gemini API:
def _generate_analysis(self, poem: str, poem_type: str) -> str:
"""Generate detailed analysis using Gemini API with fallback"""
prompt = f"""
You are an expert poetry analyst. Analyze the following {poem_type} poem in detail. Provide a deep, paragraph-level 'tashreeh' (description and interpretation) in simple language, covering:
- The main theme and message
- The emotions and imagery
- The poetic devices used (like metaphors, similes, rhyme, etc.)
- The impact on the reader
- Any cultural or literary context if relevant
Poem:
{poem}
"""
# Try different models in order of preference for reliability
models_to_try = [
'models/gemini-1.5-flash-latest', # Fastest, least quota intensive
'models/gemini-2.0-flash', # Alternative flash model
'models/gemini-1.5-pro-latest' # Pro model as last resort
]
for model_name in models_to_try:
try:
model = genai.GenerativeModel(model_name)
response = model.generate_content(prompt)
result = response.text.strip()
if result and len(result) > 50: # Ensure meaningful response
return result
except Exception as e:
continue
return self._fallback_analysis(poem, poem_type)
Why This Approach:
Multi-Model Fallback: Ensures reliability even when some models fail
Cost Efficiency: Uses cheaper models first
Quality Assurance: Checks response length for meaningful content
🎶Step 3: Poetry Classification System
Our classification system uses AI-powered scoring:
def _classify_poem_type(self, poem: str) -> str:
"""Classify poem type using scoring system - demonstrates AI decision making"""
poem_lower = poem.lower()
# Enhanced indicators for better classification accuracy
lyric_indicators = ["i ", "my ", "me ", "feel", "heart", "love", "alone", "sad", "happy", "joy", "pain", "soul", "emotion", "dream", "hope", "fear", "lonely", "weep", "tears", "smile", "cry", "sigh", "ache", "longing", "desire", "passion"]
narrative_indicators = ["once", "story", "journey", "adventure", "hero", "tale", "legend", "history", "battle", "war", "quest", "travel", "road", "path", "began", "started", "went", "came", "found", "met", "said", "told", "knight", "kingdom", "dragon", "forest", "mountain", "fought", "saved", "brave", "young", "old", "land", "far", "through", "rode", "sword", "armor"]
dramatic_indicators = ["audience", "stage", "speak", "perform", "act", "scene", "drama", "theater", "monologue", "dialogue", "character", "role", "play", "recite", "voice", "speech", "address", "call", "shout", "whisper", "director", "cried", "trembling", "lines", "passion", "crowd", "listen", "friends", "truth", "echoed", "voice", "tale", "death", "choice"]
# Calculate scores for each poetry type
lyric_score = sum(1 for indicator in lyric_indicators if indicator in poem_lower)
narrative_score = sum(1 for indicator in narrative_indicators if indicator in poem_lower)
dramatic_score = sum(1 for indicator in dramatic_indicators if indicator in poem_lower)
scores = [("Lyric", lyric_score), ("Narrative", narrative_score), ("Dramatic", dramatic_score)]
return max(scores, key=lambda x: x[1])[0]
Classification Logic:
Word Matching: Counts relevant words in the poem
Scoring System: Determines the most likely poetry type
Decision Making: Returns the type with highest score
🌟Step 4: Specialized Agents
Now let's create our specialized agents for different poetry types:
# Specialized agents demonstrating handoffs concept
class PoetAgent(PoetryAgent):
"""Agent for generating poems - demonstrates single responsibility principle"""
def __init__(self):
super().__init__("Poet Agent")
def generate_poem(self) -> str:
"""Generate a sample poem for demonstration"""
return (
"In the quiet of the night, I dream alone,\n"
"Stars above whisper secrets unknown.\n"
"\n"
"A gentle breeze carries hopes anew,\n"
"Painting the sky in a tranquil hue."
)
class LyricAnalyst(PoetryAgent):
"""Specialized agent for lyric poetry - demonstrates specialization"""
def __init__(self):
super().__init__("Lyric Analyst")
class NarrativeAnalyst(PoetryAgent):
"""Specialized agent for narrative poetry - demonstrates specialization"""
def __init__(self):
super().__init__("Narrative Analyst")
class DramaticAnalyst(PoetryAgent):
"""Specialized agent for dramatic poetry - demonstrates specialization"""
def __init__(self):
super().__init__("Dramatic Analyst")
Agent Design Principles:
Single Responsibility: Each agent has one specific purpose
Inheritance: All agents inherit from the base class
Specialization: Each analyst focuses on specific poetry types
📊Step 5: Triage Agent (Handoffs Orchestrator)
This is where the magic happens - our orchestrator agent that demonstrates handoffs:
class TriageAgent(PoetryAgent):
"""Orchestrator agent for handoffs - demonstrates agent coordination"""
def __init__(self):
super().__init__("Triage Agent")
# Initialize specialized analysts for handoffs
self.analysts = {
"Lyric": LyricAnalyst(),
"Narrative": NarrativeAnalyst(),
"Dramatic": DramaticAnalyst()
}
def process_poem(self, poem: str) -> Tuple[str, str]:
"""Main handoff logic - demonstrates agent coordination and tool calling"""
# Step 1: Classify the poem using tool calling
poem_type = self.call_tool("classify_poem", poem=poem)
# Step 2: Handoff to appropriate analyst (core handoffs concept)
analyst = self.analysts.get(poem_type, self.analysts["Lyric"])
analysis = analyst.call_tool("analyze_poem", poem=poem, poem_type=poem_type)
return poem_type, analysis
Handoffs Process Explained:
Classification: Uses tool calling to determine poetry type
Agent Selection: Chooses appropriate specialized analyst
Delegation: Passes poem to selected analyst
Analysis: Gets specialized analysis from the expert agent
🔮Step 6: Modern Web UI with Streamlit
Now let's create a beautiful, modern interface:
import streamlit as st
def main():
st.set_page_config(
page_title="Poetry Analysis - Handoffs Demo",
page_icon="📚",
layout="wide"
)
# Custom CSS for modern dark theme UI
st.markdown("""
<style>
.stApp {
background-color: #1a1a1a;
color: #ffffff;
}
.main-header {
background: linear-gradient(90deg, #2c3e50 0%, #34495e 100%);
padding: 2rem;
border-radius: 15px;
color: white;
text-align: center;
margin-bottom: 2rem;
box-shadow: 0 8px 32px rgba(0,0,0,0.3);
}
.agent-card {
background: #2d3748;
padding: 1.5rem;
border-radius: 10px;
border-left: 4px solid #667eea;
margin: 1rem 0;
color: #e2e8f0;
box-shadow: 0 4px 16px rgba(0,0,0,0.2);
}
.success-box {
background: #1a4731;
border: 1px solid #2d5a3d;
border-radius: 8px;
padding: 1rem;
margin: 1rem 0;
color: #d4edda;
box-shadow: 0 4px 16px rgba(0,0,0,0.2);
}
.info-box {
background: #1e3a5f;
border: 1px solid #2d5a8a;
border-radius: 8px;
padding: 1rem;
margin: 1rem 0;
color: #d1ecf1;
box-shadow: 0 4px 16px rgba(0,0,0,0.2);
}
</style>
""", unsafe_allow_html=True)
# Header with project title and description
st.markdown("""
<div class="main-header">
<h1>🎭 Poetry Analysis System</h1>
<p><strong>OpenAI SDK Demo: Tool Calling & Agent Handoffs</strong></p>
</div>
""", unsafe_allow_html=True)
# Initialize agents in session state for persistence
if 'triage_agent' not in st.session_state:
st.session_state.triage_agent = TriageAgent()
if 'poet_agent' not in st.session_state:
st.session_state.poet_agent = PoetAgent()
🪑Step 7: UI Components and User Interaction
Let's add the interactive components:
# Sidebar for controls and agent status
with st.sidebar:
st.markdown("### 🎛️ Controls")
col1, col2 = st.columns(2)
with col1:
if st.button("📝 Generate Sample", use_container_width=True):
st.session_state.poem = st.session_state.poet_agent.generate_poem()
st.rerun()
with col2:
if st.button("🗑️ Clear", use_container_width=True):
st.session_state.poem = ""
st.rerun()
st.markdown("---")
st.markdown("### 📊 Agent Status")
st.success("✅ Triage Agent: Active")
st.success("✅ Lyric Analyst: Ready")
st.success("✅ Narrative Analyst: Ready")
st.success("✅ Dramatic Analyst: Ready")
# Main content area with input and examples
col1, col2 = st.columns([2, 1])
with col1:
st.markdown("### 📝 Enter Your Poem")
poem_input = st.text_area(
"Paste or write your poem here:",
value=st.session_state.get('poem', ''),
height=200,
placeholder="Enter your poem here..."
)
st.session_state.poem = poem_input
if st.button("🔍 Analyze Poem", type="primary", use_container_width=True):
if not poem_input.strip():
st.warning("⚠️ Please enter or generate a poem first.")
else:
with st.spinner("🤖 Processing with AI agents..."):
poem_type, analysis = st.session_state.triage_agent.process_poem(poem_input)
st.session_state.result = (poem_type, analysis)
st.rerun()
with col2:
st.markdown("### 🎯 Quick Examples")
# Example poems for each type
examples = {
"Lyric": "I feel the weight of sorrow in my heart\nAs tears fall like rain in the dark",
"Narrative": "Once upon a time in lands afar\nA brave young knight set out to war",
"Dramatic": '"Speak to the audience," the director cried\nAs I stood trembling, terrified'
}
for poem_type, example in examples.items():
if st.button(f"📖 {poem_type}", key=f"example_{poem_type}"):
st.session_state.poem = example
st.rerun()
📱Step 8: Results Display
Finally, let's add the results display:
# Results section displaying analysis
if hasattr(st.session_state, 'result'):
poem_type, analysis = st.session_state.result
st.markdown("---")
st.markdown("### 📊 Analysis Results")
# Display detected poem type
st.markdown(f"""
<div class="success-box">
<h4>🎯 Detected Type: <strong>{poem_type}</strong></h4>
</div>
""", unsafe_allow_html=True)
# Display detailed analysis
st.markdown(f"""
<div class="info-box">
<h4>📝 Detailed Analysis (Tashreeh)</h4>
<div style="margin-top: 1rem;">
{analysis.replace(chr(10), '<br>')}
</div>
</div>
""", unsafe_allow_html=True)
# Display agent handoff process
st.markdown(f"""
<div class="agent-card">
<h5>🤖 Agent Handoff Process</h5>
<p><strong>Triage Agent</strong> → <strong>{poem_type} Analyst</strong></p>
</div>
""", unsafe_allow_html=True)
if __name__ == "__main__":
main()
Running the Application
To run your poetry analysis system:
streamlit run main.py
The application will be available at http://localhost:8501
Key Concepts Demonstrated
1. Tool Calling
Our agents can call specific tools for different tasks:
analyze_poem()
: Generates detailed literary analysisclassify_poem()
: Determines poetry type using AI scoring
2. Agent Handoffs
The system demonstrates intelligent routing:
Triage Agent: Receives input and makes decisions
Specialized Analysts: Provide expert analysis for specific poetry types
Seamless Coordination: Agents work together without conflicts
3. Fallback Systems
Reliability is built into the system:
Multi-Model Support: Tries different Gemini models
Manual Fallback: Provides analysis when APIs fail
Error Handling: Graceful degradation under any circumstances
Testing the System
Try these example poems to see the system in action:
Lyric Poetry Example:
I feel the weight of sorrow in my heart
As tears fall like rain in the dark
Narrative Poetry Example:
Once upon a time in lands afar
A brave young knight set out to war
Dramatic Poetry Example:
"Speak to the audience," the director cried
As I stood trembling, terrified
Conclusion
This project demonstrates how to build sophisticated AI systems using core concepts from the OpenAI SDK:
Tool Calling: Enables modular, reusable functionality
Agent Handoffs: Allows specialized processing and coordination
Fallback Systems: Ensures reliability and user experience
Modern UI: Professional interface with Streamlit
The architecture is scalable, maintainable, and demonstrates real-world application of AI agent concepts. You can extend this system by adding new poetry types, specialized analysts, or additional tools.
Next Steps
Consider these enhancements:
Add more poetry types (Epic, Sonnet, Haiku)
Implement conversation memory for multi-turn interactions
Add image analysis for visual poetry
Create API endpoints for external integration
Add user authentication and poem history
The foundation we've built is solid and extensible for any AI agent system you want to create!
🔹 Final Thoughts from MughalSyntax
You can try the live demo here:
👉 Poetry Analysis App
Follow me for more:
Next blog? Maybe…
“Teaching AI to write ghazals?” 🌧️
Stay poetic, stay agentic. ✨
Subscribe to my newsletter
Read articles from Ayesha Mughal directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Ayesha Mughal
Ayesha Mughal
💻 CS Student | Python & Web Dev Enthusiast 🚀 Exploring Agentic AI | CS50x Certified ✨ Crafting logic with elegance