Class: FactDb::Services::FactService
- Inherits:
-
Object
- Object
- FactDb::Services::FactService
- Defined in:
- lib/fact_db/services/fact_service.rb
Overview
Service class for managing facts in the database
Provides methods for creating, querying, and manipulating facts including temporal queries, semantic search, and conflict resolution.
Instance Attribute Summary collapse
-
#config ⇒ FactDb::Config
readonly
The configuration object.
-
#entity_service ⇒ FactDb::Services::EntityService
readonly
The entity service instance.
-
#resolver ⇒ FactDb::Resolution::FactResolver
readonly
The fact resolver instance.
Instance Method Summary collapse
-
#build_timeline_fact(entity_id:, topic: nil) ⇒ Hash
Builds a timeline fact summarizing an entity’s history.
-
#by_extraction_method(method, limit: nil) ⇒ ActiveRecord::Relation
Returns facts by extraction method.
-
#corroborate(fact_id, corroborating_fact_id) ⇒ FactDb::Models::Fact
Links a corroborating fact to support another fact.
-
#create(text, valid_at:, invalid_at: nil, status: :canonical, source_id: nil, mentions: [], extraction_method: :manual, confidence: 1.0, metadata: {}) ⇒ FactDb::Models::Fact
Creates a new fact in the database.
-
#current_facts(entity: nil, topic: nil, limit: nil) ⇒ ActiveRecord::Relation
Returns currently valid facts.
-
#extract_from_source(source_id, extractor: config.default_extractor) ⇒ Array<FactDb::Models::Fact>
(also: #extract_from_content)
Extracts facts from a source document.
-
#fact_stats(entity_id = nil) ⇒ Hash
Returns fact statistics for an entity (or all facts).
-
#facts_at(date, entity: nil, topic: nil) ⇒ ActiveRecord::Relation
Returns facts valid at a specific date.
-
#find(id) ⇒ FactDb::Models::Fact
Finds a fact by ID.
-
#find_conflicts(entity_id: nil, topic: nil) ⇒ Array<Hash>
Finds conflicting facts for an entity or topic.
-
#find_or_create(text, valid_at:, invalid_at: nil, status: :canonical, source_id: nil, mentions: [], extraction_method: :manual, confidence: 1.0, metadata: {}) ⇒ FactDb::Models::Fact
Finds an existing fact or creates a new one.
-
#initialize(config = FactDb.config) ⇒ FactService
constructor
Initializes a new FactService instance.
-
#invalidate(fact_id, at: Time.current) ⇒ FactDb::Models::Fact
Invalidates a fact at a specific time.
-
#query(topic: nil, at: nil, entity: nil, status: :canonical, limit: nil) ⇒ ActiveRecord::Relation
Queries facts with filtering options.
-
#recent(limit: 10, status: :canonical) ⇒ ActiveRecord::Relation
Returns recently created facts.
-
#resolve_conflict(keep_fact_id, supersede_fact_ids, reason: nil) ⇒ FactDb::Models::Fact
Resolves a conflict by keeping one fact and superseding others.
-
#search(query, entity: nil, status: :canonical, limit: 20) ⇒ ActiveRecord::Relation
Searches facts using full-text search.
-
#semantic_search(query, entity: nil, at: nil, limit: 20) ⇒ ActiveRecord::Relation
Searches facts using semantic similarity (vector search).
-
#stats ⇒ Hash
Returns aggregate statistics about all facts.
-
#supersede(old_fact_id, new_text, valid_at:, mentions: []) ⇒ FactDb::Models::Fact
Supersedes an old fact with new information.
-
#synthesize(source_fact_ids, synthesized_text, valid_at:, invalid_at: nil, mentions: []) ⇒ FactDb::Models::Fact
Synthesizes multiple facts into a single summary fact.
-
#timeline(entity_id:, from: nil, to: nil) ⇒ FactDb::Temporal::Timeline
Builds a timeline of facts for an entity.
Constructor Details
#initialize(config = FactDb.config) ⇒ FactService
Initializes a new FactService instance
27 28 29 30 31 |
# File 'lib/fact_db/services/fact_service.rb', line 27 def initialize(config = FactDb.config) @config = config @resolver = Resolution::FactResolver.new(config) @entity_service = EntityService.new(config) end |
Instance Attribute Details
#config ⇒ FactDb::Config (readonly)
Returns the configuration object.
16 17 18 |
# File 'lib/fact_db/services/fact_service.rb', line 16 def config @config end |
#entity_service ⇒ FactDb::Services::EntityService (readonly)
Returns the entity service instance.
22 23 24 |
# File 'lib/fact_db/services/fact_service.rb', line 22 def entity_service @entity_service end |
#resolver ⇒ FactDb::Resolution::FactResolver (readonly)
Returns the fact resolver instance.
19 20 21 |
# File 'lib/fact_db/services/fact_service.rb', line 19 def resolver @resolver end |
Instance Method Details
#build_timeline_fact(entity_id:, topic: nil) ⇒ Hash
Builds a timeline fact summarizing an entity’s history
324 325 326 |
# File 'lib/fact_db/services/fact_service.rb', line 324 def build_timeline_fact(entity_id:, topic: nil) @resolver.build_timeline_fact(entity_id: entity_id, topic: topic) end |
#by_extraction_method(method, limit: nil) ⇒ ActiveRecord::Relation
Returns facts by extraction method
343 344 345 346 347 |
# File 'lib/fact_db/services/fact_service.rb', line 343 def by_extraction_method(method, limit: nil) scope = Models::Fact.extracted_by(method.to_s).order(created_at: :desc) scope = scope.limit(limit) if limit scope end |
#corroborate(fact_id, corroborating_fact_id) ⇒ FactDb::Models::Fact
Links a corroborating fact to support another fact
260 261 262 |
# File 'lib/fact_db/services/fact_service.rb', line 260 def corroborate(fact_id, ) @resolver.corroborate(fact_id, ) end |
#create(text, valid_at:, invalid_at: nil, status: :canonical, source_id: nil, mentions: [], extraction_method: :manual, confidence: 1.0, metadata: {}) ⇒ FactDb::Models::Fact
Creates a new fact in the database
55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 |
# File 'lib/fact_db/services/fact_service.rb', line 55 def create(text, valid_at:, invalid_at: nil, status: :canonical, source_id: nil, mentions: [], extraction_method: :manual, confidence: 1.0, metadata: {}) = (text) fact = Models::Fact.create!( text: text, valid_at: valid_at, invalid_at: invalid_at, status: status.to_s, extraction_method: extraction_method.to_s, confidence: confidence, metadata: , embedding: ) # Link to source if source_id source = Models::Source.find(source_id) fact.add_source(source: source, kind: "primary") end # Add entity mentions mentions.each do |mention| entity = resolve_or_create_entity(mention) fact.add_mention( entity: entity, text: mention[:text] || mention[:name], role: mention[:role], confidence: mention[:confidence] || 1.0 ) end fact end |
#current_facts(entity: nil, topic: nil, limit: nil) ⇒ ActiveRecord::Relation
Returns currently valid facts
194 195 196 |
# File 'lib/fact_db/services/fact_service.rb', line 194 def current_facts(entity: nil, topic: nil, limit: nil) query(topic: topic, entity: entity, at: nil, status: :canonical, limit: limit) end |
#extract_from_source(source_id, extractor: config.default_extractor) ⇒ Array<FactDb::Models::Fact> Also known as: extract_from_content
Extracts facts from a source document
Uses the configured extractor to parse the source content and create facts.
141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 |
# File 'lib/fact_db/services/fact_service.rb', line 141 def extract_from_source(source_id, extractor: config.default_extractor) source = Models::Source.find(source_id) extractor_instance = Extractors::Base.for(extractor, config) extracted = extractor_instance.extract( source.content, { captured_at: source.captured_at } ) extracted.map do |fact_data| create( fact_data[:text], valid_at: fact_data[:valid_at], invalid_at: fact_data[:invalid_at], source_id: source_id, mentions: fact_data[:mentions], extraction_method: fact_data[:extraction_method] || extractor, confidence: fact_data[:confidence] || 1.0, metadata: fact_data[:metadata] || {} ) end end |
#fact_stats(entity_id = nil) ⇒ Hash
Returns fact statistics for an entity (or all facts)
368 369 370 371 372 373 374 375 376 377 |
# File 'lib/fact_db/services/fact_service.rb', line 368 def fact_stats(entity_id = nil) scope = entity_id ? Models::Fact.mentioning_entity(entity_id) : Models::Fact.all { canonical: scope.where(status: "canonical").count, superseded: scope.where(status: "superseded").count, corroborated: scope.where.not(corroborated_by_ids: nil).where.not(corroborated_by_ids: []).count, synthesized: scope.where(status: "synthesized").count } end |
#facts_at(date, entity: nil, topic: nil) ⇒ ActiveRecord::Relation
Returns facts valid at a specific date
204 205 206 |
# File 'lib/fact_db/services/fact_service.rb', line 204 def facts_at(date, entity: nil, topic: nil) query(topic: topic, entity: entity, at: date, status: :canonical) end |
#find(id) ⇒ FactDb::Models::Fact
Finds a fact by ID
127 128 129 |
# File 'lib/fact_db/services/fact_service.rb', line 127 def find(id) Models::Fact.find(id) end |
#find_conflicts(entity_id: nil, topic: nil) ⇒ Array<Hash>
Finds conflicting facts for an entity or topic
305 306 307 |
# File 'lib/fact_db/services/fact_service.rb', line 305 def find_conflicts(entity_id: nil, topic: nil) @resolver.find_conflicts(entity_id: entity_id, topic: topic) end |
#find_or_create(text, valid_at:, invalid_at: nil, status: :canonical, source_id: nil, mentions: [], extraction_method: :manual, confidence: 1.0, metadata: {}) ⇒ FactDb::Models::Fact
Finds an existing fact or creates a new one
Uses a SHA256 digest of the text and valid_at date to find duplicates.
103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 |
# File 'lib/fact_db/services/fact_service.rb', line 103 def find_or_create(text, valid_at:, invalid_at: nil, status: :canonical, source_id: nil, mentions: [], extraction_method: :manual, confidence: 1.0, metadata: {}) digest = Digest::SHA256.hexdigest(text) existing = Models::Fact.find_by(digest: digest, valid_at: valid_at) return existing if existing create( text, valid_at: valid_at, invalid_at: invalid_at, status: status, source_id: source_id, mentions: mentions, extraction_method: extraction_method, confidence: confidence, metadata: ) end |
#invalidate(fact_id, at: Time.current) ⇒ FactDb::Models::Fact
Invalidates a fact at a specific time
251 252 253 |
# File 'lib/fact_db/services/fact_service.rb', line 251 def invalidate(fact_id, at: Time.current) @resolver.invalidate(fact_id, at: at) end |
#query(topic: nil, at: nil, entity: nil, status: :canonical, limit: nil) ⇒ ActiveRecord::Relation
Queries facts with filtering options
178 179 180 181 182 183 184 185 186 |
# File 'lib/fact_db/services/fact_service.rb', line 178 def query(topic: nil, at: nil, entity: nil, status: :canonical, limit: nil) Temporal::Query.new.execute( topic: topic, at: at, entity_id: entity, status: status, limit: limit ) end |
#recent(limit: 10, status: :canonical) ⇒ ActiveRecord::Relation
Returns recently created facts
333 334 335 336 |
# File 'lib/fact_db/services/fact_service.rb', line 333 def recent(limit: 10, status: :canonical) scope = Models::Fact.where(status: status.to_s).order(created_at: :desc) scope.limit(limit) end |
#resolve_conflict(keep_fact_id, supersede_fact_ids, reason: nil) ⇒ FactDb::Models::Fact
Resolves a conflict by keeping one fact and superseding others
315 316 317 |
# File 'lib/fact_db/services/fact_service.rb', line 315 def resolve_conflict(keep_fact_id, supersede_fact_ids, reason: nil) @resolver.resolve_conflict(keep_fact_id, supersede_fact_ids, reason: reason) end |
#search(query, entity: nil, status: :canonical, limit: 20) ⇒ ActiveRecord::Relation
Searches facts using full-text search
271 272 273 274 275 |
# File 'lib/fact_db/services/fact_service.rb', line 271 def search(query, entity: nil, status: :canonical, limit: 20) scope = Models::Fact.search_text(query) scope = apply_filters(scope, entity: entity, status: status) scope.order(valid_at: :desc).limit(limit) end |
#semantic_search(query, entity: nil, at: nil, limit: 20) ⇒ ActiveRecord::Relation
Searches facts using semantic similarity (vector search)
Requires an embedding generator to be configured.
289 290 291 292 293 294 295 296 297 298 |
# File 'lib/fact_db/services/fact_service.rb', line 289 def semantic_search(query, entity: nil, at: nil, limit: 20) = (query) return Models::Fact.none unless scope = Models::Fact.canonical.nearest_neighbors(, limit: limit * 2) scope = scope.currently_valid if at.nil? scope = scope.valid_at(at) if at scope = scope.mentioning_entity(entity) if entity scope.limit(limit) end |
#stats ⇒ Hash
Returns aggregate statistics about all facts
352 353 354 355 356 357 358 359 360 361 362 |
# File 'lib/fact_db/services/fact_service.rb', line 352 def stats { total: Models::Fact.count, total_count: Models::Fact.count, canonical_count: Models::Fact.canonical.count, currently_valid_count: Models::Fact.canonical.currently_valid.count, by_status: Models::Fact.group(:status).count, by_extraction_method: Models::Fact.group(:extraction_method).count, average_confidence: Models::Fact.average(:confidence)&.to_f&.round(3) } end |
#supersede(old_fact_id, new_text, valid_at:, mentions: []) ⇒ FactDb::Models::Fact
Supersedes an old fact with new information
Marks the old fact as superseded and creates a new canonical fact.
230 231 232 |
# File 'lib/fact_db/services/fact_service.rb', line 230 def supersede(old_fact_id, new_text, valid_at:, mentions: []) @resolver.supersede(old_fact_id, new_text, valid_at: valid_at, mentions: mentions) end |
#synthesize(source_fact_ids, synthesized_text, valid_at:, invalid_at: nil, mentions: []) ⇒ FactDb::Models::Fact
Synthesizes multiple facts into a single summary fact
242 243 244 |
# File 'lib/fact_db/services/fact_service.rb', line 242 def synthesize(source_fact_ids, synthesized_text, valid_at:, invalid_at: nil, mentions: []) @resolver.synthesize(source_fact_ids, synthesized_text, valid_at: valid_at, invalid_at: invalid_at, mentions: mentions) end |
#timeline(entity_id:, from: nil, to: nil) ⇒ FactDb::Temporal::Timeline
Builds a timeline of facts for an entity
217 218 219 |
# File 'lib/fact_db/services/fact_service.rb', line 217 def timeline(entity_id:, from: nil, to: nil) Temporal::Timeline.new.build(entity_id: entity_id, from: from, to: to) end |