smart_rag 0.1.0 → 0.2.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 (90) hide show
  1. checksums.yaml +4 -4
  2. data/.env.example +252 -0
  3. data/.rspec +2 -0
  4. data/AGENTS.md +33 -0
  5. data/API_DOCUMENTATION.md +828 -0
  6. data/CHANGELOG.md +16 -1
  7. data/ER-diagram.mmd +144 -0
  8. data/Gemfile +50 -0
  9. data/Gemfile.lock +398 -0
  10. data/Hybrid_Reranking.md +171 -0
  11. data/README.en.md +420 -28
  12. data/README.md +534 -63
  13. data/Rakefile +268 -0
  14. data/SETUP_GUIDE.md +650 -0
  15. data/SmartChunking.md +180 -0
  16. data/USAGE_EXAMPLES.md +1002 -0
  17. data/config/llm_config.yml +4 -2
  18. data/config/smart_rag.yml +45 -1
  19. data/config.ru +15 -0
  20. data/db/migrations/006_create_text_search_configs.rb +3 -2
  21. data/db/migrations/008_create_embeddings.rb +5 -4
  22. data/db/migrations/012_add_metadata_to_source_sections.rb +11 -0
  23. data/db/migrations/013_create_media_jobs.rb +25 -0
  24. data/db/migrations/014_add_media_job_operations_indexes.rb +11 -0
  25. data/db/migrations/015_add_media_leases_and_objects.rb +80 -0
  26. data/db/migrations/016_add_document_principals_and_staging_references.rb +38 -0
  27. data/db/migrations/017_add_media_job_request_fingerprint.rb +48 -0
  28. data/db/seeds/text_search_configs.sql +3 -3
  29. data/design.md +1057 -0
  30. data/docs/API_DOCUMENTATION.md +838 -0
  31. data/docs/DOCUMENTATION_INDEX.en.md +60 -0
  32. data/docs/DOCUMENTATION_INDEX.md +65 -0
  33. data/docs/FIX_SUMMARY.md +256 -0
  34. data/docs/FIX_SUMMARY_COMPLETE.md +273 -0
  35. data/docs/Hybrid_Reranking.md +171 -0
  36. data/docs/MIGRATION_GUIDE.md +151 -0
  37. data/docs/PERFORMANCE_GUIDE.md +58 -0
  38. data/docs/SETUP_GUIDE.md +659 -0
  39. data/docs/SmartChunking.md +180 -0
  40. data/docs/USAGE_EXAMPLES.md +1008 -0
  41. data/docs/design.md +1057 -0
  42. data/docs/evidence_pack.md +211 -0
  43. data/docs/requirements.md +376 -0
  44. data/docs/retrieval_plan.md +251 -0
  45. data/docs/smartrag_improvement_plan.md +201 -0
  46. data/docs/smartrag_refactor.md +216 -0
  47. data/docs/todo.md +931 -0
  48. data/examples/common.rb +1 -1
  49. data/exe/smart-rag-db +163 -0
  50. data/exe/smart-rag-media-worker +34 -0
  51. data/lib/smart_rag/config.rb +12 -0
  52. data/lib/smart_rag/core/document_processor.rb +80 -16
  53. data/lib/smart_rag/core/local_content_store.rb +51 -0
  54. data/lib/smart_rag/core/media_extractors.rb +140 -0
  55. data/lib/smart_rag/core/media_job_queue.rb +353 -0
  56. data/lib/smart_rag/core/media_metadata_extractor.rb +188 -0
  57. data/lib/smart_rag/core/media_object_registry.rb +79 -0
  58. data/lib/smart_rag/core/media_processor.rb +228 -0
  59. data/lib/smart_rag/core/media_safety_policy.rb +61 -0
  60. data/lib/smart_rag/core/s3_content_store.rb +78 -0
  61. data/lib/smart_rag/core/transcript_normalizer.rb +44 -0
  62. data/lib/smart_rag/core/video_semantic_extractor.rb +130 -0
  63. data/lib/smart_rag/http_access_policy.rb +86 -0
  64. data/lib/smart_rag/http_app.rb +188 -0
  65. data/lib/smart_rag/models/embedding.rb +1 -1
  66. data/lib/smart_rag/models/research_topic.rb +1 -1
  67. data/lib/smart_rag/models/research_topic_section.rb +5 -0
  68. data/lib/smart_rag/models/research_topic_tag.rb +5 -0
  69. data/lib/smart_rag/models/search_log.rb +1 -1
  70. data/lib/smart_rag/models/section_fts.rb +5 -0
  71. data/lib/smart_rag/models/section_tag.rb +5 -0
  72. data/lib/smart_rag/models/source_document.rb +1 -1
  73. data/lib/smart_rag/models/source_section.rb +1 -1
  74. data/lib/smart_rag/models/tag.rb +1 -1
  75. data/lib/smart_rag/models/text_search_config.rb +5 -0
  76. data/lib/smart_rag/retrieve.rb +72 -1
  77. data/lib/smart_rag/services/embedding_service.rb +1 -1
  78. data/lib/smart_rag/services/fulltext_search_service.rb +11 -13
  79. data/lib/smart_rag/services/hybrid_search_service.rb +15 -11
  80. data/lib/smart_rag/services/summarization_service.rb +1 -1
  81. data/lib/smart_rag/services/tag_service.rb +1 -1
  82. data/lib/smart_rag/version.rb +1 -1
  83. data/lib/smart_rag.rb +264 -30
  84. data/patch_language.rb +27 -0
  85. data/requirements.md +376 -0
  86. data/source_documents_export.json +11072 -0
  87. data/todo.md +931 -0
  88. data/workers/analyze_content.rb +6 -2
  89. data/workers/get_embedding.rb +1 -1
  90. metadata +151 -38
