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

Ayesha MughalAyesha Mughal
10 min read

🚀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:

  1. Base Agent Class: Foundation for all agents

  2. Specialized Analysts: Each for different poetry types

  3. Triage Agent: Orchestrates handoffs between agents

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

  • Modular 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:

  1. Classification: Uses tool calling to determine poetry type

  2. Agent Selection: Chooses appropriate specialized analyst

  3. Delegation: Passes poem to selected analyst

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

  • classify_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:

  1. Add more poetry types (Epic, Sonnet, Haiku)

  2. Implement conversation memory for multi-turn interactions

  3. Add image analysis for visual poetry

  4. Create API endpoints for external integration

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

1
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