Skip to content

Architecture

PromptManager follows a modular, layered architecture designed for flexibility, extensibility, and maintainability.

High-Level Architecture

graph TB
    subgraph "Application Layer"
        APP[Your Application]
        CLI[CLI Tools]
        WEB[Web Interface]
    end

    subgraph "Core Layer"
        PROMPT[PromptManager::Prompt]
        DP[DirectiveProcessor]
        ERB[ERB Engine]
        PARAM[Parameter Manager]
    end

    subgraph "Storage Layer"
        SA[Storage Adapter Interface]
        FS[FileSystemAdapter]
        AR[ActiveRecordAdapter] 
        CUSTOM[Custom Adapters]
    end

    subgraph "Data Layer"
        FILES[File System]
        DB[Database]
        API[External APIs]
    end

    APP --> PROMPT
    CLI --> PROMPT
    WEB --> PROMPT

    PROMPT --> DP
    PROMPT --> ERB
    PROMPT --> PARAM
    PROMPT --> SA

    SA --> FS
    SA --> AR
    SA --> CUSTOM

    FS --> FILES
    AR --> DB
    CUSTOM --> API

Core Components

1. Prompt Class

The central PromptManager::Prompt class orchestrates all functionality:

class PromptManager::Prompt
  attr_accessor :id, :parameters, :context
  attr_reader :keywords, :directives

  def initialize(options = {})
    @id = options[:id]
    @context = options[:context] || []
    @directives_processor = options[:directives_processor]
    @erb_flag = options[:erb_flag] || false
    @envar_flag = options[:envar_flag] || false
  end

  def to_s
    # Processing pipeline
    text = load_raw_text
    text = process_directives(text)
    text = process_erb(text) if @erb_flag
    text = substitute_envars(text) if @envar_flag
    text = substitute_parameters(text)
    text
  end
end

2. Storage Adapter Pattern

Storage adapters implement a common interface:

classDiagram
    class StorageAdapter {
        <<interface>>
        +get(id) String, Hash
        +save(id, text, params) void
        +delete(id) void
        +list() Array~String~
        +search(query) Array~String~
    }

    class FileSystemAdapter {
        +prompts_dir Path
        +prompt_extension String
        +params_extension String
        +search_proc Proc
        +get(id) String, Hash
        +save(id, text, params) void
    }

    class ActiveRecordAdapter {
        +model Class
        +id_column Symbol
        +text_column Symbol  
        +parameters_column Symbol
        +get(id) String, Hash
        +save(id, text, params) void
    }

    StorageAdapter <|-- FileSystemAdapter
    StorageAdapter <|-- ActiveRecordAdapter

3. Processing Pipeline

The prompt processing follows a well-defined pipeline:

graph LR
    A[Raw Text] --> B[Parse Comments]
    B --> C[Extract Keywords]
    C --> D[Process Directives]
    D --> E[Apply ERB]
    E --> F[Substitute Envars]
    F --> G[Replace Parameters]
    G --> H[Final Text]

    subgraph "Error Handling"
        I[Validation]
        J[Error Recovery]
    end

    C --> I
    D --> I
    E --> I
    F --> I
    G --> I
    I --> J

Design Patterns

1. Adapter Pattern

Storage adapters use the Adapter pattern to provide a consistent interface across different storage backends:

# Common interface
module StorageAdapter
  def get(id)
    raise NotImplementedError
  end

  def save(id, text, parameters)  
    raise NotImplementedError
  end
end

# Specific implementations
class FileSystemAdapter
  include StorageAdapter

  def get(id)
    text = File.read(prompt_path(id))
    params = JSON.parse(File.read(params_path(id)))
    [text, params]
  end
end

2. Strategy Pattern

Directive processing uses the Strategy pattern for different directive types:

class DirectiveProcessor
  def process_directive(directive, prompt)
    case directive
    when /^\/\/include (.+)$/
      IncludeStrategy.new.process($1, prompt)
    when /^\/\/import (.+)$/  
      ImportStrategy.new.process($1, prompt)
    else
      raise "Unknown directive: #{directive}"
    end
  end
end

3. Template Method Pattern

The prompt generation uses Template Method pattern:

class Prompt
  def to_s
    template_method
  end

  private

  def template_method
    text = load_text          # Hook 1
    text = preprocess(text)   # Hook 2  
    text = process(text)      # Hook 3
    text = postprocess(text)  # Hook 4
    text
  end

  # Hooks can be overridden by subclasses
  def preprocess(text); text; end
  def process(text); substitute_parameters(text); end
  def postprocess(text); text; end
end

4. Configuration Object Pattern

Storage adapters use configuration objects for flexible setup:

class FileSystemAdapter
  attr_reader :config

  def self.config(&block)
    @config ||= Configuration.new
    block.call(@config) if block_given?
    self
  end

  class Configuration  
    attr_accessor :prompts_dir, :prompt_extension, :params_extension

    def initialize
      @prompts_dir = nil
      @prompt_extension = '.txt'
      @params_extension = '.json'
    end
  end
end

Data Flow

1. Prompt Loading

sequenceDiagram
    participant App as Application
    participant P as Prompt
    participant SA as StorageAdapter
    participant FS as FileSystem

    App->>P: new(id: 'greeting')
    P->>SA: get('greeting')
    SA->>FS: read greeting.txt
    FS-->>SA: raw text
    SA->>FS: read greeting.json  
    FS-->>SA: parameters
    SA-->>P: text, parameters
    P-->>App: Prompt instance

2. Prompt Processing