@@ -1,6 +1,5 @@
1
1
  adapters:
2
2
  openai: OpenAIAdapter
3
- logger_file: ./log/prompt.log
4
3
  llms:
5
4
  OllamaEmbedding:
6
5
  adapter: openai
@@ -10,6 +9,9 @@ llms:
10
9
  SiliconFlow:
11
10
  adapter: openai
12
11
  url: https://api.siliconflow.cn/v1/
13
- api_key: ""
12
+ api_key: ENV["SILICONFLOW_API_KEY"]
13
+ # Worker files are loaded by SmartRAG services directly from the gem's
14
+ # workers/ directory. These paths are only fallbacks for standalone
15
+ # SmartPrompt usage and must stay non-empty.
14
16
  worker_path: "./workers"
15
17
  template_path: "./templates"
data/config/smart_rag.yml CHANGED
@@ -29,7 +29,7 @@ embedding:
29
29
  api_key: <%= ENV['EMBEDDING_API_KEY'] || 'ollama-local' %>
30
30
  endpoint: <%= ENV['EMBEDDING_ENDPOINT'] || 'http://localhost:11434/v1/embeddings' %>
31
31
  model: <%= ENV['EMBEDDING_MODEL'] || 'qwen3-embedding' %>
32
- dimensions: <%= ENV['EMBEDDING_DIMENSIONS'] || 1024 %>
32
+ dimensions: <%= ENV['EMBEDDING_DIMENSIONS'] || 4096 %>
33
33
  # Request timeout in seconds
34
34
  timeout: 30
35
35
  # Batch size for embedding generation
@@ -130,6 +130,50 @@ document:
130
130
  # Download timeout for URLs
131
131
  download_timeout: <%= ENV['DOWNLOAD_TIMEOUT'] || 30 %>
132
132
 
