htm 0.0.1 → 0.0.10
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/.aigcm_msg +1 -0
- data/.architecture/reviews/comprehensive-codebase-review.md +577 -0
- data/.claude/settings.local.json +92 -0
- data/.envrc +1 -0
- data/.irbrc +283 -80
- data/.tbls.yml +31 -0
- data/CHANGELOG.md +314 -16
- data/CLAUDE.md +603 -0
- data/README.md +76 -5
- data/Rakefile +5 -0
- data/SETUP.md +132 -101
- data/db/migrate/{20250101000001_enable_extensions.rb → 00001_enable_extensions.rb} +0 -1
- data/db/migrate/00002_create_robots.rb +11 -0
- data/db/migrate/00003_create_file_sources.rb +20 -0
- data/db/migrate/00004_create_nodes.rb +65 -0
- data/db/migrate/00005_create_tags.rb +13 -0
- data/db/migrate/00006_create_node_tags.rb +18 -0
- data/db/migrate/00007_create_robot_nodes.rb +26 -0
- data/db/migrate/00009_add_working_memory_to_robot_nodes.rb +12 -0
- data/db/schema.sql +390 -36
- data/docs/api/database.md +19 -232
- data/docs/api/embedding-service.md +1 -7
- data/docs/api/htm.md +305 -364
- data/docs/api/index.md +1 -7
- data/docs/api/long-term-memory.md +342 -590
- data/docs/api/yard/HTM/ActiveRecordConfig.md +23 -0
- data/docs/api/yard/HTM/AuthorizationError.md +11 -0
- data/docs/api/yard/HTM/CircuitBreaker.md +92 -0
- data/docs/api/yard/HTM/CircuitBreakerOpenError.md +34 -0
- data/docs/api/yard/HTM/Configuration.md +175 -0
- data/docs/api/yard/HTM/Database.md +99 -0
- data/docs/api/yard/HTM/DatabaseError.md +14 -0
- data/docs/api/yard/HTM/EmbeddingError.md +18 -0
- data/docs/api/yard/HTM/EmbeddingService.md +58 -0
- data/docs/api/yard/HTM/Error.md +11 -0
- data/docs/api/yard/HTM/JobAdapter.md +39 -0
- data/docs/api/yard/HTM/LongTermMemory.md +342 -0
- data/docs/api/yard/HTM/NotFoundError.md +17 -0
- data/docs/api/yard/HTM/Observability.md +107 -0
- data/docs/api/yard/HTM/QueryTimeoutError.md +19 -0
- data/docs/api/yard/HTM/Railtie.md +27 -0
- data/docs/api/yard/HTM/ResourceExhaustedError.md +13 -0
- data/docs/api/yard/HTM/TagError.md +18 -0
- data/docs/api/yard/HTM/TagService.md +67 -0
- data/docs/api/yard/HTM/Timeframe/Result.md +24 -0
- data/docs/api/yard/HTM/Timeframe.md +40 -0
- data/docs/api/yard/HTM/TimeframeExtractor/Result.md +24 -0
- data/docs/api/yard/HTM/TimeframeExtractor.md +45 -0
- data/docs/api/yard/HTM/ValidationError.md +20 -0
- data/docs/api/yard/HTM/WorkingMemory.md +131 -0
- data/docs/api/yard/HTM.md +80 -0
- data/docs/api/yard/index.csv +179 -0
- data/docs/api/yard-reference.md +51 -0
- data/docs/architecture/adrs/001-postgresql-timescaledb.md +1 -1
- data/docs/architecture/adrs/003-ollama-embeddings.md +1 -1
- data/docs/architecture/adrs/010-redis-working-memory-rejected.md +2 -27
- data/docs/architecture/adrs/index.md +2 -13
- data/docs/architecture/hive-mind.md +165 -166
- data/docs/architecture/index.md +2 -2
- data/docs/architecture/overview.md +5 -171
- data/docs/architecture/two-tier-memory.md +1 -35
- data/docs/assets/images/adr-010-current-architecture.svg +37 -0
- data/docs/assets/images/adr-010-proposed-architecture.svg +48 -0
- data/docs/assets/images/adr-dependency-tree.svg +93 -0
- data/docs/assets/images/class-hierarchy.svg +55 -0
- data/docs/assets/images/exception-hierarchy.svg +45 -0
- data/docs/assets/images/htm-architecture-overview.svg +83 -0
- data/docs/assets/images/htm-complete-memory-flow.svg +160 -0
- data/docs/assets/images/htm-context-assembly-flow.svg +148 -0
- data/docs/assets/images/htm-eviction-process.svg +141 -0
- data/docs/assets/images/htm-memory-addition-flow.svg +138 -0
- data/docs/assets/images/htm-memory-recall-flow.svg +152 -0
- data/docs/assets/images/htm-node-states.svg +123 -0
- data/docs/assets/images/project-structure.svg +78 -0
- data/docs/assets/images/test-directory-structure.svg +38 -0
- data/{dbdoc → docs/database}/README.md +127 -125
- data/docs/database/public.file_sources.md +42 -0
- data/docs/database/public.file_sources.svg +211 -0
- data/{dbdoc → docs/database}/public.node_tags.md +7 -8
- data/docs/database/public.node_tags.svg +239 -0
- data/{dbdoc → docs/database}/public.nodes.md +22 -17
- data/docs/database/public.nodes.svg +271 -0
- data/docs/database/public.robot_nodes.md +46 -0
- data/docs/database/public.robot_nodes.svg +243 -0
- data/{dbdoc → docs/database}/public.robots.md +2 -3
- data/docs/database/public.robots.svg +161 -0
- data/docs/database/public.tags.svg +139 -0
- data/{dbdoc → docs/database}/schema.json +941 -630
- data/docs/database/schema.svg +282 -0
- data/docs/development/index.md +1 -29
- data/docs/development/schema.md +134 -309
- data/docs/development/testing.md +1 -9
- data/docs/getting-started/index.md +47 -0
- data/docs/{installation.md → getting-started/installation.md} +2 -2
- data/docs/{quick-start.md → getting-started/quick-start.md} +5 -5
- data/docs/guides/adding-memories.md +295 -643
- data/docs/guides/recalling-memories.md +36 -1
- data/docs/guides/search-strategies.md +85 -51
- data/docs/images/htm-er-diagram.svg +156 -0
- data/docs/index.md +16 -31
- data/docs/multi_framework_support.md +4 -4
- data/examples/README.md +280 -0
- data/examples/basic_usage.rb +18 -16
- data/examples/cli_app/htm_cli.rb +146 -8
- data/examples/cli_app/temp.log +93 -0
- data/examples/custom_llm_configuration.rb +1 -2
- data/examples/example_app/app.rb +11 -14
- data/examples/file_loader_usage.rb +177 -0
- data/examples/robot_groups/lib/robot_group.rb +419 -0
- data/examples/robot_groups/lib/working_memory_channel.rb +140 -0
- data/examples/robot_groups/multi_process.rb +286 -0
- data/examples/robot_groups/robot_worker.rb +136 -0
- data/examples/robot_groups/same_process.rb +229 -0
- data/examples/sinatra_app/Gemfile +1 -0
- data/examples/sinatra_app/Gemfile.lock +166 -0
- data/examples/sinatra_app/app.rb +219 -24
- data/examples/timeframe_demo.rb +276 -0
- data/lib/htm/active_record_config.rb +10 -3
- data/lib/htm/circuit_breaker.rb +202 -0
- data/lib/htm/configuration.rb +313 -80
- data/lib/htm/database.rb +67 -36
- data/lib/htm/embedding_service.rb +39 -2
- data/lib/htm/errors.rb +131 -11
- data/lib/htm/{sinatra.rb → integrations/sinatra.rb} +87 -12
- data/lib/htm/job_adapter.rb +10 -3
- data/lib/htm/jobs/generate_embedding_job.rb +5 -4
- data/lib/htm/jobs/generate_tags_job.rb +4 -0
- data/lib/htm/loaders/markdown_loader.rb +263 -0
- data/lib/htm/loaders/paragraph_chunker.rb +112 -0
- data/lib/htm/long_term_memory.rb +601 -321
- data/lib/htm/models/file_source.rb +99 -0
- data/lib/htm/models/node.rb +116 -12
- data/lib/htm/models/robot.rb +53 -4
- data/lib/htm/models/robot_node.rb +51 -0
- data/lib/htm/models/tag.rb +302 -0
- data/lib/htm/observability.rb +395 -0
- data/lib/htm/tag_service.rb +60 -3
- data/lib/htm/tasks.rb +29 -0
- data/lib/htm/timeframe.rb +194 -0
- data/lib/htm/timeframe_extractor.rb +307 -0
- data/lib/htm/version.rb +1 -1
- data/lib/htm/working_memory.rb +165 -70
- data/lib/htm.rb +352 -133
- data/lib/tasks/doc.rake +300 -0
- data/lib/tasks/files.rake +299 -0
- data/lib/tasks/htm.rake +188 -2
- data/lib/tasks/jobs.rake +10 -12
- data/lib/tasks/tags.rake +194 -0
- data/mkdocs.yml +91 -9
- data/notes/ARCHITECTURE_REVIEW.md +1167 -0
- data/notes/IMPLEMENTATION_SUMMARY.md +606 -0
- data/notes/MULTI_FRAMEWORK_IMPLEMENTATION.md +451 -0
- data/notes/next_steps.md +100 -0
- data/notes/plan.md +627 -0
- data/notes/tag_ontology_enhancement_ideas.md +222 -0
- data/notes/timescaledb_removal_summary.md +200 -0
- metadata +177 -37
- data/db/migrate/20250101000002_create_robots.rb +0 -14
- data/db/migrate/20250101000003_create_nodes.rb +0 -42
- data/db/migrate/20250101000005_create_tags.rb +0 -38
- data/db/migrate/20250101000007_add_node_vector_indexes.rb +0 -30
- data/dbdoc/public.node_tags.svg +0 -112
- data/dbdoc/public.nodes.svg +0 -118
- data/dbdoc/public.robots.svg +0 -90
- data/dbdoc/public.tags.svg +0 -60
- data/dbdoc/schema.svg +0 -154
- data/{dbdoc → docs/database}/public.node_stats.md +0 -0
- data/{dbdoc → docs/database}/public.node_stats.svg +0 -0
- data/{dbdoc → docs/database}/public.nodes_tags.md +0 -0
- data/{dbdoc → docs/database}/public.nodes_tags.svg +0 -0
- data/{dbdoc → docs/database}/public.ontology_structure.md +0 -0
- data/{dbdoc → docs/database}/public.ontology_structure.svg +0 -0
- data/{dbdoc → docs/database}/public.operations_log.md +0 -0
- data/{dbdoc → docs/database}/public.operations_log.svg +0 -0
- data/{dbdoc → docs/database}/public.relationships.md +0 -0
- data/{dbdoc → docs/database}/public.relationships.svg +0 -0
- data/{dbdoc → docs/database}/public.robot_activity.md +0 -0
- data/{dbdoc → docs/database}/public.robot_activity.svg +0 -0
- data/{dbdoc → docs/database}/public.schema_migrations.md +0 -0
- data/{dbdoc → docs/database}/public.schema_migrations.svg +0 -0
- data/{dbdoc → docs/database}/public.tags.md +3 -3
- /data/{dbdoc → docs/database}/public.topic_relationships.md +0 -0
- /data/{dbdoc → docs/database}/public.topic_relationships.svg +0 -0
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Timeframe Demo - Demonstrates the various ways to use timeframes with recall
|
|
5
|
+
#
|
|
6
|
+
# Run with:
|
|
7
|
+
# HTM_DBURL="postgresql://localhost/htm_development" ruby examples/timeframe_demo.rb
|
|
8
|
+
|
|
9
|
+
require_relative "../lib/htm"
|
|
10
|
+
|
|
11
|
+
puts <<~HEADER
|
|
12
|
+
╔══════════════════════════════════════════════════════════════════╗
|
|
13
|
+
║ HTM Timeframe Demo ║
|
|
14
|
+
║ ║
|
|
15
|
+
║ Demonstrates the flexible timeframe options for recall queries ║
|
|
16
|
+
╚══════════════════════════════════════════════════════════════════╝
|
|
17
|
+
|
|
18
|
+
HEADER
|
|
19
|
+
|
|
20
|
+
# Configure week start (optional - defaults to :sunday)
|
|
21
|
+
HTM.configure do |config|
|
|
22
|
+
config.week_start = :sunday # or :monday
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
puts "Configuration:"
|
|
26
|
+
puts " week_start: #{HTM.configuration.week_start}"
|
|
27
|
+
puts
|
|
28
|
+
|
|
29
|
+
# Initialize HTM
|
|
30
|
+
htm = HTM.new(robot_name: "Timeframe Demo Robot")
|
|
31
|
+
|
|
32
|
+
puts "=" * 70
|
|
33
|
+
puts "TIMEFRAME OPTIONS FOR RECALL"
|
|
34
|
+
puts "=" * 70
|
|
35
|
+
puts
|
|
36
|
+
|
|
37
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
38
|
+
# 1. No timeframe filter (nil)
|
|
39
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
40
|
+
puts "1. NO TIMEFRAME FILTER (nil)"
|
|
41
|
+
puts " When timeframe is nil, no time-based filtering is applied."
|
|
42
|
+
puts
|
|
43
|
+
puts " Code:"
|
|
44
|
+
puts " htm.recall('PostgreSQL', timeframe: nil)"
|
|
45
|
+
puts
|
|
46
|
+
puts " SQL equivalent: No WHERE clause on created_at"
|
|
47
|
+
puts
|
|
48
|
+
|
|
49
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
50
|
+
# 2. Date object - entire day
|
|
51
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
52
|
+
puts "2. DATE OBJECT (entire day)"
|
|
53
|
+
puts " A Date is expanded to cover 00:00:00 to 23:59:59 of that day."
|
|
54
|
+
puts
|
|
55
|
+
puts " Code:"
|
|
56
|
+
puts " htm.recall('meetings', timeframe: Date.today)"
|
|
57
|
+
puts " htm.recall('notes', timeframe: Date.new(2025, 11, 15))"
|
|
58
|
+
puts
|
|
59
|
+
|
|
60
|
+
today = Date.today
|
|
61
|
+
range = HTM::Timeframe.normalize(today)
|
|
62
|
+
puts " Date.today (#{today}) normalizes to:"
|
|
63
|
+
puts " #{range.begin} .. #{range.end}"
|
|
64
|
+
puts
|
|
65
|
+
|
|
66
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
67
|
+
# 3. DateTime object - treated same as Date
|
|
68
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
69
|
+
puts "3. DATETIME OBJECT (entire day)"
|
|
70
|
+
puts " DateTime is treated the same as Date - the entire day is included."
|
|
71
|
+
puts
|
|
72
|
+
puts " Code:"
|
|
73
|
+
puts " htm.recall('events', timeframe: DateTime.now)"
|
|
74
|
+
puts
|
|
75
|
+
|
|
76
|
+
datetime = DateTime.now
|
|
77
|
+
range = HTM::Timeframe.normalize(datetime)
|
|
78
|
+
puts " DateTime.now normalizes to:"
|
|
79
|
+
puts " #{range.begin} .. #{range.end}"
|
|
80
|
+
puts
|
|
81
|
+
|
|
82
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
83
|
+
# 4. Time object - entire day
|
|
84
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
85
|
+
puts "4. TIME OBJECT (entire day)"
|
|
86
|
+
puts " Time is also normalized to cover the entire day."
|
|
87
|
+
puts
|
|
88
|
+
puts " Code:"
|
|
89
|
+
puts " htm.recall('logs', timeframe: Time.now)"
|
|
90
|
+
puts
|
|
91
|
+
|
|
92
|
+
time = Time.now
|
|
93
|
+
range = HTM::Timeframe.normalize(time)
|
|
94
|
+
puts " Time.now normalizes to:"
|
|
95
|
+
puts " #{range.begin} .. #{range.end}"
|
|
96
|
+
puts
|
|
97
|
+
|
|
98
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
99
|
+
# 5. Range - passed through directly
|
|
100
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
101
|
+
puts "5. RANGE (passed through)"
|
|
102
|
+
puts " A Range of Time objects is used directly for precise control."
|
|
103
|
+
puts
|
|
104
|
+
puts " Code:"
|
|
105
|
+
puts " start_time = Time.now - (7 * 24 * 60 * 60) # 7 days ago"
|
|
106
|
+
puts " end_time = Time.now"
|
|
107
|
+
puts " htm.recall('updates', timeframe: start_time..end_time)"
|
|
108
|
+
puts
|
|
109
|
+
|
|
110
|
+
start_time = Time.now - (7 * 24 * 60 * 60)
|
|
111
|
+
end_time = Time.now
|
|
112
|
+
puts " Range example:"
|
|
113
|
+
puts " #{start_time} .. #{end_time}"
|
|
114
|
+
puts
|
|
115
|
+
|
|
116
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
117
|
+
# 6. String - natural language parsing via Chronic
|
|
118
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
119
|
+
puts "6. STRING (natural language)"
|
|
120
|
+
puts " Natural language time expressions are parsed using the Chronic gem."
|
|
121
|
+
puts
|
|
122
|
+
puts " Standard expressions:"
|
|
123
|
+
puts " htm.recall('notes', timeframe: 'yesterday')"
|
|
124
|
+
puts " htm.recall('notes', timeframe: 'last week')"
|
|
125
|
+
puts " htm.recall('notes', timeframe: 'last month')"
|
|
126
|
+
puts " htm.recall('notes', timeframe: 'this morning')"
|
|
127
|
+
puts
|
|
128
|
+
|
|
129
|
+
expressions = ["yesterday", "last week", "last month", "today"]
|
|
130
|
+
expressions.each do |expr|
|
|
131
|
+
result = HTM::Timeframe.normalize(expr)
|
|
132
|
+
if result
|
|
133
|
+
puts " '#{expr}' => #{result.begin.strftime('%Y-%m-%d %H:%M')} .. #{result.end.strftime('%Y-%m-%d %H:%M')}"
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
puts
|
|
137
|
+
|
|
138
|
+
puts " 'Few' keyword (maps to 3):"
|
|
139
|
+
puts " htm.recall('notes', timeframe: 'few days ago')"
|
|
140
|
+
puts " htm.recall('notes', timeframe: 'a few hours ago')"
|
|
141
|
+
puts " htm.recall('notes', timeframe: 'few weeks ago')"
|
|
142
|
+
puts
|
|
143
|
+
|
|
144
|
+
few_expressions = ["few days ago", "a few hours ago", "few weeks ago"]
|
|
145
|
+
few_expressions.each do |expr|
|
|
146
|
+
result = HTM::Timeframe.normalize(expr)
|
|
147
|
+
if result
|
|
148
|
+
time_point = result.is_a?(Range) ? result.begin : result
|
|
149
|
+
puts " '#{expr}' => #{time_point.strftime('%Y-%m-%d %H:%M')}"
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
puts
|
|
153
|
+
|
|
154
|
+
puts " Weekend expressions:"
|
|
155
|
+
puts " htm.recall('notes', timeframe: 'last weekend')"
|
|
156
|
+
puts " htm.recall('notes', timeframe: 'weekend before last')"
|
|
157
|
+
puts " htm.recall('notes', timeframe: '2 weekends ago')"
|
|
158
|
+
puts " htm.recall('notes', timeframe: 'three weekends ago')"
|
|
159
|
+
puts
|
|
160
|
+
|
|
161
|
+
weekend_expressions = ["last weekend", "weekend before last", "2 weekends ago"]
|
|
162
|
+
weekend_expressions.each do |expr|
|
|
163
|
+
result = HTM::Timeframe.normalize(expr)
|
|
164
|
+
if result && result.is_a?(Range)
|
|
165
|
+
puts " '#{expr}' =>"
|
|
166
|
+
puts " #{result.begin.strftime('%A %Y-%m-%d')} .. #{result.end.strftime('%A %Y-%m-%d')}"
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
puts
|
|
170
|
+
|
|
171
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
172
|
+
# 7. :auto - extract timeframe from query text
|
|
173
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
174
|
+
puts "7. :auto (EXTRACT FROM QUERY)"
|
|
175
|
+
puts " The timeframe is extracted from the query text automatically."
|
|
176
|
+
puts " The temporal expression is removed from the search query."
|
|
177
|
+
puts
|
|
178
|
+
puts " Code:"
|
|
179
|
+
puts " htm.recall('what did we discuss last week about databases', timeframe: :auto)"
|
|
180
|
+
puts
|
|
181
|
+
|
|
182
|
+
queries = [
|
|
183
|
+
"what did we discuss last week about databases",
|
|
184
|
+
"show me notes from yesterday about PostgreSQL",
|
|
185
|
+
"what happened few days ago with the API",
|
|
186
|
+
"recent discussions about embeddings",
|
|
187
|
+
"show me weekend before last notes about Ruby"
|
|
188
|
+
]
|
|
189
|
+
|
|
190
|
+
puts " Examples:"
|
|
191
|
+
queries.each do |query|
|
|
192
|
+
result = HTM::Timeframe.normalize(:auto, query: query)
|
|
193
|
+
puts
|
|
194
|
+
puts " Original: '#{query}'"
|
|
195
|
+
puts " Cleaned: '#{result.query}'"
|
|
196
|
+
puts " Extracted: '#{result.extracted}'"
|
|
197
|
+
if result.timeframe
|
|
198
|
+
if result.timeframe.is_a?(Range)
|
|
199
|
+
puts " Timeframe: #{result.timeframe.begin.strftime('%Y-%m-%d %H:%M')} .. #{result.timeframe.end.strftime('%Y-%m-%d %H:%M')}"
|
|
200
|
+
else
|
|
201
|
+
puts " Timeframe: #{result.timeframe.strftime('%Y-%m-%d %H:%M')}"
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
puts
|
|
206
|
+
|
|
207
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
208
|
+
# 8. Array of Ranges - multiple time windows (OR'd together)
|
|
209
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
210
|
+
puts "8. ARRAY OF RANGES (multiple time windows)"
|
|
211
|
+
puts " Multiple time windows are OR'd together in the query."
|
|
212
|
+
puts
|
|
213
|
+
puts " Code:"
|
|
214
|
+
puts " today = Date.today"
|
|
215
|
+
puts " last_friday = today - ((today.wday + 2) % 7)"
|
|
216
|
+
puts " two_fridays_ago = last_friday - 7"
|
|
217
|
+
puts " "
|
|
218
|
+
puts " htm.recall('standup notes', timeframe: [last_friday, two_fridays_ago])"
|
|
219
|
+
puts
|
|
220
|
+
|
|
221
|
+
today = Date.today
|
|
222
|
+
# Calculate last Friday
|
|
223
|
+
days_since_friday = (today.wday + 2) % 7
|
|
224
|
+
days_since_friday = 7 if days_since_friday == 0
|
|
225
|
+
last_friday = today - days_since_friday
|
|
226
|
+
two_fridays_ago = last_friday - 7
|
|
227
|
+
|
|
228
|
+
ranges = HTM::Timeframe.normalize([last_friday, two_fridays_ago])
|
|
229
|
+
puts " Dates: #{last_friday} and #{two_fridays_ago}"
|
|
230
|
+
puts " Normalized to #{ranges.length} ranges:"
|
|
231
|
+
ranges.each_with_index do |range, i|
|
|
232
|
+
puts " [#{i + 1}] #{range.begin} .. #{range.end}"
|
|
233
|
+
end
|
|
234
|
+
puts
|
|
235
|
+
puts " SQL equivalent:"
|
|
236
|
+
puts " WHERE (created_at BETWEEN '...' AND '...')"
|
|
237
|
+
puts " OR (created_at BETWEEN '...' AND '...')"
|
|
238
|
+
puts
|
|
239
|
+
|
|
240
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
241
|
+
# Summary
|
|
242
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
243
|
+
puts "=" * 70
|
|
244
|
+
puts "SUMMARY OF TIMEFRAME OPTIONS"
|
|
245
|
+
puts "=" * 70
|
|
246
|
+
puts
|
|
247
|
+
puts " | Input Type | Behavior |"
|
|
248
|
+
puts " |-----------------|---------------------------------------------|"
|
|
249
|
+
puts " | nil | No time filter |"
|
|
250
|
+
puts " | Date | Entire day (00:00:00 to 23:59:59) |"
|
|
251
|
+
puts " | DateTime | Entire day (same as Date) |"
|
|
252
|
+
puts " | Time | Entire day (same as Date) |"
|
|
253
|
+
puts " | Range | Exact time window |"
|
|
254
|
+
puts " | String | Natural language parsing via Chronic |"
|
|
255
|
+
puts " | :auto | Extract from query, return cleaned query |"
|
|
256
|
+
puts " | Array<Range> | Multiple time windows OR'd together |"
|
|
257
|
+
puts
|
|
258
|
+
|
|
259
|
+
puts "=" * 70
|
|
260
|
+
puts "SPECIAL KEYWORDS"
|
|
261
|
+
puts "=" * 70
|
|
262
|
+
puts
|
|
263
|
+
puts " | Keyword | Meaning |"
|
|
264
|
+
puts " |---------------------------|----------------------------------|"
|
|
265
|
+
puts " | few, a few, several | Maps to #{HTM::TimeframeExtractor::FEW} (configurable via FEW constant) |"
|
|
266
|
+
puts " | recently, recent | Last #{HTM::TimeframeExtractor::FEW} days |"
|
|
267
|
+
puts " | weekend before last | 2 weekends ago (Sat-Mon) |"
|
|
268
|
+
puts " | N weekends ago | N weekends back (Sat-Mon range) |"
|
|
269
|
+
puts
|
|
270
|
+
|
|
271
|
+
puts <<~FOOTER
|
|
272
|
+
|
|
273
|
+
╔══════════════════════════════════════════════════════════════════╗
|
|
274
|
+
║ Demo Complete ║
|
|
275
|
+
╚══════════════════════════════════════════════════════════════════╝
|
|
276
|
+
FOOTER
|
|
@@ -53,9 +53,14 @@ class HTM
|
|
|
53
53
|
|
|
54
54
|
# Check if connection is established and active
|
|
55
55
|
def connected?
|
|
56
|
-
ActiveRecord::Base
|
|
57
|
-
|
|
58
|
-
|
|
56
|
+
return false unless defined?(ActiveRecord::Base)
|
|
57
|
+
return false unless ActiveRecord::Base.connection_handler.connection_pool_list.any?
|
|
58
|
+
|
|
59
|
+
ActiveRecord::Base.connected? && ActiveRecord::Base.connection.active?
|
|
60
|
+
rescue ActiveRecord::ConnectionNotDefined, ActiveRecord::ConnectionNotEstablished
|
|
61
|
+
false
|
|
62
|
+
rescue StandardError => e
|
|
63
|
+
HTM.logger.debug "Connection check failed: #{e.class} - #{e.message}"
|
|
59
64
|
false
|
|
60
65
|
end
|
|
61
66
|
|
|
@@ -105,8 +110,10 @@ class HTM
|
|
|
105
110
|
def require_models
|
|
106
111
|
require_relative 'models/robot'
|
|
107
112
|
require_relative 'models/node'
|
|
113
|
+
require_relative 'models/robot_node'
|
|
108
114
|
require_relative 'models/tag'
|
|
109
115
|
require_relative 'models/node_tag'
|
|
116
|
+
require_relative 'models/file_source'
|
|
110
117
|
end
|
|
111
118
|
end
|
|
112
119
|
end
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'errors'
|
|
4
|
+
|
|
5
|
+
class HTM
|
|
6
|
+
# Circuit Breaker - Prevents cascading failures from external LLM services
|
|
7
|
+
#
|
|
8
|
+
# Implements the circuit breaker pattern to protect against repeated failures
|
|
9
|
+
# when calling external LLM APIs for embeddings or tag extraction.
|
|
10
|
+
#
|
|
11
|
+
# States:
|
|
12
|
+
# - :closed - Normal operation, requests flow through
|
|
13
|
+
# - :open - Circuit tripped, requests fail fast with CircuitBreakerOpenError
|
|
14
|
+
# - :half_open - Testing if service recovered, allows limited requests
|
|
15
|
+
#
|
|
16
|
+
# @example Basic usage
|
|
17
|
+
# breaker = HTM::CircuitBreaker.new(name: 'embedding')
|
|
18
|
+
# result = breaker.call { external_api_call }
|
|
19
|
+
#
|
|
20
|
+
# @example With custom thresholds
|
|
21
|
+
# breaker = HTM::CircuitBreaker.new(
|
|
22
|
+
# name: 'tag_extraction',
|
|
23
|
+
# failure_threshold: 3,
|
|
24
|
+
# reset_timeout: 30
|
|
25
|
+
# )
|
|
26
|
+
#
|
|
27
|
+
class CircuitBreaker
|
|
28
|
+
attr_reader :name, :state, :failure_count, :last_failure_time
|
|
29
|
+
|
|
30
|
+
# Default configuration
|
|
31
|
+
DEFAULT_FAILURE_THRESHOLD = 5 # Failures before opening circuit
|
|
32
|
+
DEFAULT_RESET_TIMEOUT = 60 # Seconds before trying half-open
|
|
33
|
+
DEFAULT_HALF_OPEN_MAX_CALLS = 3 # Successful calls to close circuit
|
|
34
|
+
|
|
35
|
+
# Initialize a new circuit breaker
|
|
36
|
+
#
|
|
37
|
+
# @param name [String] Identifier for this circuit breaker (for logging)
|
|
38
|
+
# @param failure_threshold [Integer] Number of failures before opening circuit
|
|
39
|
+
# @param reset_timeout [Integer] Seconds to wait before attempting recovery
|
|
40
|
+
# @param half_open_max_calls [Integer] Successful calls needed to close circuit
|
|
41
|
+
#
|
|
42
|
+
def initialize(
|
|
43
|
+
name:,
|
|
44
|
+
failure_threshold: DEFAULT_FAILURE_THRESHOLD,
|
|
45
|
+
reset_timeout: DEFAULT_RESET_TIMEOUT,
|
|
46
|
+
half_open_max_calls: DEFAULT_HALF_OPEN_MAX_CALLS
|
|
47
|
+
)
|
|
48
|
+
@name = name
|
|
49
|
+
@failure_threshold = failure_threshold
|
|
50
|
+
@reset_timeout = reset_timeout
|
|
51
|
+
@half_open_max_calls = half_open_max_calls
|
|
52
|
+
|
|
53
|
+
@state = :closed
|
|
54
|
+
@failure_count = 0
|
|
55
|
+
@success_count = 0
|
|
56
|
+
@last_failure_time = nil
|
|
57
|
+
@mutex = Mutex.new
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Execute a block with circuit breaker protection
|
|
61
|
+
#
|
|
62
|
+
# @yield Block containing the protected operation
|
|
63
|
+
# @return [Object] Result of the block if successful
|
|
64
|
+
# @raise [CircuitBreakerOpenError] If circuit is open
|
|
65
|
+
# @raise [StandardError] If the block raises an error (after recording failure)
|
|
66
|
+
#
|
|
67
|
+
def call
|
|
68
|
+
@mutex.synchronize do
|
|
69
|
+
case @state
|
|
70
|
+
when :open
|
|
71
|
+
check_reset_timeout
|
|
72
|
+
if @state == :open
|
|
73
|
+
HTM.logger.warn "CircuitBreaker[#{@name}]: Circuit is OPEN, failing fast"
|
|
74
|
+
raise CircuitBreakerOpenError, "Circuit breaker '#{@name}' is open. Service unavailable."
|
|
75
|
+
end
|
|
76
|
+
when :half_open
|
|
77
|
+
HTM.logger.debug "CircuitBreaker[#{@name}]: Circuit is HALF-OPEN, testing service"
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
begin
|
|
82
|
+
result = yield
|
|
83
|
+
record_success
|
|
84
|
+
result
|
|
85
|
+
rescue StandardError => e
|
|
86
|
+
record_failure(e)
|
|
87
|
+
raise
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Check if circuit is currently open
|
|
92
|
+
#
|
|
93
|
+
# @return [Boolean] true if circuit is open
|
|
94
|
+
#
|
|
95
|
+
def open?
|
|
96
|
+
@mutex.synchronize { @state == :open }
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Check if circuit is currently closed (normal operation)
|
|
100
|
+
#
|
|
101
|
+
# @return [Boolean] true if circuit is closed
|
|
102
|
+
#
|
|
103
|
+
def closed?
|
|
104
|
+
@mutex.synchronize { @state == :closed }
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Check if circuit is in half-open state (testing recovery)
|
|
108
|
+
#
|
|
109
|
+
# @return [Boolean] true if circuit is half-open
|
|
110
|
+
#
|
|
111
|
+
def half_open?
|
|
112
|
+
@mutex.synchronize { @state == :half_open }
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Manually reset the circuit breaker to closed state
|
|
116
|
+
#
|
|
117
|
+
# @return [void]
|
|
118
|
+
#
|
|
119
|
+
def reset!
|
|
120
|
+
@mutex.synchronize do
|
|
121
|
+
@state = :closed
|
|
122
|
+
@failure_count = 0
|
|
123
|
+
@success_count = 0
|
|
124
|
+
@last_failure_time = nil
|
|
125
|
+
HTM.logger.info "CircuitBreaker[#{@name}]: Manually reset to CLOSED"
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Get current circuit breaker statistics
|
|
130
|
+
#
|
|
131
|
+
# @return [Hash] Statistics including state, failure count, etc.
|
|
132
|
+
#
|
|
133
|
+
def stats
|
|
134
|
+
@mutex.synchronize do
|
|
135
|
+
{
|
|
136
|
+
name: @name,
|
|
137
|
+
state: @state,
|
|
138
|
+
failure_count: @failure_count,
|
|
139
|
+
success_count: @success_count,
|
|
140
|
+
last_failure_time: @last_failure_time,
|
|
141
|
+
failure_threshold: @failure_threshold,
|
|
142
|
+
reset_timeout: @reset_timeout
|
|
143
|
+
}
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
private
|
|
148
|
+
|
|
149
|
+
# Record a successful call
|
|
150
|
+
def record_success
|
|
151
|
+
@mutex.synchronize do
|
|
152
|
+
case @state
|
|
153
|
+
when :half_open
|
|
154
|
+
@success_count += 1
|
|
155
|
+
if @success_count >= @half_open_max_calls
|
|
156
|
+
@state = :closed
|
|
157
|
+
@failure_count = 0
|
|
158
|
+
@success_count = 0
|
|
159
|
+
HTM.logger.info "CircuitBreaker[#{@name}]: Service recovered, circuit CLOSED"
|
|
160
|
+
end
|
|
161
|
+
when :closed
|
|
162
|
+
# Reset failure count on success in closed state
|
|
163
|
+
@failure_count = 0 if @failure_count > 0
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Record a failed call
|
|
169
|
+
def record_failure(error)
|
|
170
|
+
@mutex.synchronize do
|
|
171
|
+
@failure_count += 1
|
|
172
|
+
@last_failure_time = Time.now
|
|
173
|
+
@success_count = 0
|
|
174
|
+
|
|
175
|
+
HTM.logger.warn "CircuitBreaker[#{@name}]: Failure ##{@failure_count} - #{error.class}: #{error.message}"
|
|
176
|
+
|
|
177
|
+
case @state
|
|
178
|
+
when :closed
|
|
179
|
+
if @failure_count >= @failure_threshold
|
|
180
|
+
@state = :open
|
|
181
|
+
HTM.logger.error "CircuitBreaker[#{@name}]: Threshold reached (#{@failure_threshold}), circuit OPEN"
|
|
182
|
+
end
|
|
183
|
+
when :half_open
|
|
184
|
+
@state = :open
|
|
185
|
+
HTM.logger.warn "CircuitBreaker[#{@name}]: Failed during recovery test, circuit OPEN"
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Check if reset timeout has elapsed and transition to half-open
|
|
191
|
+
def check_reset_timeout
|
|
192
|
+
return unless @state == :open && @last_failure_time
|
|
193
|
+
|
|
194
|
+
elapsed = Time.now - @last_failure_time
|
|
195
|
+
if elapsed >= @reset_timeout
|
|
196
|
+
@state = :half_open
|
|
197
|
+
@success_count = 0
|
|
198
|
+
HTM.logger.info "CircuitBreaker[#{@name}]: Reset timeout elapsed (#{@reset_timeout}s), circuit HALF-OPEN"
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|