Building an Enhanced Persona Generator and Responder with Python and OpenAI

In the age of artificial intelligence, creating personalized and context-aware applications has become increasingly accessible. One such application is the Enhanced Persona Generator and Responder, which analyzes a sample text to generate a detailed persona and then uses that persona to craft tailored responses to user prompts. In this blog post, we’ll walk through building this application step-by-step using Python and OpenAI’s powerful language models.

Table of Contents

  1. Introduction
  2. Prerequisites
  3. Project Setup
  4. Implementing the Agents
  5. Utility Modules
  6. Main Orchestrator (main.py)
  7. Running the Application
  8. Troubleshooting
  9. Conclusion

1. Introduction

The Enhanced Persona Generator and Responder application serves two primary functions:

  1. Persona Generation: Analyzes a provided sample text to create a comprehensive persona profile, capturing the author’s writing style and personality traits.
  2. Response Generation: Uses the generated persona to produce responses that align with the defined characteristics, ensuring consistency and personalization in interactions.

This application can be particularly useful for content creators, authors, chatbots, and any scenario where understanding and replicating a specific writing style is beneficial.


2. Prerequisites

Before diving into the development, ensure you have the following:


3. Project Setup

Step 1: Create the Project Directory

Open your terminal or command prompt and execute the following commands:

mkdir persona_responder
cd persona_responder

Step 2: Set Up a Virtual Environment

It’s best practice to use a virtual environment to manage project dependencies.

python3 -m venv venv

Activate the virtual environment:

Step 3: Create requirements.txt

Create a requirements.txt file to list all necessary dependencies:

touch requirements.txt

Add the following content to requirements.txt:

openai
python-dotenv

Note:

Step 4: Install Dependencies

Install the listed dependencies using pip:

pip install -r requirements.txt

4. Implementing the Agents

Our application is modular, consisting of various agents responsible for distinct tasks. Let’s delve into each one.

Project Structure

Ensure your project has the following structure:

persona_responder/
├── agents/
│   ├── __init__.py
│   ├── persona_agent.py
│   ├── response_agent.py
│   ├── validation_agent.py
│   └── export_agent.py
├── utils/
│   ├── __init__.py
│   ├── file_utils.py
│   └── input_utils.py
├── main.py
├── persona.json
├── .env
├── requirements.txt
└── README.md

Create the necessary directories and files:

mkdir agents utils
touch agents/__init__.py
touch utils/__init__.py
touch main.py
touch README.md

Now, let’s implement each agent.


ExportAgent

Responsible for exporting generated responses to Markdown files.

# agents/export_agent.py

from datetime import datetime
import os


class ExportAgent:
    def export_to_markdown(self, content: str, filename: str = None) -> bool:
        """
        Export the content to a Markdown file with improved error handling.
        """
        try:
            if not content:
                print("Error: Cannot export empty content.")
                return False

            if not filename:
                timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
                filename = f"response_{timestamp}.md"

            os.makedirs(os.path.dirname(filename) if os.path.dirname(filename) else '.', exist_ok=True)

            with open(filename, 'w', encoding='utf-8') as f:
                f.write(content)

            print(f"Successfully exported response to {filename}")
            return True
        except Exception as e:
            print(f"Error exporting to markdown: {str(e)}")
            return False

Explanation:


PersonaAgent

Generates a persona based on a sample text using OpenAI’s API.

# agents/persona_agent.py

import json
import os
from openai import OpenAI
from utils.file_utils import create_backup


