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
|
@@ -10,28 +10,20 @@
|
|
|
10
10
|
# - Creating facts manually
|
|
11
11
|
# - Querying facts
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
require "
|
|
13
|
+
require_relative "utilities"
|
|
14
|
+
require "amazing_print"
|
|
15
|
+
|
|
16
|
+
demo_setup!("FactDb Basic Usage Demo")
|
|
17
|
+
demo_configure_logging(__FILE__)
|
|
15
18
|
|
|
16
|
-
# Configure FactDb
|
|
17
19
|
FactDb.configure do |config|
|
|
18
|
-
config.database_url = ENV.fetch("DATABASE_URL", "postgres://#{ENV['USER']}@localhost/fact_db_demo")
|
|
19
20
|
config.default_extractor = :manual
|
|
20
|
-
config.fuzzy_match_threshold = 0.85
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
-
#
|
|
24
|
-
FactDb
|
|
25
|
-
|
|
26
|
-
# Create a new FactDb instance (the "clock")
|
|
27
|
-
clock = FactDb.new
|
|
28
|
-
|
|
29
|
-
puts "=" * 60
|
|
30
|
-
puts "FactDb Basic Usage Demo"
|
|
31
|
-
puts "=" * 60
|
|
23
|
+
# Create a new FactDb instance
|
|
24
|
+
facts = FactDb.new
|
|
32
25
|
|
|
33
|
-
|
|
34
|
-
puts "\n--- Step 1: Ingesting Content ---\n"
|
|
26
|
+
demo_section("Step 1: Ingesting Content")
|
|
35
27
|
|
|
36
28
|
email_content = <<~EMAIL
|
|
37
29
|
From: hr@acme.com
|
|
@@ -49,9 +41,9 @@ email_content = <<~EMAIL
|
|
|
49
41
|
HR Department
|
|
50
42
|
EMAIL
|
|
51
43
|
|
|
52
|
-
content =
|
|
44
|
+
content = facts.ingest(
|
|
53
45
|
email_content,
|
|
54
|
-
|
|
46
|
+
kind: :email,
|
|
55
47
|
title: "New Hire Announcement - Jane Smith",
|
|
56
48
|
captured_at: Time.new(2026, 1, 8)
|
|
57
49
|
)
|
|
@@ -60,42 +52,40 @@ puts "Ingested content ID: #{content.id}"
|
|
|
60
52
|
puts "Content hash: #{content.content_hash}"
|
|
61
53
|
puts "Word count: #{content.word_count}"
|
|
62
54
|
|
|
63
|
-
|
|
64
|
-
puts "\n--- Step 2: Creating Entities ---\n"
|
|
55
|
+
demo_section("Step 2: Creating/Finding Entities")
|
|
65
56
|
|
|
66
|
-
entity_service =
|
|
57
|
+
entity_service = facts.entity_service
|
|
67
58
|
|
|
68
|
-
jane = entity_service.
|
|
59
|
+
jane = entity_service.resolve_or_create(
|
|
69
60
|
"Jane Smith",
|
|
70
|
-
|
|
61
|
+
kind: :person,
|
|
71
62
|
aliases: ["J. Smith"],
|
|
72
63
|
description: "Director of Engineering at Acme Corp"
|
|
73
64
|
)
|
|
74
|
-
puts "
|
|
65
|
+
puts "Entity: #{jane.name} (ID: #{jane.id})"
|
|
75
66
|
|
|
76
|
-
acme = entity_service.
|
|
67
|
+
acme = entity_service.resolve_or_create(
|
|
77
68
|
"Acme Corp",
|
|
78
|
-
|
|
69
|
+
kind: :organization,
|
|
79
70
|
aliases: ["Acme", "Acme Corporation"],
|
|
80
71
|
description: "Technology company"
|
|
81
72
|
)
|
|
82
|
-
puts "
|
|
73
|
+
puts "Entity: #{acme.name} (ID: #{acme.id})"
|
|
83
74
|
|
|
84
|
-
techstartup = entity_service.
|
|
75
|
+
techstartup = entity_service.resolve_or_create(
|
|
85
76
|
"TechStartup Inc",
|
|
86
|
-
|
|
77
|
+
kind: :organization,
|
|
87
78
|
aliases: ["TechStartup"],
|
|
88
79
|
description: "Technology startup company"
|
|
89
80
|
)
|
|
90
|
-
puts "
|
|
81
|
+
puts "Entity: #{techstartup.name} (ID: #{techstartup.id})"
|
|
91
82
|
|
|
92
|
-
|
|
93
|
-
puts "\n--- Step 3: Creating Facts ---\n"
|
|
83
|
+
demo_section("Step 3: Creating/Finding Facts")
|
|
94
84
|
|
|
95
|
-
fact_service =
|
|
85
|
+
fact_service = facts.fact_service
|
|
96
86
|
|
|
97
87
|
# Fact 1: Jane works at Acme
|
|
98
|
-
fact1 = fact_service.
|
|
88
|
+
fact1 = fact_service.find_or_create(
|
|
99
89
|
"Jane Smith is Director of Engineering at Acme Corp",
|
|
100
90
|
valid_at: Date.new(2026, 1, 8),
|
|
101
91
|
extraction_method: :manual,
|
|
@@ -105,11 +95,11 @@ fact1 = fact_service.create(
|
|
|
105
95
|
{ entity_id: acme.id, role: :object, text: "Acme Corp" }
|
|
106
96
|
]
|
|
107
97
|
)
|
|
108
|
-
puts "
|
|
98
|
+
puts "Fact: #{fact1.text}"
|
|
109
99
|
puts " Valid from: #{fact1.valid_at}"
|
|
110
100
|
|
|
111
101
|
# Fact 2: Jane previously worked at TechStartup (now invalid)
|
|
112
|
-
fact2 = fact_service.
|
|
102
|
+
fact2 = fact_service.find_or_create(
|
|
113
103
|
"Jane Smith was VP of Engineering at TechStartup Inc",
|
|
114
104
|
valid_at: Date.new(2023, 1, 1),
|
|
115
105
|
invalid_at: Date.new(2026, 1, 7),
|
|
@@ -120,45 +110,46 @@ fact2 = fact_service.create(
|
|
|
120
110
|
{ entity_id: techstartup.id, role: :object, text: "TechStartup Inc" }
|
|
121
111
|
]
|
|
122
112
|
)
|
|
123
|
-
puts "
|
|
113
|
+
puts "Fact: #{fact2.text}"
|
|
124
114
|
puts " Valid from: #{fact2.valid_at} to #{fact2.invalid_at}"
|
|
125
115
|
|
|
126
|
-
# Link facts to source content
|
|
127
|
-
fact1.add_source(
|
|
128
|
-
fact2.add_source(
|
|
116
|
+
# Link facts to source content (skip if already linked)
|
|
117
|
+
fact1.add_source(source: content, kind: :primary, confidence: 1.0) rescue nil
|
|
118
|
+
fact2.add_source(source: content, kind: :supporting, confidence: 0.8) rescue nil
|
|
129
119
|
|
|
130
|
-
|
|
131
|
-
puts "\n--- Step 4: Querying Facts ---\n"
|
|
120
|
+
demo_section("Step 4: Querying Facts")
|
|
132
121
|
|
|
133
122
|
# Get current facts about Jane
|
|
134
123
|
puts "\nCurrent facts about Jane Smith:"
|
|
135
124
|
current_facts = fact_service.current_facts(entity: jane.id)
|
|
136
125
|
current_facts.each do |fact|
|
|
137
|
-
puts " - #{fact.
|
|
126
|
+
puts " - #{fact.text}"
|
|
138
127
|
end
|
|
139
128
|
|
|
140
129
|
# Get facts valid at a specific date (when Jane was at TechStartup)
|
|
141
130
|
puts "\nFacts about Jane on January 1, 2024:"
|
|
142
131
|
past_facts = fact_service.facts_at(Date.new(2024, 1, 1), entity: jane.id)
|
|
143
132
|
past_facts.each do |fact|
|
|
144
|
-
puts " - #{fact.
|
|
133
|
+
puts " - #{fact.text}"
|
|
145
134
|
end
|
|
146
135
|
|
|
147
136
|
# Get all facts (including historical)
|
|
148
137
|
puts "\nAll facts in the system:"
|
|
149
|
-
all_facts =
|
|
150
|
-
all_facts.
|
|
151
|
-
status = fact
|
|
152
|
-
puts " - #{fact
|
|
138
|
+
all_facts = facts.query_facts
|
|
139
|
+
all_facts.each_fact do |fact|
|
|
140
|
+
status = fact[:invalid_at] ? "(historical)" : "(current)"
|
|
141
|
+
puts " - #{fact[:text]} #{status}"
|
|
153
142
|
end
|
|
154
143
|
|
|
155
|
-
|
|
156
|
-
|
|
144
|
+
demo_section("Step 5: Statistics")
|
|
145
|
+
|
|
146
|
+
puts "\nSource stats:"
|
|
147
|
+
ap facts.source_service.stats
|
|
148
|
+
|
|
149
|
+
puts "\nEntity stats:"
|
|
150
|
+
ap entity_service.stats
|
|
157
151
|
|
|
158
|
-
puts "
|
|
159
|
-
|
|
160
|
-
puts "Fact stats: #{fact_service.stats}"
|
|
152
|
+
puts "\nFact stats:"
|
|
153
|
+
ap fact_service.stats
|
|
161
154
|
|
|
162
|
-
|
|
163
|
-
puts "Demo complete!"
|
|
164
|
-
puts "=" * 60
|
|
155
|
+
demo_footer
|
|
@@ -11,69 +11,56 @@
|
|
|
11
11
|
# - Searching entities
|
|
12
12
|
# - Building entity timelines
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
require "fact_db"
|
|
14
|
+
require_relative "utilities"
|
|
16
15
|
|
|
17
|
-
FactDb
|
|
18
|
-
|
|
19
|
-
config.fuzzy_match_threshold = 0.85
|
|
20
|
-
config.auto_merge_threshold = 0.95
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
# Ensure database tables exist
|
|
24
|
-
FactDb::Database.migrate!
|
|
16
|
+
demo_setup!("FactDb Entity Management Demo")
|
|
17
|
+
demo_configure_logging(__FILE__)
|
|
25
18
|
|
|
26
|
-
|
|
27
|
-
entity_service =
|
|
28
|
-
fact_service =
|
|
19
|
+
facts = FactDb.new
|
|
20
|
+
entity_service = facts.entity_service
|
|
21
|
+
fact_service = facts.fact_service
|
|
29
22
|
|
|
30
|
-
|
|
31
|
-
puts "FactDb Entity Management Demo"
|
|
32
|
-
puts "=" * 60
|
|
33
|
-
|
|
34
|
-
# Section 1: Creating Entities
|
|
35
|
-
puts "\n--- Section 1: Creating Entities ---\n"
|
|
23
|
+
demo_section("Section 1: Creating Entities")
|
|
36
24
|
|
|
37
25
|
# Create a person entity with aliases
|
|
38
26
|
person = entity_service.create(
|
|
39
27
|
"Robert Johnson",
|
|
40
|
-
|
|
28
|
+
kind: :person,
|
|
41
29
|
aliases: ["Bob Johnson", "R. Johnson", "Bobby"],
|
|
42
30
|
attributes: { email: "rjohnson@example.com", department: "Sales" },
|
|
43
31
|
description: "Senior Sales Representative"
|
|
44
32
|
)
|
|
45
|
-
puts "Created person: #{person.
|
|
46
|
-
puts " Aliases: #{person.aliases.map(&:
|
|
47
|
-
puts " Type: #{person.
|
|
33
|
+
puts "Created person: #{person.name}"
|
|
34
|
+
puts " Aliases: #{person.aliases.map(&:name).join(', ')}"
|
|
35
|
+
puts " Type: #{person.kind}"
|
|
48
36
|
|
|
49
37
|
# Create organization entities
|
|
50
38
|
org1 = entity_service.create(
|
|
51
39
|
"Global Industries Inc",
|
|
52
|
-
|
|
40
|
+
kind: :organization,
|
|
53
41
|
aliases: ["Global Industries", "GII"],
|
|
54
42
|
description: "Fortune 500 manufacturing company"
|
|
55
43
|
)
|
|
56
|
-
puts "\nCreated organization: #{org1.
|
|
44
|
+
puts "\nCreated organization: #{org1.name}"
|
|
57
45
|
|
|
58
46
|
# Create a location entity
|
|
59
47
|
location = entity_service.create(
|
|
60
48
|
"San Francisco",
|
|
61
|
-
|
|
49
|
+
kind: :place,
|
|
62
50
|
aliases: ["SF", "San Fran"],
|
|
63
51
|
attributes: { country: "USA", state: "California" }
|
|
64
52
|
)
|
|
65
|
-
puts "Created place: #{location.
|
|
53
|
+
puts "Created place: #{location.name}"
|
|
66
54
|
|
|
67
|
-
|
|
68
|
-
puts "\n--- Section 2: Entity Resolution ---\n"
|
|
55
|
+
demo_section("Section 2: Entity Resolution")
|
|
69
56
|
|
|
70
57
|
# Try to resolve entities using fuzzy matching
|
|
71
58
|
test_names = ["Bob Johnson", "R Johnson", "Robert J", "Global Ind", "GII"]
|
|
72
59
|
|
|
73
60
|
test_names.each do |name|
|
|
74
|
-
resolved = entity_service.resolve(name,
|
|
61
|
+
resolved = entity_service.resolve(name, kind: nil)
|
|
75
62
|
if resolved
|
|
76
|
-
puts "Resolved '#{name}' -> #{resolved.
|
|
63
|
+
puts "Resolved '#{name}' -> #{resolved.name} (#{resolved.kind})"
|
|
77
64
|
else
|
|
78
65
|
puts "Could not resolve '#{name}'"
|
|
79
66
|
end
|
|
@@ -83,81 +70,82 @@ end
|
|
|
83
70
|
puts "\nUsing resolve_or_create:"
|
|
84
71
|
new_person = entity_service.resolve_or_create(
|
|
85
72
|
"Maria Garcia",
|
|
86
|
-
|
|
73
|
+
kind: :person,
|
|
87
74
|
description: "New employee"
|
|
88
75
|
)
|
|
89
|
-
puts "Result: #{new_person.
|
|
76
|
+
puts "Result: #{new_person.name} (new: #{new_person.created_at == new_person.updated_at})"
|
|
90
77
|
|
|
91
|
-
|
|
92
|
-
puts "\n--- Section 3: Managing Aliases ---\n"
|
|
78
|
+
demo_section("Section 3: Managing Aliases")
|
|
93
79
|
|
|
94
80
|
# Add more aliases to an existing entity
|
|
95
|
-
entity_service.add_alias(person.id, "Robert J.",
|
|
96
|
-
entity_service.add_alias(person.id, "rjohnson@example.com",
|
|
81
|
+
entity_service.add_alias(person.id, "Robert J.", kind: :name, confidence: 0.9)
|
|
82
|
+
entity_service.add_alias(person.id, "rjohnson@example.com", kind: :email, confidence: 1.0)
|
|
97
83
|
|
|
98
84
|
person.reload
|
|
99
|
-
puts "Updated aliases for #{person.
|
|
85
|
+
puts "Updated aliases for #{person.name}:"
|
|
100
86
|
person.aliases.each do |a|
|
|
101
|
-
puts " - #{a.
|
|
87
|
+
puts " - #{a.name} (#{a.kind}, confidence: #{a.confidence})"
|
|
102
88
|
end
|
|
103
89
|
|
|
104
|
-
|
|
105
|
-
puts "\n--- Section 4: Merging Entities ---\n"
|
|
90
|
+
demo_section("Section 4: Merging Entities")
|
|
106
91
|
|
|
107
92
|
# Create a duplicate entity (simulating data entry error)
|
|
93
|
+
# Using "Robert Johnsen" (misspelling) to demonstrate fuzzy matching
|
|
108
94
|
duplicate = entity_service.create(
|
|
109
|
-
"
|
|
110
|
-
|
|
111
|
-
description: "Possible duplicate of Robert Johnson"
|
|
95
|
+
"Robert Johnsen",
|
|
96
|
+
kind: :person,
|
|
97
|
+
description: "Possible duplicate of Robert Johnson (typo)"
|
|
112
98
|
)
|
|
113
|
-
puts "Created potential duplicate: #{duplicate.
|
|
99
|
+
puts "Created potential duplicate: #{duplicate.name} (ID: #{duplicate.id})"
|
|
114
100
|
|
|
115
101
|
# Find potential duplicates
|
|
116
102
|
puts "\nSearching for duplicates:"
|
|
117
103
|
duplicates = entity_service.find_duplicates(threshold: 0.8)
|
|
118
|
-
duplicates.
|
|
119
|
-
puts "
|
|
120
|
-
|
|
104
|
+
if duplicates.empty?
|
|
105
|
+
puts " No duplicates found above threshold 0.8"
|
|
106
|
+
else
|
|
107
|
+
duplicates.each do |dup_pair|
|
|
108
|
+
puts " Potential duplicate: #{dup_pair[:entity1].name} (ID: #{dup_pair[:entity1].id}) <-> #{dup_pair[:entity2].name} (ID: #{dup_pair[:entity2].id})"
|
|
109
|
+
puts " Similarity: #{dup_pair[:similarity].round(3)}"
|
|
110
|
+
end
|
|
121
111
|
end
|
|
122
112
|
|
|
123
113
|
# Merge the duplicate into the canonical entity
|
|
124
114
|
puts "\nMerging entities..."
|
|
125
115
|
entity_service.merge(person.id, duplicate.id)
|
|
126
|
-
puts "Merged '#{duplicate.
|
|
116
|
+
puts "Merged '#{duplicate.name}' into '#{person.name}'"
|
|
127
117
|
|
|
128
118
|
# Verify the duplicate is marked as merged
|
|
129
119
|
duplicate.reload
|
|
130
120
|
puts "Duplicate status: #{duplicate.resolution_status}"
|
|
131
|
-
puts "
|
|
121
|
+
puts "Canonical ID: #{duplicate.canonical_id}"
|
|
132
122
|
|
|
133
|
-
|
|
134
|
-
puts "\n--- Section 5: Searching Entities ---\n"
|
|
123
|
+
demo_section("Section 5: Searching Entities")
|
|
135
124
|
|
|
136
125
|
# Create more entities for search demo
|
|
137
|
-
entity_service.create("Jennifer Wilson",
|
|
138
|
-
entity_service.create("John Williams",
|
|
139
|
-
entity_service.create("Wilson & Associates",
|
|
126
|
+
entity_service.create("Jennifer Wilson", kind: :person, description: "Marketing Manager")
|
|
127
|
+
entity_service.create("John Williams", kind: :person, description: "Software Engineer")
|
|
128
|
+
entity_service.create("Wilson & Associates", kind: :organization, description: "Law firm")
|
|
140
129
|
|
|
141
130
|
# Text search
|
|
142
131
|
puts "Search results for 'Wilson':"
|
|
143
132
|
results = entity_service.search("Wilson")
|
|
144
133
|
results.each do |entity|
|
|
145
|
-
puts " - #{entity.
|
|
134
|
+
puts " - #{entity.name} (#{entity.kind})"
|
|
146
135
|
end
|
|
147
136
|
|
|
148
137
|
# Filter by type
|
|
149
138
|
puts "\nPeople only:"
|
|
150
|
-
entity_service.
|
|
151
|
-
puts " - #{entity.
|
|
139
|
+
entity_service.by_kind("person").each do |entity|
|
|
140
|
+
puts " - #{entity.name}"
|
|
152
141
|
end
|
|
153
142
|
|
|
154
143
|
puts "\nOrganizations only:"
|
|
155
|
-
entity_service.
|
|
156
|
-
puts " - #{entity.
|
|
144
|
+
entity_service.by_kind("organization").each do |entity|
|
|
145
|
+
puts " - #{entity.name}"
|
|
157
146
|
end
|
|
158
147
|
|
|
159
|
-
|
|
160
|
-
puts "\n--- Section 6: Entity Timeline ---\n"
|
|
148
|
+
demo_section("Section 6: Entity Timeline")
|
|
161
149
|
|
|
162
150
|
# Create some facts about Bob to build a timeline
|
|
163
151
|
fact_service.create(
|
|
@@ -189,28 +177,25 @@ fact_service.create(
|
|
|
189
177
|
)
|
|
190
178
|
|
|
191
179
|
# Build timeline for the person
|
|
192
|
-
puts "Timeline for #{person.
|
|
180
|
+
puts "Timeline for #{person.name}:"
|
|
193
181
|
timeline = entity_service.timeline_for(person.id, from: Date.new(2017, 1, 1), to: Date.today)
|
|
194
182
|
timeline.each do |entry|
|
|
195
183
|
date_range = entry[:invalid_at] ? "#{entry[:valid_at]} - #{entry[:invalid_at]}" : "#{entry[:valid_at]} - present"
|
|
196
184
|
puts " [#{date_range}]"
|
|
197
|
-
puts " #{entry[:
|
|
185
|
+
puts " #{entry[:text]}"
|
|
198
186
|
end
|
|
199
187
|
|
|
200
|
-
|
|
201
|
-
puts "\n--- Section 7: Entity Statistics ---\n"
|
|
188
|
+
demo_section("Section 7: Entity Statistics")
|
|
202
189
|
|
|
203
190
|
stats = entity_service.stats
|
|
204
191
|
puts "Total entities: #{stats[:total]}"
|
|
205
|
-
puts "By
|
|
206
|
-
stats[:
|
|
207
|
-
puts " #{
|
|
192
|
+
puts "By kind:"
|
|
193
|
+
stats[:by_kind].each do |kind, count|
|
|
194
|
+
puts " #{kind}: #{count}"
|
|
208
195
|
end
|
|
209
196
|
puts "By resolution status:"
|
|
210
197
|
stats[:by_status].each do |status, count|
|
|
211
198
|
puts " #{status}: #{count}"
|
|
212
199
|
end
|
|
213
200
|
|
|
214
|
-
|
|
215
|
-
puts "Entity Management Demo Complete!"
|
|
216
|
-
puts "=" * 60
|
|
201
|
+
demo_footer("Entity Management Demo Complete!")
|
|
@@ -10,55 +10,44 @@
|
|
|
10
10
|
# - Detecting fact changes over time
|
|
11
11
|
# - Building temporal diffs
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
require "fact_db"
|
|
13
|
+
require_relative "utilities"
|
|
15
14
|
|
|
16
|
-
FactDb
|
|
17
|
-
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
# Ensure database tables exist
|
|
21
|
-
FactDb::Database.migrate!
|
|
22
|
-
|
|
23
|
-
clock = FactDb.new
|
|
24
|
-
entity_service = clock.entity_service
|
|
25
|
-
fact_service = clock.fact_service
|
|
15
|
+
demo_setup!("FactDb Temporal Queries Demo")
|
|
16
|
+
demo_configure_logging(__FILE__)
|
|
26
17
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
18
|
+
facts = FactDb.new
|
|
19
|
+
entity_service = facts.entity_service
|
|
20
|
+
fact_service = facts.fact_service
|
|
30
21
|
|
|
31
|
-
|
|
32
|
-
puts "\n--- Setup: Creating Entities ---\n"
|
|
22
|
+
demo_section("Setup: Creating Entities")
|
|
33
23
|
|
|
34
24
|
company = entity_service.create(
|
|
35
25
|
"TechCorp Ltd",
|
|
36
|
-
|
|
26
|
+
kind: :organization,
|
|
37
27
|
description: "Technology company"
|
|
38
28
|
)
|
|
39
29
|
|
|
40
30
|
ceo = entity_service.create(
|
|
41
31
|
"Alice Chen",
|
|
42
|
-
|
|
32
|
+
kind: :person,
|
|
43
33
|
description: "Executive"
|
|
44
34
|
)
|
|
45
35
|
|
|
46
36
|
new_ceo = entity_service.create(
|
|
47
37
|
"David Park",
|
|
48
|
-
|
|
38
|
+
kind: :person,
|
|
49
39
|
description: "Executive"
|
|
50
40
|
)
|
|
51
41
|
|
|
52
42
|
cfo = entity_service.create(
|
|
53
43
|
"Sarah Miller",
|
|
54
|
-
|
|
44
|
+
kind: :person,
|
|
55
45
|
description: "Finance executive"
|
|
56
46
|
)
|
|
57
47
|
|
|
58
|
-
puts "Created entities: #{company.
|
|
48
|
+
puts "Created entities: #{company.name}, #{ceo.name}, #{new_ceo.name}, #{cfo.name}"
|
|
59
49
|
|
|
60
|
-
|
|
61
|
-
puts "\n--- Section 1: Creating Temporal Facts ---\n"
|
|
50
|
+
demo_section("Section 1: Creating Temporal Facts")
|
|
62
51
|
|
|
63
52
|
# Fact with open-ended validity (still true)
|
|
64
53
|
fact1 = fact_service.create(
|
|
@@ -66,7 +55,7 @@ fact1 = fact_service.create(
|
|
|
66
55
|
valid_at: Date.new(2015, 1, 1),
|
|
67
56
|
mentions: [{ entity_id: company.id, role: :subject, text: "TechCorp Ltd" }]
|
|
68
57
|
)
|
|
69
|
-
puts "Created: #{fact1.
|
|
58
|
+
puts "Created: #{fact1.text}"
|
|
70
59
|
puts " Valid: #{fact1.valid_at} - present"
|
|
71
60
|
|
|
72
61
|
# Fact with closed validity (historical)
|
|
@@ -79,7 +68,7 @@ fact2 = fact_service.create(
|
|
|
79
68
|
{ entity_id: company.id, role: :object, text: "TechCorp Ltd" }
|
|
80
69
|
]
|
|
81
70
|
)
|
|
82
|
-
puts "\nCreated: #{fact2.
|
|
71
|
+
puts "\nCreated: #{fact2.text}"
|
|
83
72
|
puts " Valid: #{fact2.valid_at} - #{fact2.invalid_at}"
|
|
84
73
|
|
|
85
74
|
# Current CEO
|
|
@@ -91,7 +80,7 @@ fact3 = fact_service.create(
|
|
|
91
80
|
{ entity_id: company.id, role: :object, text: "TechCorp Ltd" }
|
|
92
81
|
]
|
|
93
82
|
)
|
|
94
|
-
puts "\nCreated: #{fact3.
|
|
83
|
+
puts "\nCreated: #{fact3.text}"
|
|
95
84
|
puts " Valid: #{fact3.valid_at} - present"
|
|
96
85
|
|
|
97
86
|
# Another current fact
|
|
@@ -103,11 +92,10 @@ fact4 = fact_service.create(
|
|
|
103
92
|
{ entity_id: company.id, role: :object, text: "TechCorp Ltd" }
|
|
104
93
|
]
|
|
105
94
|
)
|
|
106
|
-
puts "\nCreated: #{fact4.
|
|
95
|
+
puts "\nCreated: #{fact4.text}"
|
|
107
96
|
puts " Valid: #{fact4.valid_at} - present"
|
|
108
97
|
|
|
109
|
-
|
|
110
|
-
puts "\n--- Section 2: Point-in-Time Queries ---\n"
|
|
98
|
+
demo_section("Section 2: Point-in-Time Queries")
|
|
111
99
|
|
|
112
100
|
# Query facts valid at different dates
|
|
113
101
|
dates_to_query = [
|
|
@@ -121,24 +109,22 @@ dates_to_query.each do |date|
|
|
|
121
109
|
puts "\nFacts about TechCorp on #{date}:"
|
|
122
110
|
facts = fact_service.facts_at(date, entity: company.id)
|
|
123
111
|
facts.each do |fact|
|
|
124
|
-
puts " - #{fact.
|
|
112
|
+
puts " - #{fact.text}"
|
|
125
113
|
end
|
|
126
114
|
end
|
|
127
115
|
|
|
128
|
-
|
|
129
|
-
puts "\n--- Section 3: Current vs Historical Facts ---\n"
|
|
116
|
+
demo_section("Section 3: Current vs Historical Facts")
|
|
130
117
|
|
|
131
118
|
puts "Currently valid facts about TechCorp:"
|
|
132
119
|
current = fact_service.current_facts(entity: company.id)
|
|
133
|
-
current.each { |f| puts " - #{f.
|
|
120
|
+
current.each { |f| puts " - #{f.text}" }
|
|
134
121
|
|
|
135
122
|
puts "\nAll historical facts:"
|
|
136
123
|
FactDb::Models::Fact.historical.each do |fact|
|
|
137
|
-
puts " - #{fact.
|
|
124
|
+
puts " - #{fact.text} (ended: #{fact.invalid_at})"
|
|
138
125
|
end
|
|
139
126
|
|
|
140
|
-
|
|
141
|
-
puts "\n--- Section 4: Superseding Facts ---\n"
|
|
127
|
+
demo_section("Section 4: Superseding Facts")
|
|
142
128
|
|
|
143
129
|
# Company valuation that changes over time
|
|
144
130
|
valuation_2020 = fact_service.create(
|
|
@@ -146,7 +132,7 @@ valuation_2020 = fact_service.create(
|
|
|
146
132
|
valid_at: Date.new(2020, 1, 1),
|
|
147
133
|
mentions: [{ entity_id: company.id, role: :subject, text: "TechCorp Ltd" }]
|
|
148
134
|
)
|
|
149
|
-
puts "Created valuation fact: #{valuation_2020.
|
|
135
|
+
puts "Created valuation fact: #{valuation_2020.text}"
|
|
150
136
|
|
|
151
137
|
# Supersede with new valuation
|
|
152
138
|
valuation_2023 = fact_service.supersede(
|
|
@@ -155,15 +141,14 @@ valuation_2023 = fact_service.supersede(
|
|
|
155
141
|
valid_at: Date.new(2023, 1, 1),
|
|
156
142
|
mentions: [{ entity_id: company.id, role: :subject, text: "TechCorp Ltd" }]
|
|
157
143
|
)
|
|
158
|
-
puts "\nSuperseded with: #{valuation_2023.
|
|
144
|
+
puts "\nSuperseded with: #{valuation_2023.text}"
|
|
159
145
|
|
|
160
146
|
# Check the old fact status
|
|
161
147
|
valuation_2020.reload
|
|
162
148
|
puts "\nOriginal fact status: #{valuation_2020.status}"
|
|
163
149
|
puts "Original fact now invalid at: #{valuation_2020.invalid_at}"
|
|
164
150
|
|
|
165
|
-
|
|
166
|
-
puts "\n--- Section 5: Temporal Timeline for Company ---\n"
|
|
151
|
+
demo_section("Section 5: Temporal Timeline")
|
|
167
152
|
|
|
168
153
|
timeline = fact_service.timeline(
|
|
169
154
|
entity_id: company.id,
|
|
@@ -171,15 +156,14 @@ timeline = fact_service.timeline(
|
|
|
171
156
|
to: Date.today
|
|
172
157
|
)
|
|
173
158
|
|
|
174
|
-
puts "Complete timeline for #{company.
|
|
159
|
+
puts "Complete timeline for #{company.name}:"
|
|
175
160
|
timeline.each do |entry|
|
|
176
161
|
end_date = entry[:invalid_at] || "present"
|
|
177
162
|
status_indicator = entry[:status] == "canonical" ? "" : " [#{entry[:status]}]"
|
|
178
|
-
puts " #{entry[:valid_at]} - #{end_date}: #{entry[:
|
|
163
|
+
puts " #{entry[:valid_at]} - #{end_date}: #{entry[:text]}#{status_indicator}"
|
|
179
164
|
end
|
|
180
165
|
|
|
181
|
-
|
|
182
|
-
puts "\n--- Section 6: Temporal Diff ---\n"
|
|
166
|
+
demo_section("Section 6: Temporal Diff")
|
|
183
167
|
|
|
184
168
|
temporal_query = FactDb::Temporal::Query.new
|
|
185
169
|
|
|
@@ -193,53 +177,49 @@ diff = temporal_query.diff(
|
|
|
193
177
|
|
|
194
178
|
if diff[:added].any?
|
|
195
179
|
puts "\n Added:"
|
|
196
|
-
diff[:added].each { |f| puts " + #{f.
|
|
180
|
+
diff[:added].each { |f| puts " + #{f.text}" }
|
|
197
181
|
end
|
|
198
182
|
|
|
199
183
|
if diff[:removed].any?
|
|
200
184
|
puts "\n Removed:"
|
|
201
|
-
diff[:removed].each { |f| puts " - #{f.
|
|
185
|
+
diff[:removed].each { |f| puts " - #{f.text}" }
|
|
202
186
|
end
|
|
203
187
|
|
|
204
188
|
if diff[:unchanged].any?
|
|
205
189
|
puts "\n Unchanged:"
|
|
206
|
-
diff[:unchanged].each { |f| puts " = #{f.
|
|
190
|
+
diff[:unchanged].each { |f| puts " = #{f.text}" }
|
|
207
191
|
end
|
|
208
192
|
|
|
209
|
-
|
|
210
|
-
puts "\n--- Section 7: Facts by Creation/Invalidation Period ---\n"
|
|
193
|
+
demo_section("Section 7: Facts Created/Invalidated in Date Range")
|
|
211
194
|
|
|
212
195
|
puts "Facts that became valid in 2025:"
|
|
213
196
|
new_facts = temporal_query.facts_created_between(
|
|
214
197
|
from: Date.new(2025, 1, 1),
|
|
215
198
|
to: Date.new(2025, 12, 31)
|
|
216
199
|
)
|
|
217
|
-
new_facts.each { |f| puts " - #{f.
|
|
200
|
+
new_facts.each { |f| puts " - #{f.text} (valid from #{f.valid_at})" }
|
|
218
201
|
|
|
219
202
|
puts "\nFacts that ended in 2024:"
|
|
220
203
|
ended_facts = temporal_query.facts_invalidated_between(
|
|
221
204
|
from: Date.new(2024, 1, 1),
|
|
222
205
|
to: Date.new(2024, 12, 31)
|
|
223
206
|
)
|
|
224
|
-
ended_facts.each { |f| puts " - #{f.
|
|
207
|
+
ended_facts.each { |f| puts " - #{f.text} (ended #{f.invalid_at})" }
|
|
225
208
|
|
|
226
|
-
|
|
227
|
-
puts "\n--- Section 8: Query by Entity Role ---\n"
|
|
209
|
+
demo_section("Section 8: Entity Role Queries")
|
|
228
210
|
|
|
229
211
|
puts "Facts where TechCorp is the subject:"
|
|
230
212
|
subject_facts = temporal_query.facts_with_entity_role(
|
|
231
213
|
entity_id: company.id,
|
|
232
214
|
role: :subject
|
|
233
215
|
)
|
|
234
|
-
subject_facts.each { |f| puts " - #{f.
|
|
216
|
+
subject_facts.each { |f| puts " - #{f.text}" }
|
|
235
217
|
|
|
236
218
|
puts "\nFacts where TechCorp is the object:"
|
|
237
219
|
object_facts = temporal_query.facts_with_entity_role(
|
|
238
220
|
entity_id: company.id,
|
|
239
221
|
role: :object
|
|
240
222
|
)
|
|
241
|
-
object_facts.each { |f| puts " - #{f.
|
|
223
|
+
object_facts.each { |f| puts " - #{f.text}" }
|
|
242
224
|
|
|
243
|
-
|
|
244
|
-
puts "Temporal Queries Demo Complete!"
|
|
245
|
-
puts "=" * 60
|
|
225
|
+
demo_footer
|