fact_db 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. checksums.yaml +7 -0
  2. data/.envrc +1 -0
  3. data/CHANGELOG.md +48 -0
  4. data/COMMITS.md +196 -0
  5. data/README.md +102 -0
  6. data/Rakefile +41 -0
  7. data/db/migrate/001_enable_extensions.rb +7 -0
  8. data/db/migrate/002_create_contents.rb +44 -0
  9. data/db/migrate/003_create_entities.rb +36 -0
  10. data/db/migrate/004_create_entity_aliases.rb +18 -0
  11. data/db/migrate/005_create_facts.rb +65 -0
  12. data/db/migrate/006_create_entity_mentions.rb +18 -0
  13. data/db/migrate/007_create_fact_sources.rb +18 -0
  14. data/docs/api/extractors/index.md +71 -0
  15. data/docs/api/extractors/llm.md +162 -0
  16. data/docs/api/extractors/manual.md +92 -0
  17. data/docs/api/extractors/rule-based.md +165 -0
  18. data/docs/api/facts.md +300 -0
  19. data/docs/api/index.md +66 -0
  20. data/docs/api/models/content.md +165 -0
  21. data/docs/api/models/entity.md +202 -0
  22. data/docs/api/models/fact.md +270 -0
  23. data/docs/api/models/index.md +77 -0
  24. data/docs/api/pipeline/extraction.md +175 -0
  25. data/docs/api/pipeline/index.md +72 -0
  26. data/docs/api/pipeline/resolution.md +209 -0
  27. data/docs/api/services/content-service.md +166 -0
  28. data/docs/api/services/entity-service.md +202 -0
  29. data/docs/api/services/fact-service.md +223 -0
  30. data/docs/api/services/index.md +55 -0
  31. data/docs/architecture/database-schema.md +293 -0
  32. data/docs/architecture/entity-resolution.md +293 -0
  33. data/docs/architecture/index.md +149 -0
  34. data/docs/architecture/temporal-facts.md +268 -0
  35. data/docs/architecture/three-layer-model.md +242 -0
  36. data/docs/assets/css/custom.css +137 -0
  37. data/docs/assets/fact_db.jpg +0 -0
  38. data/docs/assets/images/fact_db.jpg +0 -0
  39. data/docs/concepts.md +183 -0
  40. data/docs/examples/basic-usage.md +235 -0
  41. data/docs/examples/hr-onboarding.md +312 -0
  42. data/docs/examples/index.md +64 -0
  43. data/docs/examples/news-analysis.md +288 -0
  44. data/docs/getting-started/database-setup.md +170 -0
  45. data/docs/getting-started/index.md +71 -0
  46. data/docs/getting-started/installation.md +98 -0
  47. data/docs/getting-started/quick-start.md +191 -0
  48. data/docs/guides/batch-processing.md +325 -0
  49. data/docs/guides/configuration.md +243 -0
  50. data/docs/guides/entity-management.md +364 -0
  51. data/docs/guides/extracting-facts.md +299 -0
  52. data/docs/guides/index.md +22 -0
  53. data/docs/guides/ingesting-content.md +252 -0
  54. data/docs/guides/llm-integration.md +299 -0
  55. data/docs/guides/temporal-queries.md +315 -0
  56. data/docs/index.md +121 -0
  57. data/examples/README.md +130 -0
  58. data/examples/basic_usage.rb +164 -0
  59. data/examples/entity_management.rb +216 -0
  60. data/examples/hr_system.rb +428 -0
  61. data/examples/rule_based_extraction.rb +258 -0
  62. data/examples/temporal_queries.rb +245 -0
  63. data/lib/fact_db/config.rb +71 -0
  64. data/lib/fact_db/database.rb +45 -0
  65. data/lib/fact_db/errors.rb +10 -0
  66. data/lib/fact_db/extractors/base.rb +117 -0
  67. data/lib/fact_db/extractors/llm_extractor.rb +179 -0
  68. data/lib/fact_db/extractors/manual_extractor.rb +53 -0
  69. data/lib/fact_db/extractors/rule_based_extractor.rb +228 -0
  70. data/lib/fact_db/llm/adapter.rb +109 -0
  71. data/lib/fact_db/models/content.rb +62 -0
  72. data/lib/fact_db/models/entity.rb +84 -0
  73. data/lib/fact_db/models/entity_alias.rb +26 -0
  74. data/lib/fact_db/models/entity_mention.rb +33 -0
  75. data/lib/fact_db/models/fact.rb +192 -0
  76. data/lib/fact_db/models/fact_source.rb +35 -0
  77. data/lib/fact_db/pipeline/extraction_pipeline.rb +146 -0
  78. data/lib/fact_db/pipeline/resolution_pipeline.rb +129 -0
  79. data/lib/fact_db/resolution/entity_resolver.rb +261 -0
  80. data/lib/fact_db/resolution/fact_resolver.rb +259 -0
  81. data/lib/fact_db/services/content_service.rb +93 -0
  82. data/lib/fact_db/services/entity_service.rb +150 -0
  83. data/lib/fact_db/services/fact_service.rb +193 -0
  84. data/lib/fact_db/temporal/query.rb +125 -0
  85. data/lib/fact_db/temporal/timeline.rb +134 -0
  86. data/lib/fact_db/version.rb +5 -0
  87. data/lib/fact_db.rb +141 -0
  88. data/mkdocs.yml +198 -0
  89. metadata +288 -0