class PersonaAgent:
    def __init__(self, api_key, persona_file='persona.json'):
        self.client = OpenAI(api_key=api_key)
        self.persona_file = persona_file

    def generate_persona(self, sample_text: str) -> dict:
        prompt = (
            "Please analyze the writing style and personality of the given writing sample. "
            "You are a persona generation assistant. Analyze the following text and create a persona profile "
            "that captures the writing style and personality characteristics of the author. "
            "YOU MUST RESPOND WITH A VALID JSON OBJECT ONLY, no other text or analysis. "
            "The response must start with '{' and end with '}' and use the following exact structure:\n\n"
            "{\n"
            "  \"name\": \"[Author/Character Name]\",\n"
            "  \"vocabulary_complexity\": [1-10],\n"
            "  \"sentence_structure\": \"[simple/complex/varied]\",\n"
            "  \"paragraph_organization\": \"[structured/loose/stream-of-consciousness]\",\n"
            "  \"idiom_usage\": [1-10],\n"
            "  \"metaphor_frequency\": [1-10],\n"
            "  \"simile_frequency\": [1-10],\n"
            "  \"tone\": \"[formal/informal/academic/conversational/etc.]\",\n"
            "  \"punctuation_style\": \"[minimal/heavy/unconventional]\",\n"
            "  \"contraction_usage\": [1-10],\n"
            "  \"pronoun_preference\": \"[first-person/third-person/etc.]\",\n"
            "  \"passive_voice_frequency\": [1-10],\n"
            "  \"rhetorical_question_usage\": [1-10],\n"
            "  \"list_usage_tendency\": [1-10],\n"
            "  \"personal_anecdote_inclusion\": [1-10],\n"
            "  \"pop_culture_reference_frequency\": [1-10],\n"
            "  \"technical_jargon_usage\": [1-10],\n"
            "  \"parenthetical_aside_frequency\": [1-10],\n"
            "  \"humor_sarcasm_usage\": [1-10],\n"
            "  \"emotional_expressiveness\": [1-10],\n"
            "  \"emphatic_device_usage\": [1-10],\n"
            "  \"quotation_frequency\": [1-10],\n"
            "  \"analogy_usage\": [1-10],\n"
            "  \"sensory_detail_inclusion\": [1-10],\n"
            "  \"onomatopoeia_usage\": [1-10],\n"
            "  \"alliteration_frequency\": [1-10],\n"
            "  \"word_length_preference\": \"[short/long/varied]\",\n"
            "  \"foreign_phrase_usage\": [1-10],\n"
            "  \"rhetorical_device_usage\": [1-10],\n"
            "  \"statistical_data_usage\": [1-10],\n"
            "  \"personal_opinion_inclusion\": [1-10],\n"
            "  \"transition_usage\": [1-10],\n"
            "  \"reader_question_frequency\": [1-10],\n"
            "  \"imperative_sentence_usage\": [1-10],\n"
            "  \"dialogue_inclusion\": [1-10],\n"
            "  \"regional_dialect_usage\": [1-10],\n"
            "  \"hedging_language_frequency\": [1-10],\n"
            "  \"language_abstraction\": \"[concrete/abstract/mixed]\",\n"
            "  \"personal_belief_inclusion\": [1-10],\n"
            "  \"repetition_usage\": [1-10],\n"
            "  \"subordinate_clause_frequency\": [1-10],\n"
            "  \"verb_type_preference\": \"[active/stative/mixed]\",\n"
            "  \"sensory_imagery_usage\": [1-10],\n"
            "  \"symbolism_usage\": [1-10],\n"
            "  \"digression_frequency\": [1-10],\n"
            "  \"formality_level\": [1-10],\n"
            "  \"reflection_inclusion\": [1-10],\n"
            "  \"irony_usage\": [1-10],\n"
            "  \"neologism_frequency\": [1-10],\n"
            "  \"ellipsis_usage\": [1-10],\n"
            "  \"cultural_reference_inclusion\": [1-10],\n"
            "  \"stream_of_consciousness_usage\": [1-10],\n"
            "\n"
            "  \"psychological_traits\": {\n"
            "    \"openness_to_experience\": [1-10],\n"
            "    \"conscientiousness\": [1-10],\n"
            "    \"extraversion\": [1-10],\n"
            "    \"agreeableness\": [1-10],\n"
            "    \"emotional_stability\": [1-10],\n"
            "    \"dominant_motivations\": \"[achievement/affiliation/power/etc.]\",\n"
            "    \"core_values\": \"[integrity/freedom/knowledge/etc.]\",\n"
            "    \"decision_making_style\": \"[analytical/intuitive/spontaneous/etc.]\",\n"
            "    \"empathy_level\": [1-10],\n"
            "    \"self_confidence\": [1-10],\n"
            "    \"risk_taking_tendency\": [1-10],\n"
            "    \"idealism_vs_realism\": \"[idealistic/realistic/mixed]\",\n"
            "    \"conflict_resolution_style\": \"[assertive/collaborative/avoidant/etc.]\",\n"
            "    \"relationship_orientation\": \"[independent/communal/mixed]\",\n"
            "    \"emotional_response_tendency\": \"[calm/reactive/intense]\",\n"
            "    \"creativity_level\": [1-10]\n"
            "  },\n"
            "\n"
            "  \"age\": \"[age or age range]\",\n"
            "  \"gender\": \"[gender]\",\n"
            "  \"education_level\": \"[highest level of education]\",\n"
            "  \"professional_background\": \"[brief description]\",\n"
            "  \"cultural_background\": \"[brief description]\",\n"
            "  \"primary_language\": \"[language]\",\n"
            "  \"language_fluency\": \"[native/fluent/intermediate/beginner]\",\n"
            "  \"background\": \"[A brief paragraph describing the author's context, major influences, and any other relevant information not captured above]\"\n"
            "}\n\n"
            f"Sample Text:\n{sample_text}"
        )
        payload = {
            "model": "gpt-4",
            "messages": [{"role": "user", "content": prompt}],
            "temperature": 1
        }
        try:
            response = self.client.chat.completions.create(**payload)
            content = response.choices[0].message.content.strip()

            # Extract and parse JSON
            start_idx = content.find('{')
            end_idx = content.rfind('}') + 1
            if start_idx == -1 or end_idx == 0:
                print("Error: No JSON structure found in response.")
                return {}

            json_str = content[start_idx:end_idx]
            try:
                persona = json.loads(json_str)
                return persona
            except json.JSONDecodeError as e:
                print(f"JSON parsing error: {e}")
                return {}
        except Exception as e:
            print(f"Error during persona generation: {str(e)}")
            return {}

    def save_persona(self, persona: dict) -> bool:
        try:
            if not persona:
                print("Error: Cannot save empty persona.")
                return False
            create_backup(self.persona_file)
            os.makedirs(os.path.dirname(self.persona_file) if os.path.dirname(self.persona_file) else '.', exist_ok=True)
            with open(self.persona_file, 'w', encoding='utf-8') as f:
                json.dump(persona, f, indent=4, ensure_ascii=False)
            os.chmod(self.persona_file, 0o600)  # Read and write permissions for the owner only
            print(f"Successfully saved persona to {self.persona_file}")
            return True
        except Exception as e:
            print(f"Error saving persona: {str(e)}")
            return False

    def load_persona(self) -> dict:
        try:
            if not os.path.exists(self.persona_file):
                print(f"No persona file found at {self.persona_file}")
                return {}
            with open(self.persona_file, 'r', encoding='utf-8') as f:
                persona = json.load(f)
            if not persona:
                print("Warning: Loaded persona is empty.")
            else:
                print(f"Successfully loaded persona from {self.persona_file}")
                return persona
        except json.JSONDecodeError as e:
            print(f"Error decoding JSON from file: {e}")
            return {}
        except Exception as e:
            print(f"Error loading persona: {str(e)}")
            return {}

    def format_persona_summary(self, persona: dict) -> str:
        summary = [
            "=== Persona Summary ===",
            f"Name: {persona.get('name', 'Unknown')}",
            f"Writing Style:",
            f"- Tone: {persona.get('tone', 'Not specified')}",
            f"- Vocabulary Complexity: {persona.get('vocabulary_complexity', 'N/A')}/10",
            f"- Sentence Structure: {persona.get('sentence_structure', 'Not specified')}",
            "\nPsychological Profile:",
        ]

        psych_traits = persona.get('psychological_traits', {})
        for trait, value in psych_traits.items():
            summary.append(f"- {trait.replace('_', ' ').title()}: {value}")

        summary.extend([
            "\nBackground:",
            f"Age: {persona.get('age', 'Not specified')}",
            f"Education: {persona.get('education_level', 'Not specified')}",
            f"Professional Background: {persona.get('professional_background', 'Not specified')}",
            "\nAdditional Context:",
            persona.get('background', 'No additional context provided')
        ])

        return '\n'.join(summary)