133
+ # Media ingestion, extraction, storage, and asynchronous jobs
134
+ media:
135
+ max_file_size_mb: <%= ENV['MEDIA_MAX_FILE_SIZE_MB'] || 50 %>
136
+ max_duration_ms: <%= ENV['MEDIA_MAX_DURATION_MS'] || 7200000 %>
137
+ command_timeout_seconds: <%= ENV['MEDIA_COMMAND_TIMEOUT'] || 120 %>
138
+ allow_private_urls: <%= ENV['MEDIA_ALLOW_PRIVATE_URLS'] == 'true' %>
139
+ allowed_hosts: <%= ENV.fetch('MEDIA_ALLOWED_HOSTS', '').split(',').map(&:strip).reject(&:empty?) %>
140
+ content_store:
141
+ enabled: <%= ENV['MEDIA_CONTENT_STORE_ENABLED'] == 'true' %>
142
+ provider: <%= ENV['MEDIA_CONTENT_STORE_PROVIDER'] || 'local' %>
143
+ root: <%= ENV['MEDIA_CONTENT_STORE_ROOT'] || File.expand_path('../storage/media', __dir__) %>
144
+ bucket: <%= ENV['MEDIA_S3_BUCKET'] %>
145
+ region: <%= ENV['MEDIA_S3_REGION'] || 'us-east-1' %>
146
+ prefix: <%= ENV['MEDIA_S3_PREFIX'] || 'smart-rag/media' %>
147
+ endpoint: <%= ENV['MEDIA_S3_ENDPOINT'] %>
148
+ force_path_style: <%= ENV['MEDIA_S3_FORCE_PATH_STYLE'] == 'true' %>
149
+ access_key_id: <%= ENV['MEDIA_S3_ACCESS_KEY_ID'] %>
150
+ secret_access_key: <%= ENV['MEDIA_S3_SECRET_ACCESS_KEY'] %>
151
+ openai:
152
+ base_url: <%= ENV['MEDIA_OPENAI_BASE_URL'] || 'https://api.openai.com' %>
153
+ api_key: <%= ENV['MEDIA_OPENAI_API_KEY'] || ENV['OPENAI_API_KEY'] %>
154
+ vision_model: <%= ENV['MEDIA_VISION_MODEL'] %>
155
+ transcription_model: <%= ENV['MEDIA_TRANSCRIPTION_MODEL'] %>
156
+ timeout_seconds: <%= ENV['MEDIA_MODEL_TIMEOUT'] || 120 %>
157
+ language: <%= ENV['MEDIA_TRANSCRIPTION_LANGUAGE'] %>
158
+ ocr:
159
+ provider: <%= ENV['MEDIA_OCR_PROVIDER'] %>
160
+ language: <%= ENV['MEDIA_OCR_LANGUAGE'] %>
161
+ timeout_seconds: <%= ENV['MEDIA_OCR_TIMEOUT'] || 60 %>
162
+ async:
163
+ max_attempts: <%= ENV['MEDIA_JOB_MAX_ATTEMPTS'] || 3 %>
164
+ stale_after_seconds: <%= ENV['MEDIA_JOB_STALE_AFTER_SECONDS'] || 900 %>
165
+ retention_seconds: <%= ENV['MEDIA_JOB_RETENTION_SECONDS'] || 604800 %>
166
+ heartbeat_interval_seconds: <%= ENV['MEDIA_JOB_HEARTBEAT_INTERVAL_SECONDS'] || 30 %>
167
+
168
+ # Optional HTTP Bearer authentication and shared PostgreSQL quotas.
169
+ http_auth:
170
+ enabled: <%= ENV['SMARTRAG_HTTP_AUTH_ENABLED'] == 'true' %>
171
+ principal: <%= ENV['SMARTRAG_HTTP_PRINCIPAL'] || 'default' %>
172
+ token: <%= ENV['SMARTRAG_HTTP_TOKEN'] %>
173
+ quota:
174
+ requests_per_minute: <%= ENV['SMARTRAG_REQUESTS_PER_MINUTE'] || 0 %>
175
+ upload_bytes_per_day: <%= ENV['SMARTRAG_UPLOAD_BYTES_PER_DAY'] || 0 %>
176
+
133
177
  # Logging Configuration
134
178
  logging:
135
179
  # Log level: debug, info, warn, error, fatal
