Class: HTM::PropositionService

Inherits:
Object
  • Object
show all
Defined in:
lib/htm/proposition_service.rb

Overview

Proposition Service - Extracts atomic factual propositions from text

This service breaks complex text into simple, self-contained factual statements that can be stored as independent memory nodes. Each proposition:

  • Expresses a single fact

  • Is understandable without context

  • Uses full names, not pronouns

  • Includes relevant dates/qualifiers

  • Contains one subject-predicate relationship

The actual LLM call is delegated to HTM.configuration.proposition_extractor

Examples:

propositions = HTM::PropositionService.extract(
  "In 1969, Neil Armstrong became the first person to walk on the Moon during Apollo 11."
)
# => ["Neil Armstrong was an astronaut.",
#     "Neil Armstrong walked on the Moon in 1969.",
#     "Neil Armstrong was the first person to walk on the Moon.",
#     "Neil Armstrong walked on the Moon during the Apollo 11 mission.",
#     "The Apollo 11 mission occurred in 1969."]

Constant Summary collapse

META_RESPONSE_PATTERNS =

Patterns that indicate meta-responses (LLM asking for input instead of extracting)

[
  /please provide/i,
  /provide the text/i,
  /provide me with/i,
  /I need the text/i,
  /I am ready/i,
  /waiting for/i,
  /send me the/i,
  /what text would you/i,
  /what would you like/i,
  /cannot extract.*without/i,
  /no text provided/i
].freeze

Class Method Summary collapse

Class Method Details

.circuit_breakerHTM::CircuitBreaker

Get or create the circuit breaker for proposition service

Returns:



53
54
55
56
57
58
59
60
61
# File 'lib/htm/proposition_service.rb', line 53

def circuit_breaker
  @circuit_breaker_mutex.synchronize do
    @circuit_breaker ||= HTM::CircuitBreaker.new(
      name: 'proposition_service',
      failure_threshold: 5,
      reset_timeout: 60
    )
  end
end

.extract(content) ⇒ Array<String>

Extract propositions from text content

Parameters:

  • content (String)

    Text to analyze

Returns:

  • (Array<String>)

    Array of atomic propositions

Raises:



81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
# File 'lib/htm/proposition_service.rb', line 81

def self.extract(content)
  # Use circuit breaker to protect against cascading failures
  raw_propositions = circuit_breaker.call do
    HTM.configuration.proposition_extractor.call(content)
  end

  # Parse response (may be string or array)
  parsed_propositions = parse_propositions(raw_propositions)

  # Validate and filter propositions
  validate_and_filter_propositions(parsed_propositions)
rescue HTM::CircuitBreakerOpenError, HTM::PropositionError
  raise
rescue StandardError => e
  HTM.logger.error "PropositionService: Failed to extract propositions: #{e.message}"
  raise HTM::PropositionError, "Proposition extraction failed: #{e.message}"
end

.max_lengthInteger

Get maximum character length from config

Returns:

  • (Integer)

    Maximum character count for valid propositions



137
138
139
140
141
# File 'lib/htm/proposition_service.rb', line 137

def self.max_length
  HTM.config.proposition.max_length || 1000
rescue
  1000
end

.meta_response?(proposition) ⇒ Boolean

Check if proposition is a meta-response (LLM asking for input)

Parameters:

  • proposition (String)

    Proposition to check

Returns:

  • (Boolean)

    True if it’s a meta-response



158
159
160
# File 'lib/htm/proposition_service.rb', line 158

def self.meta_response?(proposition)
  META_RESPONSE_PATTERNS.any? { |pattern| proposition.match?(pattern) }
end

.min_lengthInteger

Get minimum character length from config

Returns:

  • (Integer)

    Minimum character count for valid propositions



127
128
129
130
131
# File 'lib/htm/proposition_service.rb', line 127

def self.min_length
  HTM.config.proposition.min_length || 10
rescue
  10
end

.min_wordsInteger

Get minimum words from config

Returns:

  • (Integer)

    Minimum word count for valid propositions



147
148
149
150
151
# File 'lib/htm/proposition_service.rb', line 147

def self.min_words
  HTM.config.proposition.min_words || 3
rescue
  3
end

.parse_propositions(raw_propositions) ⇒ Array<String>

Parse proposition response (handles string or array input)

Parameters:

  • raw_propositions (String, Array)

    Raw response from extractor

Returns:

  • (Array<String>)

    Parsed proposition strings



104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
# File 'lib/htm/proposition_service.rb', line 104

def self.parse_propositions(raw_propositions)
  case raw_propositions
  when Array
    # Already an array, return as-is
    raw_propositions.map(&:to_s).map(&:strip).reject(&:empty?)
  when String
    # String response - split by newlines, remove list markers
    raw_propositions
      .split("\n")
      .map(&:strip)
      .map { |line| line.sub(/^[-*•]\s*/, '') } # Remove bullet points
      .map { |line| line.sub(/^\d+\.\s*/, '') } # Remove numbered lists
      .map(&:strip)
      .reject(&:empty?)
  else
    raise HTM::PropositionError, "Proposition response must be Array or String, got #{raw_propositions.class}"
  end
end

.reset_circuit_breaker!void

This method returns an undefined value.

Reset the circuit breaker (useful for testing)



67
68
69
70
71
# File 'lib/htm/proposition_service.rb', line 67

def reset_circuit_breaker!
  @circuit_breaker_mutex.synchronize do
    @circuit_breaker&.reset!
  end
end

.valid_proposition?(proposition) ⇒ Boolean

Validate single proposition

Parameters:

  • proposition (String)

    Proposition to validate

Returns:

  • (Boolean)

    True if valid



213
214
215
216
217
218
219
220
221
222
# File 'lib/htm/proposition_service.rb', line 213

def self.valid_proposition?(proposition)
  return false unless proposition.is_a?(String)
  return false if proposition.length < min_length
  return false if proposition.length > max_length
  return false unless proposition.match?(/[a-zA-Z]{3,}/)
  return false if proposition.split.size < min_words
  return false if meta_response?(proposition)

  true
end

.validate_and_filter_propositions(propositions) ⇒ Array<String>

Validate and filter propositions

Parameters:

  • propositions (Array<String>)

    Parsed propositions

Returns:

  • (Array<String>)

    Valid propositions only



167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
# File 'lib/htm/proposition_service.rb', line 167

def self.validate_and_filter_propositions(propositions)
  valid_propositions = []
  min_char_length = min_length
  max_char_length = max_length
  min_word_count = min_words

  propositions.each do |proposition|
    # Check minimum length (characters)
    next if proposition.length < min_char_length

    # Check maximum length
    if proposition.length > max_char_length
      HTM.logger.warn "PropositionService: Proposition too long, skipping: #{proposition[0..50]}..."
      next
    end

    # Check for actual content (not just punctuation/whitespace)
    unless proposition.match?(/[a-zA-Z]{3,}/)
      next
    end

    # Check minimum word count
    word_count = proposition.split.size
    if word_count < min_word_count
      HTM.logger.debug "PropositionService: Proposition too short (#{word_count} words), skipping: #{proposition}"
      next
    end

    # Filter out meta-responses (LLM asking for more input)
    if meta_response?(proposition)
      HTM.logger.warn "PropositionService: Filtered meta-response: #{proposition[0..50]}..."
      next
    end

    # Proposition is valid
    valid_propositions << proposition
  end

  valid_propositions.uniq
end