fact_db 0.0.2 → 0.0.3
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.
- checksums.yaml +4 -4
- data/.envrc +2 -0
- data/.yardopts +5 -0
- data/CHANGELOG.md +64 -0
- data/README.md +107 -6
- data/Rakefile +243 -10
- data/db/migrate/001_enable_extensions.rb +1 -0
- data/db/migrate/002_create_sources.rb +49 -0
- data/db/migrate/003_create_entities.rb +27 -15
- data/db/migrate/004_create_entity_aliases.rb +20 -7
- data/db/migrate/005_create_facts.rb +37 -21
- data/db/migrate/006_create_entity_mentions.rb +14 -6
- data/db/migrate/007_create_fact_sources.rb +16 -8
- data/docs/api/extractors/index.md +5 -5
- data/docs/api/extractors/llm.md +17 -17
- data/docs/api/extractors/rule-based.md +14 -14
- data/docs/api/facts.md +20 -20
- data/docs/api/index.md +4 -4
- data/docs/api/models/entity.md +21 -21
- data/docs/api/models/fact.md +15 -15
- data/docs/api/models/index.md +7 -7
- data/docs/api/models/{content.md → source.md} +29 -29
- data/docs/api/pipeline/extraction.md +25 -25
- data/docs/api/pipeline/index.md +1 -1
- data/docs/api/pipeline/resolution.md +4 -4
- data/docs/api/services/entity-service.md +20 -20
- data/docs/api/services/fact-service.md +12 -12
- data/docs/api/services/index.md +5 -5
- data/docs/api/services/{content-service.md → source-service.md} +27 -27
- data/docs/architecture/database-schema.md +46 -46
- data/docs/architecture/entity-resolution.md +6 -6
- data/docs/architecture/index.md +10 -10
- data/docs/architecture/temporal-facts.md +5 -5
- data/docs/architecture/three-layer-model.md +17 -17
- data/docs/concepts.md +6 -6
- data/docs/examples/basic-usage.md +20 -20
- data/docs/examples/hr-onboarding.md +17 -17
- data/docs/examples/index.md +4 -4
- data/docs/examples/news-analysis.md +23 -23
- data/docs/getting-started/database-setup.md +28 -20
- data/docs/getting-started/index.md +3 -3
- data/docs/getting-started/quick-start.md +33 -30
- data/docs/guides/batch-processing.md +26 -26
- data/docs/guides/configuration.md +158 -77
- data/docs/guides/entity-management.md +14 -14
- data/docs/guides/extracting-facts.md +28 -28
- data/docs/guides/ingesting-content.md +14 -14
- data/docs/guides/llm-integration.md +40 -32
- data/docs/guides/temporal-queries.md +11 -11
- data/docs/index.md +6 -2
- data/examples/.envrc +4 -0
- data/examples/.gitignore +1 -0
- data/examples/001_configuration.rb +312 -0
- data/examples/{basic_usage.rb → 010_basic_usage.rb} +47 -56
- data/examples/{entity_management.rb → 020_entity_management.rb} +57 -72
- data/examples/{temporal_queries.rb → 030_temporal_queries.rb} +39 -59
- data/examples/040_output_formats.rb +177 -0
- data/examples/{rule_based_extraction.rb → 050_rule_based_extraction.rb} +39 -45
- data/examples/060_fluent_temporal_api.rb +217 -0
- data/examples/070_introspection.rb +252 -0
- data/examples/{hr_system.rb → 080_hr_system.rb} +56 -75
- data/examples/090_ingest_demo.rb +515 -0
- data/examples/100_query_context.rb +668 -0
- data/examples/110_prove_it.rb +204 -0
- data/examples/120_dump_database.rb +358 -0
- data/examples/130_rag_feedback_loop.rb +858 -0
- data/examples/README.md +229 -15
- data/examples/data/lincoln_associates.md +201 -0
- data/examples/data/lincoln_biography.md +66 -0
- data/examples/data/lincoln_cabinet.md +243 -0
- data/examples/data/lincoln_family.md +163 -0
- data/examples/data/lincoln_military.md +241 -0
- data/examples/data/lincoln_todd_family.md +136 -0
- data/examples/ingest_reporter.rb +335 -0
- data/examples/utilities.rb +182 -0
- data/lib/fact_db/config/defaults.yml +254 -0
- data/lib/fact_db/config.rb +94 -35
- data/lib/fact_db/database.rb +98 -8
- data/lib/fact_db/extractors/base.rb +106 -21
- data/lib/fact_db/extractors/llm_extractor.rb +35 -63
- data/lib/fact_db/extractors/manual_extractor.rb +46 -6
- data/lib/fact_db/extractors/rule_based_extractor.rb +136 -25
- data/lib/fact_db/llm/adapter.rb +3 -3
- data/lib/fact_db/models/entity.rb +94 -22
- data/lib/fact_db/models/entity_alias.rb +41 -7
- data/lib/fact_db/models/entity_mention.rb +34 -1
- data/lib/fact_db/models/fact.rb +259 -28
- data/lib/fact_db/models/fact_source.rb +43 -9
- data/lib/fact_db/models/source.rb +113 -0
- data/lib/fact_db/pipeline/extraction_pipeline.rb +35 -35
- data/lib/fact_db/pipeline/resolution_pipeline.rb +5 -5
- data/lib/fact_db/query_result.rb +202 -0
- data/lib/fact_db/resolution/entity_resolver.rb +139 -39
- data/lib/fact_db/resolution/fact_resolver.rb +86 -14
- data/lib/fact_db/services/entity_service.rb +246 -37
- data/lib/fact_db/services/fact_service.rb +254 -17
- data/lib/fact_db/services/source_service.rb +164 -0
- data/lib/fact_db/temporal/query.rb +71 -7
- data/lib/fact_db/temporal/query_builder.rb +69 -0
- data/lib/fact_db/temporal/timeline.rb +102 -11
- data/lib/fact_db/transformers/base.rb +77 -0
- data/lib/fact_db/transformers/cypher_transformer.rb +185 -0
- data/lib/fact_db/transformers/json_transformer.rb +17 -0
- data/lib/fact_db/transformers/raw_transformer.rb +35 -0
- data/lib/fact_db/transformers/text_transformer.rb +114 -0
- data/lib/fact_db/transformers/triple_transformer.rb +138 -0
- data/lib/fact_db/validation/alias_filter.rb +185 -0
- data/lib/fact_db/version.rb +1 -1
- data/lib/fact_db.rb +281 -30
- data/mkdocs.yml +2 -2
- metadata +60 -16
- data/db/migrate/002_create_contents.rb +0 -44
- data/lib/fact_db/models/content.rb +0 -62
- data/lib/fact_db/services/content_service.rb +0 -93
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Output Formats Example for FactDb
|
|
5
|
+
#
|
|
6
|
+
# This example demonstrates:
|
|
7
|
+
# - Different output formats for LLM consumption
|
|
8
|
+
# - JSON, Triples, Cypher, and Text transformers
|
|
9
|
+
# - How each format represents the same data differently
|
|
10
|
+
|
|
11
|
+
require_relative "utilities"
|
|
12
|
+
|
|
13
|
+
demo_setup!("FactDb Output Formats Demo")
|
|
14
|
+
demo_configure_logging(__FILE__)
|
|
15
|
+
|
|
16
|
+
facts = FactDb.new
|
|
17
|
+
entity_service = facts.entity_service
|
|
18
|
+
fact_service = facts.fact_service
|
|
19
|
+
|
|
20
|
+
demo_section("Setup: Creating Sample Data")
|
|
21
|
+
|
|
22
|
+
# Create entities
|
|
23
|
+
paula = entity_service.resolve_or_create(
|
|
24
|
+
"Paula Chen",
|
|
25
|
+
kind: :person,
|
|
26
|
+
aliases: ["P. Chen"],
|
|
27
|
+
description: "Principal Engineer"
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
microsoft = entity_service.resolve_or_create(
|
|
31
|
+
"Microsoft",
|
|
32
|
+
kind: :organization,
|
|
33
|
+
aliases: ["MSFT"],
|
|
34
|
+
description: "Technology company"
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
seattle = entity_service.resolve_or_create(
|
|
38
|
+
"Seattle",
|
|
39
|
+
kind: :place,
|
|
40
|
+
description: "City in Washington state"
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
puts "Created entities: #{paula.name}, #{microsoft.name}, #{seattle.name}"
|
|
44
|
+
|
|
45
|
+
# Create facts
|
|
46
|
+
fact_service.create(
|
|
47
|
+
"Paula Chen is Principal Engineer at Microsoft",
|
|
48
|
+
valid_at: Date.new(2024, 1, 10),
|
|
49
|
+
extraction_method: :manual,
|
|
50
|
+
confidence: 1.0,
|
|
51
|
+
mentions: [
|
|
52
|
+
{ entity_id: paula.id, role: :subject, text: "Paula Chen" },
|
|
53
|
+
{ entity_id: microsoft.id, role: :object, text: "Microsoft" }
|
|
54
|
+
]
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
fact_service.create(
|
|
58
|
+
"Paula Chen works at the Seattle office",
|
|
59
|
+
valid_at: Date.new(2024, 1, 10),
|
|
60
|
+
extraction_method: :manual,
|
|
61
|
+
confidence: 0.95,
|
|
62
|
+
mentions: [
|
|
63
|
+
{ entity_id: paula.id, role: :subject, text: "Paula Chen" },
|
|
64
|
+
{ entity_id: seattle.id, role: :location, text: "Seattle" }
|
|
65
|
+
]
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
fact_service.create(
|
|
69
|
+
"Paula Chen reports to Sarah Kim",
|
|
70
|
+
valid_at: Date.new(2024, 1, 10),
|
|
71
|
+
extraction_method: :manual,
|
|
72
|
+
confidence: 0.9,
|
|
73
|
+
mentions: [
|
|
74
|
+
{ entity_id: paula.id, role: :subject, text: "Paula Chen" }
|
|
75
|
+
]
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
puts "Created facts about Paula Chen"
|
|
79
|
+
|
|
80
|
+
demo_section("Section 1: JSON Format (Default)")
|
|
81
|
+
|
|
82
|
+
json_results = facts.query_facts(entity: paula.id, format: :json)
|
|
83
|
+
puts "\nJSON output:"
|
|
84
|
+
puts JSON.pretty_generate(json_results.to_h)
|
|
85
|
+
|
|
86
|
+
demo_section("Section 2: Triples Format (Subject-Predicate-Object)")
|
|
87
|
+
|
|
88
|
+
triples_results = facts.query_facts(entity: paula.id, format: :triples)
|
|
89
|
+
puts "\nTriples output:"
|
|
90
|
+
triples_results.each do |triple|
|
|
91
|
+
puts " #{triple.inspect}"
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
puts <<~EXPLANATION
|
|
95
|
+
|
|
96
|
+
Triples format is ideal for:
|
|
97
|
+
- Knowledge graph representations
|
|
98
|
+
- Semantic reasoning
|
|
99
|
+
- LLM structured understanding
|
|
100
|
+
EXPLANATION
|
|
101
|
+
|
|
102
|
+
demo_section("Section 3: Cypher Format (Graph Notation)")
|
|
103
|
+
|
|
104
|
+
cypher_results = facts.query_facts(entity: paula.id, format: :cypher)
|
|
105
|
+
puts "\nCypher output:"
|
|
106
|
+
puts cypher_results
|
|
107
|
+
|
|
108
|
+
puts <<~EXPLANATION
|
|
109
|
+
|
|
110
|
+
Cypher format is ideal for:
|
|
111
|
+
- Graph database imports (Neo4j compatible)
|
|
112
|
+
- Visualizing entity relationships
|
|
113
|
+
- Understanding connection patterns
|
|
114
|
+
EXPLANATION
|
|
115
|
+
|
|
116
|
+
demo_section("Section 4: Text Format (Human-Readable)")
|
|
117
|
+
|
|
118
|
+
text_results = facts.query_facts(entity: paula.id, format: :text)
|
|
119
|
+
puts "\nText output:"
|
|
120
|
+
puts text_results
|
|
121
|
+
|
|
122
|
+
puts <<~EXPLANATION
|
|
123
|
+
|
|
124
|
+
Text format is ideal for:
|
|
125
|
+
- Direct LLM consumption
|
|
126
|
+
- Human debugging
|
|
127
|
+
- Report generation
|
|
128
|
+
EXPLANATION
|
|
129
|
+
|
|
130
|
+
demo_section("Section 5: Raw Format (ActiveRecord Objects)")
|
|
131
|
+
|
|
132
|
+
raw_results = facts.query_facts(entity: paula.id, format: :raw)
|
|
133
|
+
puts "\nRaw output (first result):"
|
|
134
|
+
if raw_results.respond_to?(:raw_facts) && raw_results.raw_facts.any?
|
|
135
|
+
fact = raw_results.raw_facts.first
|
|
136
|
+
puts " Class: #{fact.class}"
|
|
137
|
+
puts " ID: #{fact.id}"
|
|
138
|
+
puts " Text: #{fact.text}"
|
|
139
|
+
puts " Valid at: #{fact.valid_at}"
|
|
140
|
+
else
|
|
141
|
+
puts " Results: #{raw_results.inspect}"
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
demo_section("Section 6: Format Comparison - Same Query, Different Views")
|
|
145
|
+
|
|
146
|
+
puts "\nQuerying current facts for Paula Chen in all formats:\n"
|
|
147
|
+
|
|
148
|
+
formats = %i[json triples cypher text]
|
|
149
|
+
|
|
150
|
+
formats.each do |format|
|
|
151
|
+
puts "\n--- #{format.upcase} ---"
|
|
152
|
+
result = facts.current_facts_for(paula.id, format: format)
|
|
153
|
+
|
|
154
|
+
case format
|
|
155
|
+
when :json
|
|
156
|
+
if result.respond_to?(:to_h)
|
|
157
|
+
puts "#{result.fact_count} facts, #{result.entity_count} entities"
|
|
158
|
+
else
|
|
159
|
+
puts "#{result.size} facts" if result.respond_to?(:size)
|
|
160
|
+
end
|
|
161
|
+
when :triples
|
|
162
|
+
puts "#{result.size} triples generated"
|
|
163
|
+
when :cypher
|
|
164
|
+
puts "#{result.lines.count} lines of Cypher notation"
|
|
165
|
+
when :text
|
|
166
|
+
puts result.lines.first(5).join
|
|
167
|
+
puts "..." if result.lines.count > 5
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
demo_section("Section 7: Temporal Queries with Formats")
|
|
172
|
+
|
|
173
|
+
puts "\nFacts at specific date (2024-06-01) in Cypher format:"
|
|
174
|
+
temporal_cypher = facts.facts_at(Date.new(2024, 6, 1), entity: paula.id, format: :cypher)
|
|
175
|
+
puts temporal_cypher
|
|
176
|
+
|
|
177
|
+
demo_footer
|
|
@@ -9,28 +9,22 @@
|
|
|
9
9
|
# - Processing extracted facts into the database
|
|
10
10
|
# - Handling extraction results
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
require_relative "utilities"
|
|
13
|
+
|
|
14
|
+
demo_setup!("FactDb Rule-Based Extraction Demo")
|
|
15
|
+
demo_configure_logging(__FILE__)
|
|
14
16
|
|
|
15
17
|
FactDb.configure do |config|
|
|
16
|
-
config.database_url = ENV.fetch("DATABASE_URL", "postgres://#{ENV['USER']}@localhost/fact_db_demo")
|
|
17
18
|
config.default_extractor = :rule_based
|
|
18
19
|
end
|
|
19
20
|
|
|
20
|
-
|
|
21
|
-
FactDb::Database.migrate!
|
|
22
|
-
|
|
23
|
-
clock = FactDb.new
|
|
24
|
-
|
|
25
|
-
puts "=" * 60
|
|
26
|
-
puts "FactDb Rule-Based Extraction Demo"
|
|
27
|
-
puts "=" * 60
|
|
21
|
+
facts = FactDb.new
|
|
28
22
|
|
|
29
23
|
# Sample documents to process
|
|
30
24
|
documents = [
|
|
31
25
|
{
|
|
32
26
|
title: "Company Announcement",
|
|
33
|
-
|
|
27
|
+
kind: :document,
|
|
34
28
|
text: <<~TEXT
|
|
35
29
|
FOR IMMEDIATE RELEASE - January 15, 2026
|
|
36
30
|
|
|
@@ -51,7 +45,7 @@ documents = [
|
|
|
51
45
|
},
|
|
52
46
|
{
|
|
53
47
|
title: "HR Update Email",
|
|
54
|
-
|
|
48
|
+
kind: :email,
|
|
55
49
|
text: <<~TEXT
|
|
56
50
|
From: hr@example.com
|
|
57
51
|
Subject: Team Updates - Q1 2026
|
|
@@ -77,7 +71,7 @@ documents = [
|
|
|
77
71
|
},
|
|
78
72
|
{
|
|
79
73
|
title: "Meeting Notes",
|
|
80
|
-
|
|
74
|
+
kind: :meeting_notes,
|
|
81
75
|
text: <<~TEXT
|
|
82
76
|
Project Status Meeting - January 22, 2026
|
|
83
77
|
|
|
@@ -99,8 +93,10 @@ documents = [
|
|
|
99
93
|
# Create the rule-based extractor
|
|
100
94
|
extractor = FactDb::Extractors::Base.for(:rule_based)
|
|
101
95
|
|
|
102
|
-
#
|
|
103
|
-
|
|
96
|
+
# Store ingested content for later use
|
|
97
|
+
ingested_content = {}
|
|
98
|
+
|
|
99
|
+
demo_section("Section 1: Process Each Document")
|
|
104
100
|
|
|
105
101
|
documents.each_with_index do |doc, index|
|
|
106
102
|
puts "\n#{'=' * 40}"
|
|
@@ -108,12 +104,13 @@ documents.each_with_index do |doc, index|
|
|
|
108
104
|
puts "=" * 40
|
|
109
105
|
|
|
110
106
|
# Ingest the content
|
|
111
|
-
content =
|
|
107
|
+
content = facts.ingest(
|
|
112
108
|
doc[:text],
|
|
113
|
-
|
|
109
|
+
kind: doc[:kind],
|
|
114
110
|
title: doc[:title],
|
|
115
111
|
captured_at: Time.now
|
|
116
112
|
)
|
|
113
|
+
ingested_content[index] = content
|
|
117
114
|
puts "Ingested content ID: #{content.id}"
|
|
118
115
|
|
|
119
116
|
# Extract facts and entities
|
|
@@ -136,23 +133,25 @@ documents.each_with_index do |doc, index|
|
|
|
136
133
|
if entities.any?
|
|
137
134
|
puts "\nExtracted #{entities.length} entities:"
|
|
138
135
|
entities.each do |entity|
|
|
139
|
-
puts " - #{entity[:name]} (#{entity[:
|
|
136
|
+
puts " - #{entity[:name]} (#{entity[:kind]})"
|
|
140
137
|
end
|
|
141
138
|
end
|
|
142
139
|
end
|
|
143
140
|
|
|
144
|
-
|
|
145
|
-
puts "\n\n--- Section 2: Saving Extracted Facts ---\n"
|
|
141
|
+
demo_section("Section 2: Saving Extracted Facts")
|
|
146
142
|
|
|
147
|
-
entity_service =
|
|
148
|
-
fact_service =
|
|
143
|
+
entity_service = facts.entity_service
|
|
144
|
+
fact_service = facts.fact_service
|
|
149
145
|
|
|
150
|
-
# Process
|
|
151
|
-
sample_doc = documents
|
|
152
|
-
content =
|
|
146
|
+
# Process the third document (Meeting Notes) which has extractable facts
|
|
147
|
+
sample_doc = documents[2] # "Meeting Notes" extracts 2 facts
|
|
148
|
+
content = ingested_content[2]
|
|
153
149
|
context = { source_id: content.id, captured_at: content.captured_at }
|
|
154
150
|
result = extractor.extract(sample_doc[:text], context)
|
|
155
151
|
|
|
152
|
+
puts "Processing: #{sample_doc[:title]}"
|
|
153
|
+
puts "Found #{result.length} facts to save"
|
|
154
|
+
|
|
156
155
|
result.each do |fact_data|
|
|
157
156
|
# Resolve or create mentioned entities
|
|
158
157
|
mention_records = []
|
|
@@ -161,7 +160,7 @@ result.each do |fact_data|
|
|
|
161
160
|
# Try to resolve the entity, create if not found
|
|
162
161
|
entity = entity_service.resolve_or_create(
|
|
163
162
|
mention[:name],
|
|
164
|
-
|
|
163
|
+
kind: mention[:kind] || :unknown,
|
|
165
164
|
description: "Auto-extracted entity"
|
|
166
165
|
)
|
|
167
166
|
|
|
@@ -184,30 +183,28 @@ result.each do |fact_data|
|
|
|
184
183
|
)
|
|
185
184
|
|
|
186
185
|
# Link to source content
|
|
187
|
-
fact.add_source(
|
|
186
|
+
fact.add_source(source: content, kind: :primary, confidence: fact_data[:confidence])
|
|
188
187
|
|
|
189
|
-
puts "Saved fact: #{fact.
|
|
188
|
+
puts "Saved fact: #{fact.text}"
|
|
190
189
|
puts " ID: #{fact.id}, Mentions: #{fact.entity_mentions.count}"
|
|
191
190
|
end
|
|
192
191
|
|
|
193
|
-
|
|
194
|
-
puts "\n--- Section 3: Querying Extracted Data ---\n"
|
|
192
|
+
demo_section("Section 3: Query the Extracted Data")
|
|
195
193
|
|
|
196
194
|
# Find all extracted entities
|
|
197
195
|
puts "\nAll extracted entities:"
|
|
198
|
-
FactDb::Models::Entity.where(resolution_status: :resolved).order(:
|
|
196
|
+
FactDb::Models::Entity.where(resolution_status: :resolved).order(:name).each do |entity|
|
|
199
197
|
fact_count = entity.facts.count
|
|
200
|
-
puts " #{entity.
|
|
198
|
+
puts " #{entity.name} (#{entity.kind}) - #{fact_count} facts"
|
|
201
199
|
end
|
|
202
200
|
|
|
203
201
|
# Find facts by extraction method
|
|
204
202
|
puts "\nFacts extracted by rule-based extractor:"
|
|
205
203
|
FactDb::Models::Fact.by_extraction_method(:rule_based).limit(10).each do |fact|
|
|
206
|
-
puts " [#{fact.confidence}] #{fact.
|
|
204
|
+
puts " [#{fact.confidence}] #{fact.text}"
|
|
207
205
|
end
|
|
208
206
|
|
|
209
|
-
|
|
210
|
-
puts "\n--- Section 4: Extraction Pattern Examples ---\n"
|
|
207
|
+
demo_section("Section 4: Pattern Examples")
|
|
211
208
|
|
|
212
209
|
test_patterns = [
|
|
213
210
|
"John Smith works at Acme Corp as a Senior Engineer.",
|
|
@@ -235,14 +232,13 @@ test_patterns.each do |pattern|
|
|
|
235
232
|
end
|
|
236
233
|
end
|
|
237
234
|
|
|
238
|
-
|
|
239
|
-
puts "\n--- Section 5: Extraction Statistics ---\n"
|
|
235
|
+
demo_section("Section 5: Statistics")
|
|
240
236
|
|
|
241
|
-
|
|
242
|
-
fact_stats =
|
|
243
|
-
entity_stats = entity_service.stats
|
|
237
|
+
source_stats = facts.source_service.stats
|
|
238
|
+
fact_stats = facts.fact_service.stats
|
|
239
|
+
entity_stats = facts.entity_service.stats
|
|
244
240
|
|
|
245
|
-
puts "Content ingested: #{
|
|
241
|
+
puts "Content ingested: #{source_stats[:total]}"
|
|
246
242
|
puts "Entities created: #{entity_stats[:total]}"
|
|
247
243
|
puts "Facts extracted: #{fact_stats[:total]}"
|
|
248
244
|
|
|
@@ -253,6 +249,4 @@ if fact_stats[:by_extraction_method]
|
|
|
253
249
|
end
|
|
254
250
|
end
|
|
255
251
|
|
|
256
|
-
|
|
257
|
-
puts "Rule-Based Extraction Demo Complete!"
|
|
258
|
-
puts "=" * 60
|
|
252
|
+
demo_footer
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Fluent Temporal API Example for FactDb
|
|
5
|
+
#
|
|
6
|
+
# This example demonstrates:
|
|
7
|
+
# - The new fluent query builder API: facts.at(date).query(...)
|
|
8
|
+
# - Comparing facts between two dates with diff()
|
|
9
|
+
# - Point-in-time queries with chaining
|
|
10
|
+
# - Getting entity state at specific moments
|
|
11
|
+
|
|
12
|
+
require_relative "utilities"
|
|
13
|
+
|
|
14
|
+
demo_setup!("FactDb Fluent Temporal API Demo")
|
|
15
|
+
demo_configure_logging(__FILE__)
|
|
16
|
+
|
|
17
|
+
facts = FactDb.new
|
|
18
|
+
entity_service = facts.entity_service
|
|
19
|
+
fact_service = facts.fact_service
|
|
20
|
+
|
|
21
|
+
demo_section("Setup: Creating Career Progression Data")
|
|
22
|
+
|
|
23
|
+
# Create entities
|
|
24
|
+
alex = entity_service.resolve_or_create(
|
|
25
|
+
"Alex Rivera",
|
|
26
|
+
kind: :person,
|
|
27
|
+
description: "Software professional"
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
startup = entity_service.resolve_or_create(
|
|
31
|
+
"TechStartup Inc",
|
|
32
|
+
kind: :organization,
|
|
33
|
+
description: "Early stage company"
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
bigcorp = entity_service.resolve_or_create(
|
|
37
|
+
"BigCorp Systems",
|
|
38
|
+
kind: :organization,
|
|
39
|
+
description: "Enterprise software company"
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
sf = entity_service.resolve_or_create(
|
|
43
|
+
"San Francisco",
|
|
44
|
+
kind: :place
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
nyc = entity_service.resolve_or_create(
|
|
48
|
+
"New York City",
|
|
49
|
+
kind: :place
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
puts "Created entities for Alex's career story"
|
|
53
|
+
|
|
54
|
+
# Create temporal facts showing career progression
|
|
55
|
+
# 2020: Alex joins TechStartup as Junior Developer
|
|
56
|
+
fact_service.find_or_create(
|
|
57
|
+
"Alex Rivera is Junior Developer at TechStartup Inc",
|
|
58
|
+
valid_at: Date.new(2020, 3, 1),
|
|
59
|
+
invalid_at: Date.new(2022, 6, 30),
|
|
60
|
+
extraction_method: :manual,
|
|
61
|
+
mentions: [
|
|
62
|
+
{ entity_id: alex.id, role: :subject, text: "Alex Rivera" },
|
|
63
|
+
{ entity_id: startup.id, role: :object, text: "TechStartup Inc" }
|
|
64
|
+
]
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
fact_service.find_or_create(
|
|
68
|
+
"Alex Rivera works at San Francisco office",
|
|
69
|
+
valid_at: Date.new(2020, 3, 1),
|
|
70
|
+
invalid_at: Date.new(2023, 8, 31),
|
|
71
|
+
extraction_method: :manual,
|
|
72
|
+
mentions: [
|
|
73
|
+
{ entity_id: alex.id, role: :subject, text: "Alex Rivera" },
|
|
74
|
+
{ entity_id: sf.id, role: :location, text: "San Francisco" }
|
|
75
|
+
]
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
# 2022: Promoted to Senior Developer
|
|
79
|
+
fact_service.find_or_create(
|
|
80
|
+
"Alex Rivera is Senior Developer at TechStartup Inc",
|
|
81
|
+
valid_at: Date.new(2022, 7, 1),
|
|
82
|
+
invalid_at: Date.new(2023, 8, 31),
|
|
83
|
+
extraction_method: :manual,
|
|
84
|
+
mentions: [
|
|
85
|
+
{ entity_id: alex.id, role: :subject, text: "Alex Rivera" },
|
|
86
|
+
{ entity_id: startup.id, role: :object, text: "TechStartup Inc" }
|
|
87
|
+
]
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
# 2023: Moves to BigCorp
|
|
91
|
+
fact_service.find_or_create(
|
|
92
|
+
"Alex Rivera is Principal Engineer at BigCorp Systems",
|
|
93
|
+
valid_at: Date.new(2023, 9, 1),
|
|
94
|
+
extraction_method: :manual,
|
|
95
|
+
mentions: [
|
|
96
|
+
{ entity_id: alex.id, role: :subject, text: "Alex Rivera" },
|
|
97
|
+
{ entity_id: bigcorp.id, role: :object, text: "BigCorp Systems" }
|
|
98
|
+
]
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
fact_service.find_or_create(
|
|
102
|
+
"Alex Rivera works at New York City office",
|
|
103
|
+
valid_at: Date.new(2023, 9, 1),
|
|
104
|
+
extraction_method: :manual,
|
|
105
|
+
mentions: [
|
|
106
|
+
{ entity_id: alex.id, role: :subject, text: "Alex Rivera" },
|
|
107
|
+
{ entity_id: nyc.id, role: :location, text: "New York City" }
|
|
108
|
+
]
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
puts "Created career progression facts (2020-2024)"
|
|
112
|
+
|
|
113
|
+
demo_section("Section 1: Fluent Query Builder - Basic Usage")
|
|
114
|
+
|
|
115
|
+
puts "\nQuery: What was Alex's role in 2021?"
|
|
116
|
+
results_2021 = facts.at("2021-06-15").facts_for(alex.id)
|
|
117
|
+
puts "Date: 2021-06-15"
|
|
118
|
+
results_2021.each_fact do |fact|
|
|
119
|
+
puts " - #{fact[:text]}"
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
puts "\nQuery: What was Alex's role in 2022 (after promotion)?"
|
|
123
|
+
results_2022 = facts.at("2022-09-01").facts_for(alex.id)
|
|
124
|
+
puts "Date: 2022-09-01"
|
|
125
|
+
results_2022.each_fact do |fact|
|
|
126
|
+
puts " - #{fact[:text]}"
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
puts "\nQuery: What is Alex's current situation?"
|
|
130
|
+
results_now = facts.at(Date.today).facts_for(alex.id)
|
|
131
|
+
puts "Date: #{Date.today}"
|
|
132
|
+
results_now.each_fact do |fact|
|
|
133
|
+
puts " - #{fact[:text]}"
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
demo_section("Section 2: Query Builder with Output Formats")
|
|
137
|
+
|
|
138
|
+
puts "\nAlex's 2023 state as Cypher graph:"
|
|
139
|
+
cypher_2023 = facts.at("2023-10-01").facts_for(alex.id, format: :cypher)
|
|
140
|
+
puts cypher_2023
|
|
141
|
+
|
|
142
|
+
puts "\nAlex's 2023 state as Triples:"
|
|
143
|
+
triples_2023 = facts.at("2023-10-01").facts_for(alex.id, format: :triples)
|
|
144
|
+
triples_2023.each { |t| puts " #{t.inspect}" }
|
|
145
|
+
|
|
146
|
+
demo_section("Section 3: Comparing Two Points in Time")
|
|
147
|
+
|
|
148
|
+
puts "\nComparing Alex's situation: 2021-06-01 vs 2024-01-01"
|
|
149
|
+
diff_result = facts.diff(nil, from: "2021-06-01", to: "2024-01-01")
|
|
150
|
+
|
|
151
|
+
puts "\nRemoved facts (no longer true):"
|
|
152
|
+
diff_result[:removed].each do |fact|
|
|
153
|
+
puts " - #{fact.text}"
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
puts "\nAdded facts (became true):"
|
|
157
|
+
diff_result[:added].each do |fact|
|
|
158
|
+
puts " - #{fact.text}"
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
puts "\nUnchanged facts:"
|
|
162
|
+
if diff_result[:unchanged].empty?
|
|
163
|
+
puts " (none - everything changed!)"
|
|
164
|
+
else
|
|
165
|
+
diff_result[:unchanged].each do |fact|
|
|
166
|
+
puts " - #{fact.text}"
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
demo_section("Section 4: Query Builder - Compare To")
|
|
171
|
+
|
|
172
|
+
puts "\nUsing fluent API to compare dates:"
|
|
173
|
+
comparison = facts.at("2020-06-01").compare_to("2023-10-01")
|
|
174
|
+
|
|
175
|
+
puts "\nFrom #{comparison[:from]} to #{comparison[:to]}:"
|
|
176
|
+
puts " Added: #{comparison[:added].count} facts"
|
|
177
|
+
puts " Removed: #{comparison[:removed].count} facts"
|
|
178
|
+
puts " Unchanged: #{comparison[:unchanged].count} facts"
|
|
179
|
+
|
|
180
|
+
demo_section("Section 5: Career Timeline View")
|
|
181
|
+
|
|
182
|
+
checkpoints = [
|
|
183
|
+
Date.new(2020, 6, 1),
|
|
184
|
+
Date.new(2021, 6, 1),
|
|
185
|
+
Date.new(2022, 6, 1),
|
|
186
|
+
Date.new(2022, 8, 1),
|
|
187
|
+
Date.new(2023, 6, 1),
|
|
188
|
+
Date.new(2023, 10, 1),
|
|
189
|
+
Date.new(2024, 1, 1)
|
|
190
|
+
]
|
|
191
|
+
|
|
192
|
+
puts "\nAlex Rivera's Career Timeline:\n"
|
|
193
|
+
checkpoints.each do |date|
|
|
194
|
+
snapshot = facts.at(date).facts_for(alex.id, format: :text)
|
|
195
|
+
fact_count = facts.at(date).facts_for(alex.id).fact_count
|
|
196
|
+
|
|
197
|
+
puts "#{date.strftime('%Y-%m-%d')} (#{fact_count} facts):"
|
|
198
|
+
if fact_count.zero?
|
|
199
|
+
puts " (no facts yet)"
|
|
200
|
+
else
|
|
201
|
+
facts.at(date).facts_for(alex.id).each_fact do |fact|
|
|
202
|
+
puts " - #{fact[:text]}"
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
puts
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
demo_section("Section 6: State For - Entity State at a Date")
|
|
209
|
+
|
|
210
|
+
puts "\nSnapshot of Alex's state on 2022-08-01:"
|
|
211
|
+
state = facts.at("2022-08-01").state_for(alex.id)
|
|
212
|
+
puts "Facts at this moment:"
|
|
213
|
+
state.each_fact do |fact|
|
|
214
|
+
puts " - #{fact[:text]}"
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
demo_footer
|