data/config.ru ADDED
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ $LOAD_PATH.unshift(File.expand_path('lib', __dir__))
4
+
5
+ require 'smart_rag'
6
+ require 'smart_rag/http_app'
7
+ require 'smart_rag/http_access_policy'
8
+
9
+ config_path = ENV['SMARTRAG_CONFIG_PATH']
10
+ rag = SmartRAG::SmartRAG.new(SmartRAG::Config.load(config_path))
11
+ access_policy = SmartRAG::HttpAccessPolicy.new(db: SmartRAG.db, config: rag.config[:http_auth] || {})
12
+
13
+ # Applications can replace this file and inject server-side OCR, vision, and
14
+ # transcription adapters through HttpApp's extractors: option.
15
+ run SmartRAG::HttpApp.new(rag: rag, access_policy: access_policy)
@@ -10,11 +10,12 @@ Sequel.migration do
10
10
  add_index :text_search_configs, :config_name
11
11
 
12
12
  # Seed initial language configurations
13
- # Note: These assume pg_jieba extension is installed for Chinese support
13
+ # Note: Chinese uses pg_catalog.simple when pg_jieba is not installed.
14
+ # Set 'zh' to 'jiebacfg' after installing the pg_jieba extension for real segmentation.
14
15
  from(:text_search_configs).multi_insert(
15
16
  [
16
17
  { language_code: 'en', config_name: 'pg_catalog.english' },
17
- { language_code: 'zh', config_name: 'jiebacfg' },
18
+ { language_code: 'zh', config_name: 'pg_catalog.simple' },
18
19
  { language_code: 'ja', config_name: 'pg_catalog.simple' },
19
20
  { language_code: 'ko', config_name: 'pg_catalog.simple' },
20
21
  { language_code: 'default', config_name: 'pg_catalog.simple' }
@@ -7,13 +7,14 @@ Sequel.migration do
7
7
  primary_key :id
8
8
  foreign_key :source_id, :source_sections, null: false, on_delete: :cascade
9
9
  # Vector dimension size (adjust based on embedding model)
10
- column :vector, 'vector(1024)', null: false
10
+ column :vector, 'vector(4096)', null: false
11
11
  DateTime :created_at, default: Sequel::CURRENT_TIMESTAMP
12
12
  end
13
13
 
14
- # Create IVFFLAT index for approximate nearest neighbor search
15
- # IVFFLAT provides good accuracy with fast search
16
- run 'CREATE INDEX idx_embedding_vector ON embeddings USING ivfflat (vector vector_cosine_ops) WITH (lists = 100)'
14
+ # NOTE: no vector index is created here because pgvector's ivfflat/hnsw
15
+ # indexes only support up to 2000 dimensions, while qwen3-embedding
16
+ # produces 4096-dimension vectors. Similarity search falls back to exact
17
+ # (sequential) scan, which is fine for small to medium datasets.
17
18
 
18
19
  # Composite index for source_id lookups
19
20
  add_index :embeddings, :source_id
@@ -0,0 +1,11 @@
1
+ Sequel.migration do
2
+ up do
3
+ add_column :source_sections, :metadata, :jsonb, default: '{}', null: false
4
+ add_index :source_sections, :metadata, type: :gin
5
+ end
6
+
7
+ down do
8
+ drop_index :source_sections, :metadata
9
+ drop_column :source_sections, :metadata
10
+ end
11
+ end
@@ -0,0 +1,25 @@
1
+ Sequel.migration do
2
+ up do
3
+ create_table :media_jobs do
4
+ primary_key :id
5
+ String :operation, null: false, size: 32
6
+ String :source, text: true, null: false
7
+ column :options, :jsonb, default: '{}', null: false
8
+ String :status, null: false, size: 20, default: 'queued'
9
+ Integer :attempts, null: false, default: 0
10
+ Integer :max_attempts, null: false, default: 3
11
+ column :result, :jsonb
12
+ String :error, text: true
13
+ DateTime :available_at, null: false, default: Sequel::CURRENT_TIMESTAMP
14
+ DateTime :started_at
15
+ DateTime :finished_at
16
+ DateTime :created_at, null: false, default: Sequel::CURRENT_TIMESTAMP
17
+ DateTime :updated_at, null: false, default: Sequel::CURRENT_TIMESTAMP
18
+ index [:status, :available_at]
19
+ end
20
+ end
21
+
22
+ down do
23
+ drop_table :media_jobs
24
+ end
25
+ end
@@ -0,0 +1,11 @@
1
+ Sequel.migration do
2
+ up do
3
+ add_index :media_jobs, [:status, :started_at]
4
+ add_index :media_jobs, [:status, :finished_at]
5
+ end
6
+
7
+ down do
8
+ drop_index :media_jobs, [:status, :finished_at]
9
+ drop_index :media_jobs, [:status, :started_at]
10
+ end
11
+ end
@@ -0,0 +1,80 @@
1
+ Sequel.migration do
2
+ up do
3
+ alter_table :media_jobs do
4
+ add_column :idempotency_key, String, size: 128
5
+ add_column :principal, String, size: 128, null: false, default: 'system'
6
+ add_column :lease_token, String, size: 64
7
+ add_column :heartbeat_at, DateTime
8
+ add_index [:principal, :idempotency_key], unique: true
9
+ add_index [:status, :heartbeat_at]
10
+ end
11
+
12
+ create_table :media_objects do
13
+ primary_key :id
14
+ String :content_hash, null: false, size: 128, unique: true
15
+ String :storage_uri, text: true, null: false
16
+ Integer :byte_size, null: false
17
+ Integer :reference_count, null: false, default: 0
18
+ DateTime :created_at, null: false, default: Sequel::CURRENT_TIMESTAMP
19
+ DateTime :updated_at, null: false, default: Sequel::CURRENT_TIMESTAMP
20
+ index :reference_count
21
+ end
22
+
23
+ create_table :media_object_references do
24
+ primary_key :id
25
+ foreign_key :media_object_id, :media_objects, null: false, on_delete: :cascade
26
+ foreign_key :document_id, :source_documents, null: false, on_delete: :cascade
27
+ DateTime :created_at, null: false, default: Sequel::CURRENT_TIMESTAMP
28
+ index [:media_object_id, :document_id], unique: true, name: :media_object_document_unique
29
+ index :document_id
30
+ end
31
+
32
+
33
+ create_table :api_rate_limits do
34
+ primary_key :id
35
+ String :principal, null: false, size: 128
36
+ String :period, null: false, size: 16
37
+ DateTime :window_start, null: false
38
+ Integer :request_count, null: false, default: 0
39
+ Bignum :byte_count, null: false, default: 0
40
+ DateTime :updated_at, null: false, default: Sequel::CURRENT_TIMESTAMP
41
+ index [:principal, :period, :window_start], unique: true, name: :api_rate_limit_window_unique
42
+ end
43
+
44
+ run <<~SQL
45
+ INSERT INTO media_objects (content_hash, storage_uri, byte_size, reference_count)
46
+ SELECT metadata->>'content_hash', metadata->>'storage_uri',
47
+ COALESCE((metadata->>'file_size')::bigint, 0), 0
48
+ FROM source_documents
49
+ WHERE metadata ? 'storage_uri' AND metadata ? 'content_hash'
50
+ ON CONFLICT (content_hash) DO NOTHING
51
+ SQL
52
+ run <<~SQL
53
+ INSERT INTO media_object_references (media_object_id, document_id)
54
+ SELECT mo.id, sd.id
55
+ FROM source_documents sd
56
+ JOIN media_objects mo ON mo.content_hash = sd.metadata->>'content_hash'
57
+ WHERE sd.metadata ? 'storage_uri'
58
+ ON CONFLICT DO NOTHING
59
+ SQL
60
+ run <<~SQL
61
+ UPDATE media_objects mo SET reference_count = refs.count
62
+ FROM (SELECT media_object_id, COUNT(*) AS count FROM media_object_references GROUP BY media_object_id) refs
63
+ WHERE refs.media_object_id = mo.id
64
+ SQL
65
+ end
66
+
67
+ down do
68
+ drop_table :api_rate_limits
69
+ drop_table :media_object_references
70
+ drop_table :media_objects
71
+ alter_table :media_jobs do
72
+ drop_index [:status, :heartbeat_at]
73
+ drop_index [:principal, :idempotency_key]
74
+ drop_column :heartbeat_at
75
+ drop_column :lease_token
76
+ drop_column :idempotency_key
77
+ drop_column :principal
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,38 @@
1
+ Sequel.migration do
2
+ up do
3
+ alter_table :source_documents do
4
+ add_column :principal, String, size: 128, null: false, default: 'system'
5
+ add_index :principal
6
+ add_index [:principal, :created_at]
7
+ end
8
+
9
+ alter_table :media_jobs do
10
+ add_foreign_key :staging_media_object_id, :media_objects, on_delete: :set_null
11
+ add_index :staging_media_object_id
12
+ end
13
+
14
+ run <<~SQL
15
+ UPDATE source_documents
16
+ SET principal = COALESCE(NULLIF(metadata->>'principal', ''), 'system')
17
+ SQL
18
+ run <<~SQL
19
+ UPDATE media_jobs mj
20
+ SET staging_media_object_id = mo.id
21
+ FROM media_objects mo
22
+ WHERE mj.source = mo.storage_uri
23
+ AND mj.staging_media_object_id IS NULL
24
+ SQL
25
+ end
26
+
27
+ down do
28
+ alter_table :media_jobs do
29
+ drop_index :staging_media_object_id
30
+ drop_column :staging_media_object_id
31
+ end
32
+ alter_table :source_documents do
33
+ drop_index [:principal, :created_at]
34
+ drop_index :principal
35
+ drop_column :principal
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'digest'
4
+ require 'json'
5
+
6
+ Sequel.migration do
7
+ up do
8
+ alter_table :media_jobs do
9
+ add_column :request_fingerprint, String, size: 64
10
+ end
11
+
12
+ self[:media_jobs].select(:id, :operation, :source, :options).each do |job|
13
+ options = begin
14
+ JSON.parse(job[:options].to_s)
15
+ rescue JSON::ParserError
16
+ job[:options].to_s
17
+ end
18
+ canonicalize = lambda do |value|
19
+ case value
20
+ when Hash
21
+ value.keys.map(&:to_s).sort.to_h do |key|
22
+ original_key = value.key?(key) ? key : value.keys.find { |candidate| candidate.to_s == key }
23
+ [key, canonicalize.call(value[original_key])]
24
+ end
25
+ when Array
26
+ value.map { |item| canonicalize.call(item) }
27
+ else
28
+ value
29
+ end
30
+ end
31
+ payload = { 'operation' => job[:operation].to_s, 'source' => job[:source].to_s,
32
+ 'options' => canonicalize.call(options) }
33
+ self[:media_jobs].where(id: job[:id]).update(
34
+ request_fingerprint: Digest::SHA256.hexdigest(JSON.generate(payload))
35
+ )
36
+ end
37
+
38
+ alter_table :media_jobs do
39
+ set_column_not_null :request_fingerprint
40
+ end
41
+ end
42
+
43
+ down do
44
+ alter_table :media_jobs do
45
+ drop_column :request_fingerprint
46
+ end
47
+ end
48
+ end
@@ -9,9 +9,9 @@ INSERT INTO text_search_configs (language_code, config_name, is_installed) VALUE
9
9
  ('en', 'pg_catalog.english', true),
10
10
  ('en_us', 'pg_catalog.english', true),
11
11
  ('en_gb', 'pg_catalog.english', true),
12
- ('zh', 'jiebacfg', true),
13
- ('zh_cn', 'jiebacfg', true),
14
- ('zh_tw', 'jiebacfg', true),
12
+ ('zh', 'pg_catalog.simple', true),
13
+ ('zh_cn', 'pg_catalog.simple', true),
14
+ ('zh_tw', 'pg_catalog.simple', true),
15
15
  ('ja', 'pg_catalog.simple', true),
16
16
  ('ja_jp', 'pg_catalog.simple', true),
17
17
  ('ko', 'pg_catalog.simple', true),