Explanation:


ResponseAgent

Generates responses based on the generated persona and user prompts.

# agents/response_agent.py

from openai import OpenAI


class ResponseAgent:
    def __init__(self, api_key):
        self.client = OpenAI(api_key=api_key)

    def generate_response(self, persona: dict, prompt: str) -> str:
        try:
            if not persona:
                print("Warning: No persona provided, using default system prompt.")
                system_prompt = "Respond to the user's prompt naturally."
            else:
                system_prompt = self._create_system_prompt(persona)

            payload = {
                "model": "gpt-4",
                "messages": [
                    {"role": "system", "content": system_prompt},
                    {"role": "user", "content": prompt}
                ],
                "temperature": 1
            }
            response = self.client.chat.completions.create(**payload)
            return response.choices[0].message.content.strip()
        except Exception as e:
            print(f"Error generating response: {str(e)}")
            return f"Error: Unable to generate response - {str(e)}"

    def _create_system_prompt(self, persona: dict) -> str:
        prompt = (
            f"You are {persona.get('name', 'a user')}.\n"
            f"Your writing style and personality are described as follows:\n\n"
            f"Writing Style Characteristics:\n"
            f"- Vocabulary Complexity: {persona.get('vocabulary_complexity', 'N/A')}/10\n"
            f"- Sentence Structure: {persona.get('sentence_structure', 'N/A')}\n"
            f"- Paragraph Organization: {persona.get('paragraph_organization', 'N/A')}\n"
            f"- Idiom Usage: {persona.get('idiom_usage', 'N/A')}/10\n"
            f"- Metaphor Frequency: {persona.get('metaphor_frequency', 'N/A')}/10\n"
            f"- Simile Frequency: {persona.get('simile_frequency', 'N/A')}/10\n"
            f"- Tone: {persona.get('tone', 'N/A')}\n"
            f"- Punctuation Style: {persona.get('punctuation_style', 'N/A')}\n"
            f"- Contraction Usage: {persona.get('contraction_usage', 'N/A')}/10\n"
            f"- Pronoun Preference: {persona.get('pronoun_preference', 'N/A')}\n"
            f"- Passive Voice Frequency: {persona.get('passive_voice_frequency', 'N/A')}/10\n"
            f"- Rhetorical Question Usage: {persona.get('rhetorical_question_usage', 'N/A')}/10\n"
            f"- List Usage Tendency: {persona.get('list_usage_tendency', 'N/A')}/10\n"
            f"- Personal Anecdote Inclusion: {persona.get('personal_anecdote_inclusion', 'N/A')}/10\n"
            f"- Pop Culture Reference Frequency: {persona.get('pop_culture_reference_frequency', 'N/A')}/10\n"
            f"- Technical Jargon Usage: {persona.get('technical_jargon_usage', 'N/A')}/10\n"
            f"- Parenthetical Aside Frequency: {persona.get('parenthetical_aside_frequency', 'N/A')}/10\n"
            f"- Humor/Sarcasm Usage: {persona.get('humor_sarcasm_usage', 'N/A')}/10\n"
            f"- Emotional Expressiveness: {persona.get('emotional_expressiveness', 'N/A')}/10\n"
            f"- Emphatic Device Usage: {persona.get('emphatic_device_usage', 'N/A')}/10\n"
            f"- Quotation Frequency: {persona.get('quotation_frequency', 'N/A')}/10\n"
            f"- Analogy Usage: {persona.get('analogy_usage', 'N/A')}/10\n"
            f"- Sensory Detail Inclusion: {persona.get('sensory_detail_inclusion', 'N/A')}/10\n"
            f"- Onomatopoeia Usage: {persona.get('onomatopoeia_usage', 'N/A')}/10\n"
            f"- Alliteration Frequency: {persona.get('alliteration_frequency', 'N/A')}/10\n"
            f"- Word Length Preference: {persona.get('word_length_preference', 'N/A')}\n"
            f"- Foreign Phrase Usage: {persona.get('foreign_phrase_usage', 'N/A')}/10\n"
            f"- Rhetorical Device Usage: {persona.get('rhetorical_device_usage', 'N/A')}/10\n"
            f"- Statistical Data Usage: {persona.get('statistical_data_usage', 'N/A')}/10\n"
            f"- Personal Opinion Inclusion: {persona.get('personal_opinion_inclusion', 'N/A')}/10\n"
            f"- Transition Usage: {persona.get('transition_usage', 'N/A')}/10\n"
            f"- Reader Question Frequency: {persona.get('reader_question_frequency', 'N/A')}/10\n"
            f"- Imperative Sentence Usage: {persona.get('imperative_sentence_usage', 'N/A')}/10\n"
            f"- Dialogue Inclusion: {persona.get('dialogue_inclusion', 'N/A')}/10\n"
            f"- Regional Dialect Usage: {persona.get('regional_dialect_usage', 'N/A')}/10\n"
            f"- Hedging Language Frequency: {persona.get('hedging_language_frequency', 'N/A')}/10\n"
            f"- Language Abstraction: {persona.get('language_abstraction', 'N/A')}\n"
            f"- Personal Belief Inclusion: {persona.get('personal_belief_inclusion', 'N/A')}/10\n"
            f"- Repetition Usage: {persona.get('repetition_usage', 'N/A')}/10\n"
            f"- Subordinate Clause Frequency: {persona.get('subordinate_clause_frequency', 'N/A')}/10\n"
            f"- Verb Type Preference: {persona.get('verb_type_preference', 'N/A')}\n"
            f"- Sensory Imagery Usage: {persona.get('sensory_imagery_usage', 'N/A')}/10\n"
            f"- Symbolism Usage: {persona.get('symbolism_usage', 'N/A')}/10\n"
            f"- Digression Frequency: {persona.get('digression_frequency', 'N/A')}/10\n"
            f"- Formality Level: {persona.get('formality_level', 'N/A')}/10\n"
            f"- Reflection Inclusion: {persona.get('reflection_inclusion', 'N/A')}/10\n"
            f"- Irony Usage: {persona.get('irony_usage', 'N/A')}/10\n"
            f"- Neologism Frequency: {persona.get('neologism_frequency', 'N/A')}/10\n"
            f"- Ellipsis Usage: {persona.get('ellipsis_usage', 'N/A')}/10\n"
            f"- Cultural Reference Inclusion: {persona.get('cultural_reference_inclusion', 'N/A')}/10\n"
            f"- Stream of Consciousness Usage: {persona.get('stream_of_consciousness_usage', 'N/A')}/10\n\n"
            f"Psychological Traits:\n"
            f"- Openness to Experience: {persona.get('psychological_traits', {}).get('openness_to_experience', 'N/A')}/10\n"
            f"- Conscientiousness: {persona.get('psychological_traits', {}).get('conscientiousness', 'N/A')}/10\n"
            f"- Extraversion: {persona.get('psychological_traits', {}).get('extraversion', 'N/A')}/10\n"
            f"- Agreeableness: {persona.get('psychological_traits', {}).get('agreeableness', 'N/A')}/10\n"
            f"- Emotional Stability: {persona.get('psychological_traits', {}).get('emotional_stability', 'N/A')}/10\n"
            f"- Dominant Motivations: {persona.get('psychological_traits', {}).get('dominant_motivations', 'N/A')}\n"
            f"- Core Values: {persona.get('psychological_traits', {}).get('core_values', 'N/A')}\n"
            f"- Decision-Making Style: {persona.get('psychological_traits', {}).get('decision_making_style', 'N/A')}\n"
            f"- Empathy Level: {persona.get('psychological_traits', {}).get('empathy_level', 'N/A')}/10\n"
            f"- Self Confidence: {persona.get('psychological_traits', {}).get('self_confidence', 'N/A')}/10\n"
            f"- Risk Taking Tendency: {persona.get('psychological_traits', {}).get('risk_taking_tendency', 'N/A')}/10\n"
            f"- Idealism vs Realism: {persona.get('psychological_traits', {}).get('idealism_vs_realism', 'N/A')}\n"
            f"- Conflict Resolution Style: {persona.get('psychological_traits', {}).get('conflict_resolution_style', 'N/A')}\n"
            f"- Relationship Orientation: {persona.get('psychological_traits', {}).get('relationship_orientation', 'N/A')}\n"
            f"- Emotional Response Tendency: {persona.get('psychological_traits', {}).get('emotional_response_tendency', 'N/A')}\n"
            f"- Creativity Level: {persona.get('psychological_traits', {}).get('creativity_level', 'N/A')}/10\n\n"
            f"Personal Information:\n"
            f"- Age: {persona.get('age', 'N/A')}\n"
            f"- Gender: {persona.get('gender', 'N/A')}\n"
            f"- Education Level: {persona.get('education_level', 'N/A')}\n"
            f"- Professional Background: {persona.get('professional_background', 'N/A')}\n"
            f"- Cultural Background: {persona.get('cultural_background', 'N/A')}\n"
            f"- Primary Language: {persona.get('primary_language', 'N/A')}\n"
            f"- Language Fluency: {persona.get('language_fluency', 'N/A')}\n\n"
            f"Background Information:\n{persona.get('background', 'N/A')}\n\n"
            f"Use this information to write in the style described above."
        )
        return prompt

