Skip to main content

SPOF (Structured Prompt Output Framework) is an open-source Python framework for designing structured, maintainable, and type-safe prompts for Large Language Models (LLMs

Project description

SPOF - Structured Prompt Output Framework

SPOF (Structured Prompt Output Framework) is an open-source Python framework for designing structured, maintainable, and type-safe prompts for Large Language Models (LLMs).

Think of it as “Pydantic for prompts” — SPOF lets you model prompts as composable data structures, enforce schema validation, and render them into multiple formats (JSON, XML, Markdown, plain text) without losing intent.

With SPOF, prompts become:

  • Composable – Build reusable prompt blocks (e.g., personality, safety rules, context) and combine them like Lego pieces.
  • Type-safe – Leverage Python’s typing and Pydantic validation to guarantee correct structures before sending to an LLM.
  • Multi-format – Export the same structured prompt to any output format required by different providers or APIs.
  • Maintainable – Treat prompts as code, versioned and auditable, instead of fragile strings.

SPOF provides the missing infrastructure layer for prompt engineering at scale, turning prompts into reliable, testable, and reusable components.

🚀 Quick Start

Installation

pip install spof

Basic Example

from spof import InstructionBlock, Text, Items

class SimplePrompt(InstructionBlock):
    instruction: Text
    requirements: Items
    
    def __init__(self):
        super().__init__(
            instruction=Text("Analyze the following data carefully"),
            requirements=Items([
                "Be thorough and accurate",
                "Include specific examples", 
                "Provide clear conclusions"
            ])
        )

# Create and render
prompt = SimplePrompt()
print(prompt.to_xml())

Output:

<simple_prompt>
  <instruction>Analyze the following data carefully</instruction>
  <requirements>
    - Be thorough and accurate
    - Include specific examples  
    - Provide clear conclusions
  </requirements>
</simple_prompt>

📋 Core Components

1. InstructionBlock

The base class for all prompt components. Inherit from this to create custom blocks.

class MyBlock(InstructionBlock):
    title: str
    content: str
    
    def __init__(self, title: str, content: str):
        super().__init__(title=title, content=content)

2. Text Block

For simple text content with optional custom block names.

# Simple text
intro = Text("Welcome to our AI assistant")

# With custom block name
intro = Text("Welcome to our AI assistant", block_name="greeting")

3. Items Block

For lists of items, rendered as bullet points.

rules = Items([
    "Be respectful and helpful",
    "Provide accurate information",
    "Ask for clarification when needed"
], block_name="guidelines")

4. ModelBlock & wrap_model()

Automatically wrap any Pydantic model to make it renderable.

from pydantic import BaseModel

class UserProfile(BaseModel):
    name: str
    age: int
    role: str

user = UserProfile(name="Alice", age=30, role="Engineer")

# Wrap for rendering
user_block = wrap_model(user, block_name="user_info")
print(user_block.to_xml())

Output:

<user_info>
  <name>Alice</name>
  <age>30</age>
  <role>Engineer</role>
</user_info>

🎨 Rendering Formats

SPOF can render the same prompt structure in three formats:

XML Format (Default)

Perfect for Claude and other models that work well with structured XML:

prompt.to_xml()
# or
prompt.render(RenderFormat.XML)

Markdown Format

Great for models that prefer markdown structure:

prompt.to_markdown()
# or  
prompt.render(RenderFormat.MARKDOWN)

JSON Format

Useful for API calls and structured data exchange:

prompt.to_json()
# or
prompt.render(RenderFormat.JSON)  

🏗️ Building Complex Prompts

Nested Structures

SPOF handles nested blocks automatically:

class AnalysisPrompt(InstructionBlock):
    role: Text
    context: UserContext  # Another InstructionBlock
    instructions: InstructionSet  # Another InstructionBlock
    examples: List[ExampleCase]  # List of Pydantic models
    
    def __init__(self, user_data, examples):
        super().__init__(
            role=Text("You are a data analyst"),
            context=UserContext(user_data),
            instructions=InstructionSet(),
            examples=[wrap_model(ex) for ex in examples]
        )

Working with Pydantic Models

SPOF seamlessly integrates with existing Pydantic models:

from pydantic import BaseModel
from typing import List, Literal

class Message(BaseModel):
    sender: Literal["User", "Assistant"] 
    timestamp: datetime
    content: str

class ConversationContext(InstructionBlock):
    messages: List[Message]
    user_id: str
    
    def __init__(self, messages: List[Message], user_id: str):
        super().__init__(messages=messages, user_id=user_id)

# Messages are automatically wrapped in ModelBlocks when rendered

Custom Block Names

Control how your blocks appear in the output:

# Class-level naming
class UserInstructions(InstructionBlock):
    __block_name__ = "custom_instructions"
    
# Instance-level naming  
intro = Text("Hello", block_name="greeting")
rules = Items(["Rule 1", "Rule 2"], block_name="policies")

📱 Complete Example: Chatbot Prompt

Here's a full example showing how to build a structured chatbot prompt:

from typing import List, Optional, Literal
from datetime import datetime
from pydantic import BaseModel
from spof import InstructionBlock, Text, Items

class ChatMessage(BaseModel):
    """Individual chat message with sender, timestamp, and content"""
    sender: Literal["User", "Assistant"]
    timestamp: datetime
    content: str

class PersonalityBlock(InstructionBlock):
    """Define the chatbot's personality and behavior"""
    name: str
    traits: List[str]
    communication_style: str

    def __init__(self):
        super().__init__(
            name="Alex",
            traits=[
                "Friendly and approachable",
                "Helpful and proactive", 
                "Curious and engaging",
                "Professional but warm"
            ],
            communication_style="Conversational, clear, and empathetic. Use 'I' naturally when speaking."
        )

class DirectionsBlock(InstructionBlock):
    """Clear instructions for the chatbot"""
    primary_goals: Items
    response_guidelines: Items
    safety_rules: Items

    def __init__(self):
        super().__init__(
            primary_goals=Items([
                "Listen carefully to understand user needs",
                "Provide helpful, accurate information", 
                "Ask clarifying questions when needed",
                "Maintain a friendly, professional tone"
            ], block_name="goals"),
            
            response_guidelines=Items([
                "Keep responses concise but thorough",
                "Use examples when helpful",
                "Acknowledge when you don't know something", 
                "Offer follow-up suggestions"
            ], block_name="guidelines"),
            
            safety_rules=Items([
                "Never provide harmful or dangerous advice",
                "Protect user privacy and data",
                "Be respectful of all individuals and groups",
                "Decline inappropriate requests politely"
            ], block_name="safety")
        )

class ConversationHistoryBlock(InstructionBlock):
    """Recent conversation messages for context"""
    messages: List[ChatMessage]
    total_messages: int

    def __init__(self, messages: List[ChatMessage]):
        super().__init__(
            messages=messages[-10:],  # Keep last 10 messages
            total_messages=len(messages)
        )

class ChatbotPrompt(InstructionBlock):
    """Complete chatbot prompt structure"""
    introduction: Text
    personality: PersonalityBlock
    directions: DirectionsBlock
    conversation_history: Optional[ConversationHistoryBlock]
    current_context: Text
    final_instruction: Text

    def __init__(self, user_message: str, conversation_history: Optional[List[ChatMessage]] = None):
        super().__init__(
            introduction=Text(
                "You are Alex, a helpful AI assistant. Respond naturally and helpfully to user messages.",
                block_name="role"
            ),
            
            personality=PersonalityBlock(),
            directions=DirectionsBlock(), 
            
            conversation_history=(
                ConversationHistoryBlock(conversation_history) 
                if conversation_history else None
            ),
            
            current_context=Text(f"User's current message: {user_message}", block_name="current_request"),
            
            final_instruction=Text(
                "Based on the user's message and conversation context, provide a helpful, "
                "friendly response that follows your personality and guidelines.",
                block_name="task"
            )
        )

# Usage
user_input = "Hi! Can you help me plan a weekend trip to Paris?"

chat_history = [
    ChatMessage(
        sender="User",
        timestamp=datetime(2025, 9, 6, 14, 30, 0),
        content="Hello there!"
    ),
    ChatMessage(
        sender="Assistant", 
        timestamp=datetime(2025, 9, 6, 14, 30, 5),
        content="Hi! I'm Alex, your helpful assistant. How can I help you today?"
    ),
    ChatMessage(
        sender="User",
        timestamp=datetime(2025, 9, 6, 14, 31, 0), 
        content="I'm looking for travel advice"
    ),
    ChatMessage(
        sender="Assistant",
        timestamp=datetime(2025, 9, 6, 14, 31, 3),
        content="I'd love to help with travel planning! What destination are you considering?"
    )
]

prompt = ChatbotPrompt(user_input, chat_history)

Output (XML Format):

<chatbot_prompt>
  <introduction>You are Alex, a helpful AI assistant. Respond naturally and helpfully to user messages.</introduction>
  <personality_block>
    <name>Alex</name>
    <traits>
      - Friendly and approachable
      - Helpful and proactive
      - Curious and engaging
      - Professional but warm
    </traits>
    <communication_style>Conversational, clear, and empathetic. Use 'I' naturally when speaking.</communication_style>
  </personality_block>
  <directions_block>
    <goals>
      - Listen carefully to understand user needs
      - Provide helpful, accurate information
      - Ask clarifying questions when needed
      - Maintain a friendly, professional tone
    </goals>
    <guidelines>
      - Keep responses concise but thorough
      - Use examples when helpful
      - Acknowledge when you don't know something
      - Offer follow-up suggestions
    </guidelines>
    <safety>
      - Never provide harmful or dangerous advice
      - Protect user privacy and data
      - Be respectful of all individuals and groups
      - Decline inappropriate requests politely
    </safety>
  </directions_block>
  <conversation_history_block>
    <messages>
      <chat_message>
        <sender>User</sender>
        <timestamp>2025-09-06 14:30:00</timestamp>
        <content>Hello there!</content>
      </chat_message>
      <chat_message>
        <sender>Assistant</sender>
        <timestamp>2025-09-06 14:30:05</timestamp>
        <content>Hi! I'm Alex, your helpful assistant. How can I help you today?</content>
      </chat_message>
      <chat_message>
        <sender>User</sender>
        <timestamp>2025-09-06 14:31:00</timestamp>
        <content>I'm looking for travel advice</content>
      </chat_message>
      <chat_message>
        <sender>Assistant</sender>
        <timestamp>datetime(2025, 9, 6, 14, 31, 3)</timestamp>
        <content>I'd love to help with travel planning! What destination are you considering?</content>
      </chat_message>
    </messages>
    <total_messages>4</total_messages>
  </conversation_history_block>
  <current_request>User's current message: Hi! Can you help me plan a weekend trip to Paris?</current_request>
  <task>Based on the user's message and conversation context, provide a helpful, friendly response that follows your personality and guidelines.</task>
</chatbot_prompt>

🛠️ Advanced Features

Field Exclusion

Exclude certain fields from rendering:

user_block = wrap_model(user_model, exclude_fields=["password", "internal_id"])

Custom Rendering Logic

Override rendering for specific block types:

class CustomBlock(InstructionBlock):
    def render(self, format: RenderFormat = None, indent_level: int = 0) -> str:
        # Custom rendering logic
        if format == RenderFormat.XML:
            return "<custom>My custom XML</custom>"
        return super().render(format, indent_level)

Runtime Block Names

Change block names at runtime:

dynamic_block = Text("Content", block_name=f"section_{section_id}")

🔧 Best Practices

  1. Separate Structure from Content: Define your prompt structure once, reuse everywhere
  2. Use Type Hints: Leverage Pydantic's validation and IDE support
  3. Compose Prompts: Build complex prompts from simple, reusable blocks
  4. Test Different Formats: Same structure works for XML, Markdown, and JSON
  5. Version Control Friendly: Prompt changes are clear in diffs

📚 API Reference

Core Classes

  • InstructionBlock: Base class for all prompt blocks
  • Text: Simple text content block
  • Items: List/bullet point block
  • ModelBlock: Wrapper for Pydantic models
  • RenderFormat: Enum for output formats (XML, MARKDOWN, JSON)

Key Methods

  • render(format, indent_level): Render block in specified format
  • to_xml(): Convenience method for XML output
  • to_markdown(): Convenience method for Markdown output
  • to_json(): Convenience method for JSON output
  • to_struct(): Convert to dictionary structure

🤝 Contributing

All contibutions are welcomed.


SPOF makes building structured prompts as easy as defining Pydantic models, while giving you the flexibility to render them in whatever format your LLM prefers. Build once, render everywhere! 🚀

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

spof-0.1.1.tar.gz (14.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

spof-0.1.1-py3-none-any.whl (10.6 kB view details)

Uploaded Python 3

File details

Details for the file spof-0.1.1.tar.gz.

File metadata

  • Download URL: spof-0.1.1.tar.gz
  • Upload date:
  • Size: 14.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.8.8

File hashes

Hashes for spof-0.1.1.tar.gz
Algorithm Hash digest
SHA256 72b44f766167cf536b170f82dd7506761f0a8ca7624f93508f9da469b899fedf
MD5 7c98c2bb6ff7e4a267b5617ede29e8a3
BLAKE2b-256 4785d7c4da51ee8011200620b1e77a5424e9e9e2fcc58804fac0bc356dbc0ae8

See more details on using hashes here.

File details

Details for the file spof-0.1.1-py3-none-any.whl.

File metadata

  • Download URL: spof-0.1.1-py3-none-any.whl
  • Upload date:
  • Size: 10.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.8.8

File hashes

Hashes for spof-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 9de7e5585edcd947e6f413c601344e9792c10fda60efe73ab95d70b7f74e7f8c
MD5 23e513dda771850bb9809573c7a3b695
BLAKE2b-256 83ff54d55f52e919e5d6fa45f58ceeadad835182316c619097f789a7a7643bda

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page