From 64d76038da14453bb8b0f356ca24b1c77e9c32bb Mon Sep 17 00:00:00 2001 From: Esity Date: Wed, 6 May 2026 22:44:04 -0500 Subject: [PATCH 1/2] fix(knowledge): use Legion::LLM.chat directly for synthesis Replace llm_chat shim with direct Legion::LLM.chat call matching the rest of the codebase convention. --- CHANGELOG.md | 5 +++++ lib/legion/extensions/knowledge/runners/query.rb | 7 +++++-- lib/legion/extensions/knowledge/version.rb | 2 +- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9154b3f..1d7d1c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## [0.6.11] - 2026-05-03 + +### Fixed +- Knowledge query synthesis now passes prompts directly into native `Legion::LLM.chat` dispatch instead of routing through the legacy nil-input `llm_chat` helper path. + ## [0.6.10] - 2026-04-28 ### Fixed diff --git a/lib/legion/extensions/knowledge/runners/query.rb b/lib/legion/extensions/knowledge/runners/query.rb index 0c1269f..5d53b94 100644 --- a/lib/legion/extensions/knowledge/runners/query.rb +++ b/lib/legion/extensions/knowledge/runners/query.rb @@ -91,8 +91,11 @@ def synthesize_answer(question, chunks) "Context:\n#{context_text}\n\nQuestion: #{question}\n\nAnswer:" end - result = llm_chat(message: prompt, caller: { extension: 'lex-knowledge' }) - result.is_a?(Hash) ? result[:content] : result + result = Legion::LLM.chat( # rubocop:disable Legion/HelperMigration/DirectLlm + message: prompt, + caller: { extension: 'lex-knowledge' } + ) + result.is_a?(Hash) ? result[:content] : result.content rescue StandardError => e "Error generating answer: #{e.message}" end diff --git a/lib/legion/extensions/knowledge/version.rb b/lib/legion/extensions/knowledge/version.rb index f6663c1..427bb2d 100644 --- a/lib/legion/extensions/knowledge/version.rb +++ b/lib/legion/extensions/knowledge/version.rb @@ -3,7 +3,7 @@ module Legion module Extensions module Knowledge - VERSION = '0.6.10' + VERSION = '0.6.11' end end end From bbb89ecdab0beaa82809ca2db63f9aa877b90eb1 Mon Sep 17 00:00:00 2001 From: Esity Date: Wed, 6 May 2026 22:48:52 -0500 Subject: [PATCH 2/2] =?UTF-8?q?fix(lex-knowledge):=20gap=20fixes=20?= =?UTF-8?q?=E2=80=94=20metadata=E2=86=92context,=20embed=20fallback,=20ret?= =?UTF-8?q?ire=20tag,=20error=20logging?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix 1: ingest_to_apollo sends context: instead of metadata: so chunk provenance (source_file, heading, section_path, chunk_index, token_count) is correctly stored in apollo_entries.source_context - Fix 2: llm_embed_available? now also accepts Legion::LLM.embed (single-item); build_embed_map falls back to per-chunk embed when embed_batch is unavailable - Fix 3: retire_file uses content_type 'observation' + tags ['retired'] instead of unknown type 'document_retired'; switches metadata: to context: - Fix 4: retrieve_chunks logs retrieval errors at warn level instead of silently swallowing them - Fix 5: synthesize_answer returns nil (not error string) on LLM failure and logs at warn level - Fix 6: MaintenanceRunner#enabled? also returns true when monitors config is present (no corpus_path required); args falls back to monitors.first[:path] in that case - Fix 7: query_count queries action: 'query' (lex-apollo log value) instead of 'knowledge_query' which always returned 0 - Add log helper to Runners::Query to satisfy Legion/HelperMigration rubocop cop --- .../knowledge/actors/maintenance_runner.rb | 10 ++++--- .../extensions/knowledge/runners/ingest.rb | 26 +++++++++++++------ .../knowledge/runners/maintenance.rb | 2 +- .../extensions/knowledge/runners/query.rb | 11 ++++++-- 4 files changed, 35 insertions(+), 14 deletions(-) diff --git a/lib/legion/extensions/knowledge/actors/maintenance_runner.rb b/lib/legion/extensions/knowledge/actors/maintenance_runner.rb index 8646b71..82a5ab6 100644 --- a/lib/legion/extensions/knowledge/actors/maintenance_runner.rb +++ b/lib/legion/extensions/knowledge/actors/maintenance_runner.rb @@ -22,16 +22,20 @@ def time end def enabled? # rubocop:disable Legion/Extension/ActorEnabledSideEffects - return false unless corpus_path && !corpus_path.empty? + return true if corpus_path && !corpus_path.empty? - true + Runners::Monitor.resolve_monitors.any? rescue StandardError => e log.warn(e.message) false end def args - { path: corpus_path } + path = corpus_path + return { path: path } if path && !path.empty? + + monitors = Runners::Monitor.resolve_monitors + monitors.any? ? { path: monitors.first[:path] } : { path: nil } end private diff --git a/lib/legion/extensions/knowledge/runners/ingest.rb b/lib/legion/extensions/knowledge/runners/ingest.rb index c50f12e..784fc78 100644 --- a/lib/legion/extensions/knowledge/runners/ingest.rb +++ b/lib/legion/extensions/knowledge/runners/ingest.rb @@ -186,7 +186,7 @@ def build_exists_map(chunks) private_class_method :build_exists_map def llm_embed_available? - defined?(Legion::LLM) && Legion::LLM.respond_to?(:embed_batch) + defined?(Legion::LLM) && (Legion::LLM.respond_to?(:embed_batch) || Legion::LLM.respond_to?(:embed)) end private_class_method :llm_embed_available? @@ -196,9 +196,19 @@ def paired_without_embed(chunks, exists_map) private_class_method :paired_without_embed def build_embed_map(needs_embed) - results = Legion::LLM.embed_batch(needs_embed.map { |c| c[:content] }) # rubocop:disable Legion/HelperMigration/DirectLlm - results.each_with_object({}) do |r, h| - h[needs_embed[r[:index]][:content_hash]] = r[:vector] unless r[:error] + if Legion::LLM.respond_to?(:embed_batch) + results = Legion::LLM.embed_batch(needs_embed.map { |c| c[:content] }) # rubocop:disable Legion/HelperMigration/DirectLlm + results.each_with_object({}) do |r, h| + h[needs_embed[r[:index]][:content_hash]] = r[:vector] unless r[:error] + end + else + needs_embed.each_with_object({}) do |chunk, h| + result = Legion::LLM.embed(chunk[:content]) # rubocop:disable Legion/HelperMigration/DirectLlm + vector = result.is_a?(Hash) ? result[:vector] : result + h[chunk[:content_hash]] = vector if vector.is_a?(Array) && vector.any? + rescue StandardError => e + log.warn(e.message) + end end rescue StandardError => e log.warn(e.message) @@ -253,7 +263,7 @@ def ingest_to_apollo(chunk, embedding) content_type: 'document_chunk', content_hash: chunk[:content_hash], tags: [chunk[:source_file], chunk[:heading], 'document_chunk'].compact.uniq, - metadata: { + context: { source_file: chunk[:source_file], heading: chunk[:heading], section_path: chunk[:section_path], @@ -272,10 +282,10 @@ def retire_file(file_path:) return unless Legion::Apollo.respond_to?(:ingest) && Legion::Apollo.started? Legion::Apollo.ingest( # rubocop:disable Legion/HelperMigration/DirectKnowledge - content: file_path, - content_type: 'document_retired', + content: "Retired document: #{file_path}", + content_type: 'observation', tags: [file_path, 'retired', 'document_chunk'].uniq, - metadata: { source_file: file_path, retired: true } + context: { source_file: file_path, retired: true } ) rescue StandardError => e log.warn(e.message) diff --git a/lib/legion/extensions/knowledge/runners/maintenance.rb b/lib/legion/extensions/knowledge/runners/maintenance.rb index 49fcc50..137adba 100644 --- a/lib/legion/extensions/knowledge/runners/maintenance.rb +++ b/lib/legion/extensions/knowledge/runners/maintenance.rb @@ -279,7 +279,7 @@ def quality_summary def query_count return 0 unless defined?(Legion::Data::Model::ApolloAccessLog) - Legion::Data::Model::ApolloAccessLog.where(action: 'knowledge_query').count + Legion::Data::Model::ApolloAccessLog.where(action: 'query').count rescue StandardError => _e 0 end diff --git a/lib/legion/extensions/knowledge/runners/query.rb b/lib/legion/extensions/knowledge/runners/query.rb index 5d53b94..469a501 100644 --- a/lib/legion/extensions/knowledge/runners/query.rb +++ b/lib/legion/extensions/knowledge/runners/query.rb @@ -9,6 +9,11 @@ module Runners module Query # rubocop:disable Legion/Extension/RunnerIncludeHelpers module_function + def log + Legion::Logging + end + private_class_method :log + def query(question:, top_k: nil, synthesize: true) started = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) resolved_k = top_k || settings_top_k || 5 @@ -75,7 +80,8 @@ def retrieve_chunks(question, top_k) tags: ['document_chunk'] ) result.is_a?(Hash) && result[:success] ? Array(result[:entries]) : [] - rescue StandardError => _e + rescue StandardError => e + log.warn("[knowledge][retrieve_chunks] retrieval failed: #{e.class}: #{e.message}") [] end private_class_method :retrieve_chunks @@ -97,7 +103,8 @@ def synthesize_answer(question, chunks) ) result.is_a?(Hash) ? result[:content] : result.content rescue StandardError => e - "Error generating answer: #{e.message}" + log.warn("[knowledge][synthesize_answer] LLM synthesis failed: #{e.class}: #{e.message}") + nil end private_class_method :synthesize_answer