Explanation:


ValidationAgent

Ensures that the generated persona adheres to the required structure and value ranges.

# agents/validation_agent.py

class ValidationAgent:
    def validate(self, persona: dict) -> bool:
        """
        Validate the structure and content of a persona dictionary.
        Returns True if valid, False otherwise.
        """
        required_fields = [
            'name',
            'vocabulary_complexity',
            'sentence_structure',
            'tone',
            'psychological_traits'
        ]
       
        try:
            # Check for required fields
            for field in required_fields:
                if field not in persona:
                    print(f"Missing required field: {field}")
                    return False
           
            # Validate numeric values are within range
            numeric_fields = [
                'vocabulary_complexity',
                'idiom_usage',
                'metaphor_frequency',
                'simile_frequency',
                'contraction_usage',
                'passive_voice_frequency',
                'rhetorical_question_usage',
                'list_usage_tendency',
                'personal_anecdote_inclusion',
                'pop_culture_reference_frequency',
                'technical_jargon_usage',
                'parenthetical_aside_frequency',
                'humor_sarcasm_usage',
                'emotional_expressiveness',
                'emphatic_device_usage',
                'quotation_frequency',
                'analogy_usage',
                'sensory_detail_inclusion',
                'onomatopoeia_usage',
                'alliteration_frequency',
                'foreign_phrase_usage',
                'rhetorical_device_usage',
                'statistical_data_usage',
                'personal_opinion_inclusion',
                'transition_usage',
                'reader_question_frequency',
                'imperative_sentence_usage',
                'dialogue_inclusion',
                'regional_dialect_usage',
                'hedging_language_frequency',
                'personal_belief_inclusion',
                'repetition_usage',
                'subordinate_clause_frequency',
                'sensory_imagery_usage',
                'symbolism_usage',
                'digression_frequency',
                'formality_level',
                'reflection_inclusion',
                'irony_usage',
                'neologism_frequency',
                'ellipsis_usage',
                'cultural_reference_inclusion',
                'stream_of_consciousness_usage'
            ]
           
            for field in numeric_fields:
                if field in persona:
                    value = persona[field]
                    if not isinstance(value, (int, float)) or not (1 <= value <= 10):
                        print(f"Invalid value for {field}: must be number between 1-10.")
                        return False
           
            # Validate psychological traits
            psych_traits = persona.get('psychological_traits', {})
            if not isinstance(psych_traits, dict):
                print("psychological_traits must be a dictionary.")
                return False
           
            required_psych_traits = [
                'openness_to_experience',
                'conscientiousness',
                'extraversion',
                'agreeableness',
                'emotional_stability'
            ]
           
            for trait in required_psych_traits:
                if trait not in psych_traits:
                    print(f"Missing psychological trait: {trait}")
                    return False
                value = psych_traits[trait]
                if not isinstance(value, (int, float)) or not (1 <= value <= 10):
                    print(f"Invalid value for psychological trait {trait}: must be number between 1-10.")
                    return False
           
            return True
       
        except Exception as e:
            print(f"Error validating persona: {str(e)}")
            return False