@@ -0,0 +1,162 @@
1
+ # LLMExtractor
2
+
3
+ AI-powered fact extraction using large language models.
4
+
5
+ ## Class: `FactDb::Extractors::LLMExtractor`
6
+
7
+ ```ruby
8
+ extractor = FactDb::Extractors::LLMExtractor.new(config)
9
+ ```
10
+
11
+ ## Requirements
12
+
13
+ - `ruby_llm` gem installed
14
+ - LLM provider configured (API key, model)
15
+
16
+ ## Configuration
17
+
18
+ ```ruby
19
+ FactDb.configure do |config|
20
+ config.llm_provider = :openai
21
+ config.llm_model = "gpt-4o-mini"
22
+ config.llm_api_key = ENV['OPENAI_API_KEY']
23
+ end
24
+ ```
25
+
26
+ ## Methods
27
+
28
+ ### extract
29
+
30
+ ```ruby
31
+ def extract(content)
32
+ ```
33
+
34
+ Extract facts from content using LLM.
35
+
36
+ **Parameters:**
37
+
38
+ - `content` (Models::Content) - Content to process
39
+
40
+ **Returns:** `Array<Models::Fact>`
41
+
42
+ **Example:**
43
+
44
+ ```ruby
45
+ extractor = LLMExtractor.new(config)
46
+ facts = extractor.extract(content)
47
+
48
+ facts.each do |fact|
49
+ puts fact.fact_text
50
+ puts " Valid: #{fact.valid_at}"
51
+ puts " Confidence: #{fact.confidence}"
52
+ end
53
+ ```
54
+
55
+ ## Extraction Process
56
+
57
+ 1. **Prompt Construction** - Build prompt with content text
58
+ 2. **LLM Call** - Send to configured LLM provider
59
+ 3. **Response Parsing** - Parse JSON response
60
+ 4. **Fact Creation** - Create fact records
61
+ 5. **Entity Resolution** - Resolve mentioned entities
62
+ 6. **Source Linking** - Link facts to source content
63
+
64
+ ## Prompt Structure
65
+
66
+ The extractor uses a structured prompt:
67
+
68
+ ```
69
+ Extract temporal facts from this content. For each fact:
70
+ 1. Identify the assertion (what is being stated)
71
+ 2. Identify entities mentioned (people, organizations, places)
72
+ 3. Determine when the fact became valid
73
+ 4. Assess confidence level
74
+
75
+ Content:
76
+ {content.raw_text}
77
+
78
+ Return JSON:
79
+ {
80
+ "facts": [
81
+ {
82
+ "text": "...",
83
+ "valid_at": "YYYY-MM-DD",
84
+ "entities": [
85
+ {"name": "...", "type": "person|organization|place", "role": "subject|object|..."}
86
+ ],
87
+ "confidence": 0.0-1.0
88
+ }
89
+ ]
90
+ }
91
+ ```
92
+
93
+ ## Supported Providers
94
+
95
+ | Provider | Models | Config |
96
+ |----------|--------|--------|
97
+ | OpenAI | gpt-4o, gpt-4o-mini | `llm_provider: :openai` |
98
+ | Anthropic | claude-sonnet-4, claude-3-haiku | `llm_provider: :anthropic` |
99
+ | Google | gemini-2.0-flash | `llm_provider: :gemini` |
100
+ | Ollama | llama3.2, mistral | `llm_provider: :ollama` |
101
+ | AWS Bedrock | claude-sonnet-4 | `llm_provider: :bedrock` |
102
+ | OpenRouter | Various | `llm_provider: :openrouter` |
103
+
104
+ ## Error Handling
105
+
106
+ ```ruby
107
+ begin
108
+ facts = extractor.extract(content)
109
+ rescue FactDb::ConfigurationError => e
110
+ # LLM not configured
111
+ puts "Config error: #{e.message}"
112
+ rescue FactDb::ExtractionError => e
113
+ # Extraction failed
114
+ puts "Extraction error: #{e.message}"
115
+ end
116
+ ```
117
+
118
+ ## Advantages
119
+
120
+ - Handles unstructured text
121
+ - Understands context and nuance
122
+ - Identifies implicit facts
123
+ - Resolves entities automatically
124
+
125
+ ## Disadvantages
126
+
127
+ - API costs
128
+ - Latency
129
+ - Occasional errors
130
+ - Requires validation
131
+
132
+ ## Best Practices
133
+
134
+ ### 1. Validate Results
135
+
136
+ ```ruby
137
+ facts = extractor.extract(content)
138
+ facts.each do |fact|
139
+ if fact.confidence < 0.7
140
+ fact.update!(metadata: { needs_review: true })
141
+ end
142
+ end
143
+ ```
144
+
145
+ ### 2. Cache Responses
146
+
147
+ ```ruby
148
+ cache_key = "llm:#{content.content_hash}"
149
+ facts = Rails.cache.fetch(cache_key) do
150
+ extractor.extract(content)
151
+ end
152
+ ```
153
+
154
+ ### 3. Handle Rate Limits
155
+
156
+ ```ruby
157
+ require 'retryable'
158
+
159
+ Retryable.retryable(tries: 3, sleep: lambda { |n| 2**n }) do
160
+ extractor.extract(content)
161
+ end
162
+ ```
@@ -0,0 +1,92 @@
1
+ # ManualExtractor
2
+
3
+ API-driven fact creation for maximum control and accuracy.
4
+
5
+ ## Class: `FactDb::Extractors::ManualExtractor`
6
+
7
+ The ManualExtractor doesn't automatically extract facts - instead it provides a structured interface for creating facts programmatically.
8
+
9
+ ## Usage
10
+
11
+ ```ruby
12
+ extractor = FactDb::Extractors::ManualExtractor.new(config)
13
+ ```
14
+
15
+ ## Methods
16
+
17
+ ### extract
18
+
19
+ ```ruby
20
+ def extract(content)
21
+ ```
22
+
23
+ Returns an empty array - manual extraction is done via direct fact creation.
24
+
25
+ **Returns:** `[]`
26
+
27
+ ## When to Use
28
+
29
+ - High-stakes facts that require human verification
30
+ - Structured data import from external systems
31
+ - Fact correction or adjustment
32
+ - Initial seeding of the system
33
+
34
+ ## Creating Facts Manually
35
+
36
+ Instead of using the extractor, create facts directly:
37
+
38
+ ```ruby
39
+ facts = FactDb.new
40
+
41
+ # Create entities
42
+ paula = facts.entity_service.create("Paula Chen", type: :person)
43
+ microsoft = facts.entity_service.create("Microsoft", type: :organization)
44
+
45
+ # Create fact with explicit links
46
+ fact = facts.fact_service.create(
47
+ "Paula Chen joined Microsoft as Principal Engineer",
48
+ valid_at: Date.parse("2024-01-10"),
49
+ mentions: [
50
+ { entity: paula, role: "subject", text: "Paula Chen" },
51
+ { entity: microsoft, role: "organization", text: "Microsoft" }
52
+ ],
53
+ sources: [
54
+ { content: announcement, type: "primary", excerpt: "...accepted the offer..." }
55
+ ],
56
+ confidence: 1.0
57
+ )
58
+ ```
59
+
60
+ ## Bulk Import Pattern
61
+
62
+ ```ruby
63
+ # Import from structured data
64
+ data = [
65
+ { text: "Fact 1", date: "2024-01-01", entity: "Paula" },
66
+ { text: "Fact 2", date: "2024-01-15", entity: "Paula" }
67
+ ]
68
+
69
+ data.each do |item|
70
+ entity = facts.resolve_entity(item[:entity])
71
+
72
+ facts.fact_service.create(
73
+ item[:text],
74
+ valid_at: Date.parse(item[:date]),
75
+ mentions: [{ entity: entity, role: "subject", text: item[:entity] }],
76
+ extraction_method: "manual"
77
+ )
78
+ end
79
+ ```
80
+
81
+ ## Advantages
82
+
83
+ - Complete control over fact creation
84
+ - Highest accuracy (human-verified)
85
+ - No LLM costs
86
+ - Works without external dependencies
87
+
88
+ ## Disadvantages
89
+
90
+ - Labor intensive
91
+ - Not scalable for large volumes
92
+ - Requires domain expertise
@@ -0,0 +1,165 @@
1
+ # RuleBasedExtractor
2
+
3
+ Pattern-based fact extraction using regular expressions.
4
+
5
+ ## Class: `FactDb::Extractors::RuleBasedExtractor`
6
+
7
+ ```ruby
8
+ extractor = FactDb::Extractors::RuleBasedExtractor.new(config)
9
+ ```
10
+
11
+ ## Methods
12
+
13
+ ### extract
14
+
15
+ ```ruby
16
+ def extract(content)
17
+ ```
18
+
19
+ Extract facts using pattern matching.
20
+
21
+ **Returns:** `Array<Models::Fact>`
22
+
23
+ ## Built-in Patterns
24
+
25
+ The extractor includes patterns for common fact types:
26
+
27
+ ### Employment Events
28
+
29
+ ```ruby
30
+ # "X joined Y"
31
+ /(?<person>\w+(?:\s+\w+)*)\s+joined\s+(?<org>\w+(?:\s+\w+)*)/i
32
+
33
+ # "X left Y"
34
+ /(?<person>\w+(?:\s+\w+)*)\s+left\s+(?<org>\w+(?:\s+\w+)*)/i
35
+
36
+ # "X was hired by Y"
37
+ /(?<person>\w+(?:\s+\w+)*)\s+was\s+hired\s+by\s+(?<org>\w+(?:\s+\w+)*)/i
38
+ ```
39
+
40
+ ### Title Changes
41
+
42
+ ```ruby
43
+ # "X is/was the Y"
44
+ /(?<person>\w+(?:\s+\w+)*)\s+(?:is|was)\s+(?:the\s+)?(?<title>[A-Z][a-z]+(?:\s+[A-Z][a-z]+)*)/
45
+
46
+ # "X promoted to Y"
47
+ /(?<person>\w+(?:\s+\w+)*)\s+(?:was\s+)?promoted\s+to\s+(?<title>[A-Z][a-z]+(?:\s+[A-Z][a-z]+)*)/i
48
+ ```
49
+
50
+ ### Date Patterns
51
+
52
+ ```ruby
53
+ # "on January 10, 2024"
54
+ /on\s+(?<month>\w+)\s+(?<day>\d{1,2}),?\s+(?<year>\d{4})/i
55
+
56
+ # "in Q4 2024"
57
+ /in\s+Q(?<quarter>\d)\s+(?<year>\d{4})/i
58
+
59
+ # ISO dates
60
+ /(?<date>\d{4}-\d{2}-\d{2})/
61
+ ```
62
+
63
+ ## Usage Example
64
+
65
+ ```ruby
66
+ extractor = RuleBasedExtractor.new(config)
67
+
68
+ content = Models::Content.create!(
69
+ raw_text: "Paula Chen joined Microsoft on January 10, 2024 as Principal Engineer.",
70
+ content_type: "announcement",
71
+ captured_at: Time.current
72
+ )
73
+
74
+ facts = extractor.extract(content)
75
+ # Returns facts about:
76
+ # - Paula joining Microsoft
77
+ # - Paula's title as Principal Engineer
78
+ # - Date: January 10, 2024
79
+ ```
80
+
81
+ ## Adding Custom Patterns
82
+
83
+ Extend the extractor with custom patterns:
84
+
85
+ ```ruby
86
+ class CustomRuleExtractor < FactDb::Extractors::RuleBasedExtractor
87
+ CUSTOM_PATTERNS = [
88
+ {
89
+ pattern: /revenue\s+of\s+\$(?<amount>[\d,]+)/i,
90
+ type: :financial,
91
+ handler: :extract_revenue
92
+ }
93
+ ]
94
+
95
+ def extract(content)
96
+ facts = super(content)
97
+ facts + extract_custom_patterns(content)
98
+ end
99
+
100
+ private
101
+
102
+ def extract_custom_patterns(content)
103
+ facts = []
104
+ CUSTOM_PATTERNS.each do |rule|
105
+ content.raw_text.scan(rule[:pattern]) do |match|
106
+ facts << send(rule[:handler], match, content)
107
+ end
108
+ end
109
+ facts
110
+ end
111
+
112
+ def extract_revenue(match, content)
113
+ Models::Fact.create!(
114
+ fact_text: "Revenue of $#{match[:amount]}",
115
+ valid_at: content.captured_at,
116
+ extraction_method: "rule_based",
117
+ # ...
118
+ )
119
+ end
120
+ end
121
+ ```
122
+
123
+ ## Advantages
124
+
125
+ - Fast execution
126
+ - No external dependencies
127
+ - Predictable results
128
+ - Works offline
129
+ - Zero cost
130
+
131
+ ## Disadvantages
132
+
133
+ - Limited to defined patterns
134
+ - Misses implicit facts
135
+ - Requires pattern maintenance
136
+ - May produce false positives
137
+
138
+ ## Best Practices
139
+
140
+ ### 1. Combine with LLM
141
+
142
+ ```ruby
143
+ # Use rule-based for structured content
144
+ if content.content_type == "form"
145
+ facts = rule_extractor.extract(content)
146
+ else
147
+ facts = llm_extractor.extract(content)
148
+ end
149
+ ```
150
+
151
+ ### 2. Validate Matches
152
+
153
+ ```ruby
154
+ facts = extractor.extract(content)
155
+ facts.select { |f| f.confidence > 0.8 }
156
+ ```
157
+
158
+ ### 3. Log Unmatched Content
159
+
160
+ ```ruby
161
+ facts = extractor.extract(content)
162
+ if facts.empty?
163
+ logger.info "No patterns matched for content #{content.id}"
164
+ end
165
+ ```
data/docs/api/facts.md ADDED
@@ -0,0 +1,300 @@
1
+ # Facts
2
+
3
+ The main interface for FactDb operations.
4
+
5
+ ## Class: `FactDb::Facts`
6
+
7
+ ```ruby
8
+ facts = FactDb.new
9
+ # or
10
+ facts = FactDb::Facts.new(config: custom_config)
11
+ ```
12
+
13
+ ## Attributes
14
+
15
+ | Attribute | Type | Description |
16
+ |-----------|------|-------------|
17
+ | `config` | Config | Configuration instance |
18
+ | `content_service` | ContentService | Service for content operations |
19
+ | `entity_service` | EntityService | Service for entity operations |
20
+ | `fact_service` | FactService | Service for fact operations |
21
+ | `extraction_pipeline` | ExtractionPipeline | Pipeline for batch extraction |
22
+ | `resolution_pipeline` | ResolutionPipeline | Pipeline for batch resolution |
23
+
24
+ ## Methods
25
+
26
+ ### initialize
27
+
28
+ ```ruby
29
+ def initialize(config: nil)
30
+ ```
31
+
32
+ Create a new Facts instance.
33
+
34
+ **Parameters:**
35
+
36
+ - `config` (Config, optional) - Configuration instance. Uses `FactDb.config` if not provided.
37
+
38
+ **Example:**
39
+
40
+ ```ruby
41
+ # Use default configuration
42
+ facts = FactDb.new
43
+
44
+ # Use custom configuration
45
+ config = FactDb::Config.new
46
+ config.database_url = "postgresql://localhost/my_db"
47
+ facts = FactDb.new(config: config)
48
+ ```
49
+
50
+ ---
51
+
52
+ ### ingest
53
+
54
+ ```ruby
55
+ def ingest(raw_text, type:, captured_at: Time.current, metadata: {}, title: nil, source_uri: nil)
56
+ ```
57
+
58
+ Ingest raw content into the fact database.
59
+
60
+ **Parameters:**
61
+
62
+ - `raw_text` (String) - The content text
63
+ - `type` (Symbol) - Content type (:email, :document, :article, etc.)
64
+ - `captured_at` (Time, optional) - When content was captured
65
+ - `metadata` (Hash, optional) - Additional metadata
66
+ - `title` (String, optional) - Content title
67
+ - `source_uri` (String, optional) - Original location
68
+
69
+ **Returns:** `Models::Content`
70
+
71
+ **Example:**
72
+
73
+ ```ruby
74
+ content = facts.ingest(
75
+ "Paula joined Microsoft on Jan 10, 2024",
76
+ type: :announcement,
77
+ title: "New Hire",
78
+ captured_at: Time.current
79
+ )
80
+ ```
81
+
82
+ ---
83
+
84
+ ### extract_facts
85
+
86
+ ```ruby
87
+ def extract_facts(content_id, extractor: @config.default_extractor)
88
+ ```
89
+
90
+ Extract facts from content.
91
+
92
+ **Parameters:**
93
+
94
+ - `content_id` (Integer) - Content ID
95
+ - `extractor` (Symbol, optional) - Extraction method (:manual, :llm, :rule_based)
96
+
97
+ **Returns:** `Array<Models::Fact>`
98
+
99
+ **Example:**
100
+
101
+ ```ruby
102
+ extracted = facts.extract_facts(content.id, extractor: :llm)
103
+ ```
104
+
105
+ ---
106
+
107
+ ### query_facts
108
+
109
+ ```ruby
110
+ def query_facts(topic: nil, at: nil, entity: nil, status: :canonical)
111
+ ```
112
+
113
+ Query facts with temporal and entity filtering.
114
+
115
+ **Parameters:**
116
+
117
+ - `topic` (String, optional) - Text search query
118
+ - `at` (Date/Time, optional) - Point in time (nil = current)
119
+ - `entity` (Integer, optional) - Entity ID filter
120
+ - `status` (Symbol, optional) - Fact status filter
121
+
122
+ **Returns:** `ActiveRecord::Relation<Models::Fact>`
123
+
124
+ **Example:**
125
+
126
+ ```ruby
127
+ # Current facts about Paula
128
+ results = facts.query_facts(entity: paula.id)
129
+
130
+ # Facts on a topic
131
+ results = facts.query_facts(topic: "engineering")
132
+
133
+ # Historical query
134
+ results = facts.query_facts(at: Date.parse("2023-06-15"))
135
+ ```
136
+
137
+ ---
138
+
139
+ ### resolve_entity
140
+
141
+ ```ruby
142
+ def resolve_entity(name, type: nil)
143
+ ```
144
+
145
+ Resolve a name to an entity.
146
+
147
+ **Parameters:**
148
+
149
+ - `name` (String) - Name to resolve
150
+ - `type` (Symbol, optional) - Entity type filter
151
+
152
+ **Returns:** `Models::Entity` or `nil`
153
+
154
+ **Example:**
155
+
156
+ ```ruby
157
+ entity = facts.resolve_entity("Paula Chen", type: :person)
158
+ ```
159
+
160
+ ---
161
+
162
+ ### timeline_for
163
+
164
+ ```ruby
165
+ def timeline_for(entity_id, from: nil, to: nil)
166
+ ```
167
+
168
+ Build a timeline for an entity.
169
+
170
+ **Parameters:**
171
+
172
+ - `entity_id` (Integer) - Entity ID
173
+ - `from` (Date/Time, optional) - Start of range
174
+ - `to` (Date/Time, optional) - End of range
175
+
176
+ **Returns:** `Array<Models::Fact>`
177
+
178
+ **Example:**
179
+
180
+ ```ruby
181
+ timeline = facts.timeline_for(paula.id, from: "2023-01-01", to: "2024-12-31")
182
+ ```
183
+
184
+ ---
185
+
186
+ ### current_facts_for
187
+
188
+ ```ruby
189
+ def current_facts_for(entity_id)
190
+ ```
191
+
192
+ Get currently valid facts about an entity.
193
+
194
+ **Parameters:**
195
+
196
+ - `entity_id` (Integer) - Entity ID
197
+
198
+ **Returns:** `ActiveRecord::Relation<Models::Fact>`
199
+
200
+ **Example:**
201
+
202
+ ```ruby
203
+ current = facts.current_facts_for(paula.id)
204
+ ```
205
+
206
+ ---
207
+
208
+ ### facts_at
209
+
210
+ ```ruby
211
+ def facts_at(at, entity: nil, topic: nil)
212
+ ```
213
+
214
+ Get facts valid at a specific point in time.
215
+
216
+ **Parameters:**
217
+
218
+ - `at` (Date/Time) - Point in time
219
+ - `entity` (Integer, optional) - Entity ID filter
220
+ - `topic` (String, optional) - Text search query
221
+
222
+ **Returns:** `ActiveRecord::Relation<Models::Fact>`
223
+
224
+ **Example:**
225
+
226
+ ```ruby
227
+ historical = facts.facts_at(Date.parse("2023-06-15"), entity: paula.id)
228
+ ```
229
+
230
+ ---
231
+
232
+ ### batch_extract
233
+
234
+ ```ruby
235
+ def batch_extract(content_ids, extractor: @config.default_extractor, parallel: true)
236
+ ```
237
+
238
+ Batch extract facts from multiple content items.
239
+
240
+ **Parameters:**
241
+
242
+ - `content_ids` (Array<Integer>) - Content IDs to process
243
+ - `extractor` (Symbol, optional) - Extraction method
244
+ - `parallel` (Boolean, optional) - Use parallel processing (default: true)
245
+
246
+ **Returns:** `Array<Hash>` - Results per content
247
+
248
+ **Example:**
249
+
250
+ ```ruby
251
+ results = facts.batch_extract([c1.id, c2.id, c3.id], parallel: true)
252
+ results.each do |r|
253
+ puts "#{r[:content_id]}: #{r[:facts].count} facts"
254
+ end
255
+ ```
256
+
257
+ ---
258
+
259
+ ### batch_resolve_entities
260
+
261
+ ```ruby
262
+ def batch_resolve_entities(names, type: nil)
263
+ ```
264
+
265
+ Batch resolve entity names.
266
+
267
+ **Parameters:**
268
+
269
+ - `names` (Array<String>) - Names to resolve
270
+ - `type` (Symbol, optional) - Entity type filter
271
+
272
+ **Returns:** `Array<Hash>` - Resolution results
273
+
274
+ **Example:**
275
+
276
+ ```ruby
277
+ results = facts.batch_resolve_entities(["Paula", "Microsoft"])
278
+ ```
279
+
280
+ ---
281
+
282
+ ### detect_fact_conflicts
283
+
284
+ ```ruby
285
+ def detect_fact_conflicts(entity_ids)
286
+ ```
287
+
288
+ Detect fact conflicts for multiple entities.
289
+
290
+ **Parameters:**
291
+
292
+ - `entity_ids` (Array<Integer>) - Entity IDs to check
293
+
294
+ **Returns:** `Array<Hash>` - Conflict detection results
295
+
296
+ **Example:**
297
+
298
+ ```ruby
299
+ conflicts = facts.detect_fact_conflicts([paula.id, john.id])
300
+ ```