sequenceDiagram
    participant App as Application
    participant P as Prompt
    participant DP as DirectiveProcessor
    participant ERB as ERB Engine

    App->>P: to_s()
    P->>P: extract_keywords()
    P->>DP: process_directives(text)
    DP-->>P: processed text
    P->>ERB: process_erb(text)
    ERB-->>P: templated text
    P->>P: substitute_parameters()
    P-->>App: final text

3. Parameter Management

graph TD
    A[Set Parameters] --> B{New Format?}
    B -->|v0.3.0+| C[Store as Array]
    B -->|Legacy| D[Convert to Array]
    C --> E[Parameter History]
    D --> E
    E --> F[Get Latest Value]
    E --> G[Get Full History]
    F --> H[Parameter Substitution]
    G --> I[UI Dropdowns]

Extension Points

The architecture provides several extension points for customization:

1. Custom Storage Adapters

class RedisAdapter
  include PromptManager::StorageAdapter

  def initialize(redis_client)
    @redis = redis_client
  end

  def get(id)
    text = @redis.get("prompt:#{id}:text")
    params_json = @redis.get("prompt:#{id}:params")
    params = JSON.parse(params_json || '{}')
    [text, params]
  end
end

2. Custom Directive Processors

class CustomDirectiveProcessor < PromptManager::DirectiveProcessor
  def process_directive(directive, prompt)
    case directive
    when /^\/\/model (.+)$/
      @model = $1
      ""  # Remove directive from output
    when /^\/\/temperature (.+)$/
      @temperature = $1.to_f
      ""
    else
      super  # Delegate to parent
    end
  end
end

3. Custom Keyword Patterns

# Support multiple keyword formats
class MultiPatternPrompt < PromptManager::Prompt
  PATTERNS = [
    /(\[[A-Z _|]+\])/,           # [KEYWORD]
    /(\{\{[a-z_]+\}\})/,         # {{keyword}}  
    /(:[a-z_]+)/                 # :keyword
  ]

  def extract_keywords(text)
    keywords = []
    PATTERNS.each do |pattern|
      keywords.concat(text.scan(pattern).flatten)
    end
    keywords.uniq
  end
end

Performance Considerations

1. Lazy Loading

class Prompt
  def keywords
    @keywords ||= extract_keywords(@raw_text)
  end

  def raw_text
    @raw_text ||= storage_adapter.get(@id).first
  end
end

2. Caching

class CachingStorageAdapter
  def initialize(adapter, cache_store = {})
    @adapter = adapter
    @cache = cache_store
  end

  def get(id)
    @cache[id] ||= @adapter.get(id)
  end
end

3. Batch Operations

class BatchProcessor  
  def process_prompts(prompt_ids)
    # Load all prompts at once
    prompts_data = storage_adapter.get_batch(prompt_ids)

    # Process in parallel
    results = prompts_data.map do |id, (text, params)|
      Thread.new { process_single_prompt(id, text, params) }
    end.map(&:value)

    results
  end
end

Error Handling Architecture

graph TD
    A[Operation] --> B{Success?}
    B -->|Yes| C[Return Result]
    B -->|No| D[Classify Error]
    D --> E{Error Type}
    E -->|Storage| F[StorageError]
    E -->|Parameter| G[ParameterError]
    E -->|Configuration| H[ConfigurationError]
    E -->|Processing| I[ProcessingError]

    F --> J[Log & Report]
    G --> J
    H --> J  
    I --> J

    J --> K{Recoverable?}
    K -->|Yes| L[Apply Recovery Strategy]
    K -->|No| M[Propagate Error]

    L --> A

Error Hierarchy

module PromptManager
  class Error < StandardError; end

  class StorageError < Error
    attr_reader :operation, :id

    def initialize(message, operation: nil, id: nil)
      super(message)
      @operation = operation
      @id = id
    end
  end

  class ParameterError < Error
    attr_reader :missing_params, :invalid_params
  end

  class ConfigurationError < Error
    attr_reader :setting, :value
  end

  class ProcessingError < Error
    attr_reader :stage, :directive
  end
end

Security Considerations

1. Parameter Sanitization

class SecurePrompt < Prompt
  def substitute_parameters(text)
    safe_params = sanitize_parameters(@parameters)
    super(text, safe_params)
  end

  private

  def sanitize_parameters(params)
    params.transform_values do |value|
      # Remove potential injection attacks
      value.gsub(/[<>'"&]/, '')
    end
  end
end

2. File System Security

class SecureFileSystemAdapter < FileSystemAdapter
  def prompt_path(id)
    # Prevent directory traversal
    sanitized_id = id.gsub(/[^a-zA-Z0-9_-]/, '')
    raise SecurityError, "Invalid prompt ID" if sanitized_id != id

    File.join(@config.prompts_dir, "#{sanitized_id}.txt")
  end
end

Testing Architecture

The architecture supports comprehensive testing at multiple levels:

1. Unit Tests

  • Individual component testing
  • Mock storage adapters for isolation
  • Parameter validation testing

2. Integration Tests

  • End-to-end prompt processing
  • Multiple storage adapter combinations
  • Error handling scenarios

3. Performance Tests

  • Large prompt collections
  • Concurrent access patterns
  • Memory usage optimization

Future Architecture Goals

1. Plugin System

  • Dynamic directive loading
  • Community-contributed processors
  • Runtime plugin management

2. Distributed Storage

  • Multi-node prompt storage
  • Replication and consistency
  • Fault tolerance

3. Real-time Updates

  • Live prompt editing
  • Change notification system
  • Collaborative editing support

This architecture provides a solid foundation for current needs while remaining flexible enough to support future enhancements and extensions.