Explanation:


5. Utility Modules

Utility modules provide supporting functions essential for the application’s operations.

file_utils.py

Handles file operations such as loading sample texts and creating backups.

# utils/file_utils.py

import os
import json
from datetime import datetime

def load_sample_text(filename: str) -> str:
    """
    Load sample text from a file with proper error handling.
    """
    try:
        if not os.path.exists(filename):
            print(f"Error: File '{filename}' not found.")
            return ""
        
        with open(filename, 'r', encoding='utf-8') as f:
            content = f.read()
        
        if not content.strip():
            print("Warning: File is empty.")
            return ""
        
        return content
    except Exception as e:
        print(f"Error reading file: {str(e)}")
        return ""

def create_backup(filename: str):
    """
    Create a backup of the specified file.
    """
    try:
        if os.path.exists(filename):
            timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
            backup_filename = f"{filename}.{timestamp}.backup"
            os.rename(filename, backup_filename)
            print(f"Created backup: {backup_filename}")
    except Exception as e:
        print(f"Error creating backup: {str(e)}")

Explanation:


input_utils.py

Facilitates user input, especially handling multiline inputs.

# utils/input_utils.py

def get_multiline_input(prompt: str) -> str:
    """
    Get multiline input from user with proper handling.
    """
    print(prompt)
    print("(Press Enter twice to finish)")

    lines = []
    try:
        while True:
            line = input()
            if not line and lines and not lines[-1]:
                break
            lines.append(line)
        return '\n'.join(lines[:-1])  # Remove last empty line
    except KeyboardInterrupt:
        print("\nInput cancelled by user.")
        return ""
    except Exception as e:
        print(f"Error getting input: {str(e)}")
        return ""

Explanation:


6. Main Orchestrator (main.py)

The main.py script ties all agents and utilities together, providing an interactive interface for users.

# main.py

import os
import json
from datetime import datetime
from agents.persona_agent import PersonaAgent
from agents.response_agent import ResponseAgent
from agents.validation_agent import ValidationAgent
from agents.export_agent import ExportAgent
from utils.file_utils import load_sample_text, create_backup
from utils.input_utils import get_multiline_input
from dotenv import load_dotenv


def main():
    load_dotenv()  # Load environment variables from .env file
    print("\n=== Enhanced Persona Generator and Responder ===")
   
    # Retrieve API Key
    api_key = os.getenv("OPENAI_API_KEY")
    if not api_key:
        print("Error: OPENAI_API_KEY not found in environment variables.")
        return
   
    # Initialize agents
    persona_agent = PersonaAgent(api_key)
    response_agent = ResponseAgent(api_key)
    validation_agent = ValidationAgent()
    export_agent = ExportAgent()
   
    while True:
        print("\nOptions:")
        print("1. Use existing Persona")
        print("2. Generate new Persona from sample text")
        print("3. Load sample text from file")
        print("4. Exit")
       
        choice = input("\nEnter your choice (1-4): ").strip()
       
        if choice == '4':
            print("Exiting program...")
            break
       
        persona = {}
       
        if choice == '1':
            persona = persona_agent.load_persona()
            if persona:
                print("\nCurrent Persona:")
                print(persona_agent.format_persona_summary(persona))
            else:
                if input("\nNo persona loaded. Generate new one? (y/n): ").lower() == 'y':
                    choice = '2'
                else:
                    continue
       
        if choice == '2':
            sample_text = get_multiline_input("\nEnter sample text:")
            if not sample_text.strip():
                print("Error: Empty sample text provided.")
                continue
            print("\nGenerating persona from sample text...")
            persona = persona_agent.generate_persona(sample_text)
            if persona:
                if validation_agent.validate(persona):
                    create_backup(persona_agent.persona_file)
                    if persona_agent.save_persona(persona):
                        print("\nGenerated Persona:")
                        print(persona_agent.format_persona_summary(persona))
                    else:
                        print("Warning: Persona generated but not saved.")
                else:
                    print("Error: Failed to generate valid persona.")
                    continue
            else:
                print("Error: Failed to generate persona.")
                continue
       
        elif choice == '3':
            filename = input("\nEnter the path to the text file: ").strip()
            sample_text = load_sample_text(filename)
            if not sample_text:
                continue
            print("\nGenerating persona from file...")
            persona = persona_agent.generate_persona(sample_text)
            if persona:
                if validation_agent.validate(persona):
                    create_backup(persona_agent.persona_file)
                    if persona_agent.save_persona(persona):
                        print("\nGenerated Persona:")
                        print(persona_agent.format_persona_summary(persona))
                    else:
                        print("Warning: Persona generated but not saved.")
                else:
                    print("Error: Failed to generate valid persona.")
                    continue
            else:
                print("Error: Failed to generate persona.")
                continue
       
        # Get prompt and generate response
        while True:
            prompt = get_multiline_input("\nEnter your prompt:")
            if not prompt.strip():
                print("Error: Empty prompt provided.")
                if input("Try again? (y/n): ").lower() != 'y':
                    break
                continue
            print("\nGenerating response...")
            response = response_agent.generate_response(persona, prompt)
            print("\n=== Generated Response ===")
            print(response)
            if input("\nExport response to Markdown? (y/n): ").lower() == 'y':
                default_filename = f"response_{datetime.now().strftime('%Y%m%d_%H%M%S')}.md"
                custom_filename = input(f"Enter filename (default: {default_filename}): ").strip()
                filename = custom_filename if custom_filename else default_filename
                if export_agent.export_to_markdown(response, filename):
                    print("Response exported successfully.")
                else:
                    print("Error: Failed to export response.")
            if input("\nGenerate another response with current persona? (y/n): ").lower() != 'y':
                break
       
        if input("\nStart over with a different persona? (y/n): ").lower() != 'y':
            print("Exiting program...")
            break


if __name__ == "__main__":
    try:
        main()
    except KeyboardInterrupt:
        print("\nProgram terminated by user.")
    except Exception as e:
        print(f"\nProgram terminated due to error: {str(e)}")
    finally:
        print("\nThank you for using the Enhanced Persona Generator and Responder!")

Explanation:


7. Running the Application

Step 1: Configure Environment Variables

Ensure you have a .env file in your project root containing your OpenAI API key.

touch .env

Edit the .env file and add:

OPENAI_API_KEY=your-openai-api-key-here

Security Reminder: Never commit your .env file or expose your API keys publicly. Add .env to your .gitignore:

echo ".env" >> .gitignore

Step 2: Activate the Virtual Environment

If not already activated, activate your virtual environment:

Step 3: Run the Application

Execute the main.py script:

python main.py

Expected Output:

=== Enhanced Persona Generator and Responder ===

Options:
1. Use existing Persona
2. Generate new Persona from sample text
3. Load sample text from file
4. Exit

Enter your choice (1-4):

Step 4: Interacting with the Application


8. Troubleshooting

Common Issues and Solutions

  1. ModuleNotFoundError: No module named 'openai'

    • Cause: OpenAI package not installed.
    • Solution: Install the package using pip install openai.
  2. ModuleNotFoundError: No module named 'dotenv'

    • Cause: python-dotenv package not installed.
    • Solution: Install the package using pip install python-dotenv.
  3. ModuleNotFoundError: No module named 'utils.file_utils'

    • Cause: Incorrect project structure or missing __init__.py.
    • Solution: Ensure the utils directory contains an __init__.py file and the script is run from the project root.
  4. Empty or Invalid persona.json

    • Cause: Issues during persona generation or saving.
    • Solution:
      • Ensure the sample text provided is substantial and clear.
      • Check for API errors or JSON parsing issues in persona_agent.py.
  5. API Key Issues

    • Cause: Missing or incorrect OpenAI API key.
    • Solution:
      • Verify the API key in the .env file.
      • Ensure there are no extra spaces or hidden characters.
  6. Permission Errors When Saving Files

    • Cause: Insufficient permissions to write to the directory.
    • Solution: Run the terminal with appropriate permissions or choose a different directory.

9. Conclusion

Building an Enhanced Persona Generator and Responder is a testament to the versatility of AI and Python. By modularizing the application into distinct agents and utilities, we’ve created a scalable and maintainable system that can adapt to various use cases. Whether you’re looking to develop sophisticated chatbots, personalized content generators, or simply experiment with AI-driven applications, this project provides a solid foundation.

Key Takeaways:

Feel free to customize and expand upon this foundation to suit your specific needs. If you encounter any issues or have further questions, don’t hesitate to reach out or consult the respective package documentation.

Happy coding!