insika 0.8.0 → 1.0.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +312 -2
- data/README.md +10 -6
- data/bin/insika +4 -1
- data/bin/insika-router +0 -1
- data/docs/AGENTS.md +142 -18
- data/docs/API.md +73 -0
- data/docs/ARCHITECTURE.md +56 -50
- data/docs/ARTIFACTS.md +33 -3
- data/docs/BENCHMARK.md +50 -7
- data/docs/CHANNELS.md +19 -2
- data/docs/CONTEXT.md +96 -27
- data/docs/CROSS-HARNESS-BENCH.md +157 -0
- data/docs/DEPLOY.md +70 -13
- data/docs/EMBEDDING.md +10 -0
- data/docs/EVALS.md +129 -10
- data/docs/FACTS.md +12 -6
- data/docs/HARVEST.md +8 -3
- data/docs/KNOWLEDGE.md +14 -0
- data/docs/MEDIA.md +6 -2
- data/docs/OBSERVABILITY.md +164 -28
- data/docs/PERFORMANCE.md +110 -0
- data/docs/PLUGINS.md +4 -4
- data/docs/POLICY.md +26 -14
- data/docs/RELEASING.md +9 -0
- data/docs/ROUTER.md +7 -0
- data/docs/RUBYLLM_2_MIGRATION.md +307 -0
- data/docs/SECURITY.md +34 -1
- data/docs/SHARED-CONVERSATIONS.md +40 -0
- data/docs/SKILLS.md +5 -3
- data/docs/STUDIO.md +122 -0
- data/docs/TOOLS.md +238 -42
- data/docs/operate.md +3 -0
- data/docs/prompts/ADD-TOOL.md +12 -2
- data/docs/prompts/DIAGNOSE-TURN.md +3 -0
- data/docs/prompts/GO-LIVE.md +3 -1
- data/lib/insika/agent_defaults.rb +140 -0
- data/lib/insika/agent_file_store.rb +12 -0
- data/lib/insika/agent_profile.rb +99 -17
- data/lib/insika/artifact_signing.rb +11 -0
- data/lib/insika/channels/web/widget.js +33 -0
- data/lib/insika/channels/web.rb +5 -2
- data/lib/insika/chat_builder.rb +124 -28
- data/lib/insika/checkpoint.rb +6 -1
- data/lib/insika/checkpoint_store.rb +59 -7
- data/lib/insika/commands/agent_payload.rb +3 -3
- data/lib/insika/commands/delete_kit.rb +26 -0
- data/lib/insika/commands/gate_harvest.rb +3 -0
- data/lib/insika/commands/review_guardrail_flag.rb +38 -0
- data/lib/insika/commands/run_distillation.rb +5 -8
- data/lib/insika/commands/run_harvest.rb +17 -1
- data/lib/insika/commands/seed_session.rb +128 -0
- data/lib/insika/commands/send_message.rb +9 -3
- data/lib/insika/commands/update_settings.rb +23 -1
- data/lib/insika/commands/write_kit.rb +27 -0
- data/lib/insika/compaction.rb +33 -7
- data/lib/insika/config_store.rb +87 -7
- data/lib/insika/context/builder.rb +29 -9
- data/lib/insika/context/priority.rb +5 -0
- data/lib/insika/context/provider.rb +7 -2
- data/lib/insika/context/providers/briefing.rb +11 -8
- data/lib/insika/context/providers/fence_notice.rb +27 -0
- data/lib/insika/context/providers/knowledge.rb +32 -6
- data/lib/insika/context/providers/memory.rb +61 -8
- data/lib/insika/context/providers/pending_confirmation.rb +52 -0
- data/lib/insika/context/providers/prompt.rb +17 -4
- data/lib/insika/context/providers/session.rb +46 -6
- data/lib/insika/context/reranker.rb +75 -0
- data/lib/insika/distill.rb +22 -13
- data/lib/insika/doctor.rb +142 -4
- data/lib/insika/dsl/runtime.rb +37 -4
- data/lib/insika/dsl/server_boot.rb +4 -12
- data/lib/insika/dsl.rb +19 -5
- data/lib/insika/edge_limiter.rb +27 -16
- data/lib/insika/egress_guard.rb +11 -3
- data/lib/insika/env_schema.rb +33 -73
- data/lib/insika/errors.rb +1 -0
- data/lib/insika/evals/assertions.rb +259 -18
- data/lib/insika/evals/golden.rb +230 -4
- data/lib/insika/evals/judge.rb +19 -10
- data/lib/insika/evals/moderator_compare.rb +47 -0
- data/lib/insika/evals/report.rb +61 -9
- data/lib/insika/evals/runner.rb +64 -4
- data/lib/insika/evals/simulator.rb +21 -3
- data/lib/insika/evals/transport.rb +317 -15
- data/lib/insika/event_stream.rb +63 -7
- data/lib/insika/evidence.rb +105 -17
- data/lib/insika/executor.rb +698 -164
- data/lib/insika/fence.rb +96 -0
- data/lib/insika/golden_store.rb +3 -0
- data/lib/insika/harvest.rb +26 -14
- data/lib/insika/harvest_engine.rb +1 -0
- data/lib/insika/harvest_store.rb +12 -0
- data/lib/insika/http_client.rb +87 -4
- data/lib/insika/kits.rb +54 -0
- data/lib/insika/knowledge.rb +34 -20
- data/lib/insika/llm_configurator.rb +46 -4
- data/lib/insika/llm_provider_store.rb +1 -3
- data/lib/insika/llm_trace_store.rb +54 -0
- data/lib/insika/mcp_client.rb +27 -63
- data/lib/insika/mcp_live_tool.rb +55 -9
- data/lib/insika/mcp_store.rb +37 -3
- data/lib/insika/mcp_tool_registry.rb +76 -26
- data/lib/insika/media.rb +38 -25
- data/lib/insika/memory_store.rb +16 -3
- data/lib/insika/memory_trim.rb +60 -0
- data/lib/insika/middleware.rb +12 -3
- data/lib/insika/model_metrics_store.rb +285 -0
- data/lib/insika/model_registry.rb +110 -0
- data/lib/insika/model_resolver.rb +1 -1
- data/lib/insika/model_selection.rb +14 -21
- data/lib/insika/model_visible.rb +17 -6
- data/lib/insika/onboarding.rb +1 -0
- data/lib/insika/outcome_store.rb +1 -1
- data/lib/insika/overlay_tool_registry.rb +30 -2
- data/lib/insika/pack_importer.rb +54 -9
- data/lib/insika/packaging.rb +14 -5
- data/lib/insika/pending_action_store.rb +45 -18
- data/lib/insika/plugin/loader.rb +4 -2
- data/lib/insika/prefix_fingerprint.rb +32 -27
- data/lib/insika/profile_source.rb +5 -0
- data/lib/insika/provider_error_classifier.rb +18 -3
- data/lib/insika/queue_policy.rb +7 -2
- data/lib/insika/refinement/proposer.rb +42 -17
- data/lib/insika/reliability.rb +12 -3
- data/lib/insika/router/app.rb +14 -5
- data/lib/insika/router/session_key.rb +35 -5
- data/lib/insika/safety/config.rb +20 -3
- data/lib/insika/safety/corpus.rb +10 -3
- data/lib/insika/safety/factory.rb +39 -1
- data/lib/insika/safety/input_guardrail.rb +21 -1
- data/lib/insika/safety/judge_moderator.rb +70 -0
- data/lib/insika/safety/output_validator.rb +29 -5
- data/lib/insika/safety/prompt_echo.rb +89 -0
- data/lib/insika/schema_guard.rb +53 -76
- data/lib/insika/server/app.rb +58 -7
- data/lib/insika/server/rack_app.rb +2 -0
- data/lib/insika/server/responses.rb +50 -5
- data/lib/insika/session_actor.rb +58 -28
- data/lib/insika/session_store.rb +163 -11
- data/lib/insika/settings_store.rb +57 -2
- data/lib/insika/shared_conversations.rb +305 -0
- data/lib/insika/skill_catalog.rb +28 -2
- data/lib/insika/skill_store.rb +6 -0
- data/lib/insika/soak/runner.rb +1 -1
- data/lib/insika/spoken_transcript.rb +31 -0
- data/lib/insika/steer_injector.rb +7 -48
- data/lib/insika/store.rb +29 -1
- data/lib/insika/stores/json_value.rb +37 -0
- data/lib/insika/stores/memory.rb +22 -35
- data/lib/insika/stores/postgres.rb +295 -0
- data/lib/insika/stores/sqlite.rb +257 -43
- data/lib/insika/stores/turn_counter.rb +25 -0
- data/lib/insika/studio/app.rb +1459 -163
- data/lib/insika/studio/assets/dist/application.css +1 -1
- data/lib/insika/studio/assets/dist/application.js +25 -25
- data/lib/insika/studio/assets/dist/fonts/ibm-plex-mono-latin-400-normal.woff2 +0 -0
- data/lib/insika/studio/assets/dist/fonts/ibm-plex-mono-latin-500-normal.woff2 +0 -0
- data/lib/insika/studio/assets/dist/fonts/ibm-plex-mono-latin-600-normal.woff2 +0 -0
- data/lib/insika/studio/assets/dist/fonts/ibm-plex-sans-latin-wght-normal.woff2 +0 -0
- data/lib/insika/studio/forms.rb +189 -12
- data/lib/insika/studio/nav_icons.rb +3 -0
- data/lib/insika/studio/views/_agent_config_retrieval.erb +73 -0
- data/lib/insika/studio/views/_agent_picker.erb +15 -0
- data/lib/insika/studio/views/_agent_tab_config.erb +83 -243
- data/lib/insika/studio/views/_agent_tab_history.erb +56 -1
- data/lib/insika/studio/views/_agent_tab_performance.erb +34 -0
- data/lib/insika/studio/views/_agent_tab_prompts.erb +7 -9
- data/lib/insika/studio/views/_agent_tab_skills.erb +16 -7
- data/lib/insika/studio/views/_agent_tab_tools.erb +90 -0
- data/lib/insika/studio/views/_agents_master.erb +8 -12
- data/lib/insika/studio/views/_cfg_alerts.erb +4 -0
- data/lib/insika/studio/views/_cfg_budget.erb +20 -0
- data/lib/insika/studio/views/_cfg_distill.erb +26 -0
- data/lib/insika/studio/views/_cfg_followup.erb +30 -0
- data/lib/insika/studio/views/_cfg_grounding.erb +21 -0
- data/lib/insika/studio/views/_cfg_guardrails.erb +47 -0
- data/lib/insika/studio/views/_cfg_harvest.erb +35 -0
- data/lib/insika/studio/views/_cfg_reliability.erb +32 -0
- data/lib/insika/studio/views/_chats_master.erb +66 -0
- data/lib/insika/studio/views/_customers_master.erb +37 -0
- data/lib/insika/studio/views/_empty.erb +15 -0
- data/lib/insika/studio/views/_facts_master.erb +42 -0
- data/lib/insika/studio/views/_harvest_candidate_detail.erb +14 -0
- data/lib/insika/studio/views/_identity.erb +16 -0
- data/lib/insika/studio/views/_kpi.erb +17 -0
- data/lib/insika/studio/views/_message.erb +40 -2
- data/lib/insika/studio/views/_model_chart.erb +82 -0
- data/lib/insika/studio/views/_refinement_master.erb +38 -0
- data/lib/insika/studio/views/{tool_edit.erb → _tool_edit.erb} +38 -11
- data/lib/insika/studio/views/_tool_users.erb +17 -0
- data/lib/insika/studio/views/_why.erb +53 -0
- data/lib/insika/studio/views/agent_detail.erb +17 -10
- data/lib/insika/studio/views/agents.erb +28 -16
- data/lib/insika/studio/views/approvals.erb +16 -11
- data/lib/insika/studio/views/artifact.erb +20 -7
- data/lib/insika/studio/views/artifacts.erb +19 -14
- data/lib/insika/studio/views/chats.erb +22 -29
- data/lib/insika/studio/views/customer.erb +88 -43
- data/lib/insika/studio/views/customers.erb +16 -26
- data/lib/insika/studio/views/evals.erb +49 -31
- data/lib/insika/studio/views/fact_detail.erb +114 -0
- data/lib/insika/studio/views/facts.erb +15 -116
- data/lib/insika/studio/views/followups.erb +63 -52
- data/lib/insika/studio/views/funnel.erb +104 -43
- data/lib/insika/studio/views/harvest.erb +85 -59
- data/lib/insika/studio/views/home.erb +138 -23
- data/lib/insika/studio/views/inbox.erb +36 -0
- data/lib/insika/studio/views/kits.erb +176 -0
- data/lib/insika/studio/views/knowledge.erb +92 -80
- data/lib/insika/studio/views/layout.erb +42 -11
- data/lib/insika/studio/views/live.erb +45 -0
- data/lib/insika/studio/views/mcp.erb +43 -13
- data/lib/insika/studio/views/models.erb +187 -0
- data/lib/insika/studio/views/parity.erb +38 -32
- data/lib/insika/studio/views/playground.erb +13 -9
- data/lib/insika/studio/views/refinement.erb +15 -224
- data/lib/insika/studio/views/refinement_detail.erb +243 -0
- data/lib/insika/studio/views/session.erb +153 -118
- data/lib/insika/studio/views/settings.erb +86 -22
- data/lib/insika/studio/views/skills.erb +53 -19
- data/lib/insika/studio/views/system_files.erb +7 -7
- data/lib/insika/studio/views/task.erb +45 -15
- data/lib/insika/studio/views/tasks.erb +35 -18
- data/lib/insika/studio/views/tools.erb +62 -73
- data/lib/insika/task_actor.rb +4 -1
- data/lib/insika/task_store.rb +67 -1
- data/lib/insika/telemetry/pricing.rb +16 -14
- data/lib/insika/telemetry/recorder.rb +115 -9
- data/lib/insika/telemetry/ruby_llm_instrumenter.rb +110 -0
- data/lib/insika/telemetry.rb +12 -2
- data/lib/insika/templates/daily-digest/README.md +13 -0
- data/lib/insika/templates/daily-digest/agent.rb +22 -11
- data/lib/insika/testing/store_contract.rb +65 -0
- data/lib/insika/text_tool_call.rb +66 -0
- data/lib/insika/tick.rb +7 -1
- data/lib/insika/token_store.rb +5 -0
- data/lib/insika/tool_assembly.rb +21 -13
- data/lib/insika/tool_batch.rb +1 -1
- data/lib/insika/tool_catalog.rb +12 -2
- data/lib/insika/tool_definition.rb +82 -13
- data/lib/insika/tool_envelope.rb +266 -29
- data/lib/insika/tool_manifest.rb +1 -0
- data/lib/insika/tool_store.rb +12 -4
- data/lib/insika/tool_trace_store.rb +15 -8
- data/lib/insika/tool_usage_report.rb +14 -4
- data/lib/insika/tools/a2a_remote.rb +1 -1
- data/lib/insika/tools/agent_enum.rb +1 -1
- data/lib/insika/tools/confirm_pending.rb +112 -0
- data/lib/insika/tools/data_defined_tool.rb +24 -15
- data/lib/insika/tools/generate_image.rb +2 -2
- data/lib/insika/tools/load_knowledge.rb +7 -3
- data/lib/insika/tools/load_skill.rb +3 -3
- data/lib/insika/tools/present.rb +115 -0
- data/lib/insika/tools/remember.rb +7 -2
- data/lib/insika/tools/run_persona_eval.rb +9 -6
- data/lib/insika/tools/save_artifact.rb +16 -6
- data/lib/insika/tools/schedule_followup.rb +3 -3
- data/lib/insika/tools/stuck_signal.rb +3 -3
- data/lib/insika/tools/subagent.rb +5 -5
- data/lib/insika/tools/subagents.rb +3 -3
- data/lib/insika/tools/tool_search.rb +11 -7
- data/lib/insika/tools/tts.rb +2 -2
- data/lib/insika/tools/update_briefing.rb +4 -4
- data/lib/insika/turn_output.rb +2 -2
- data/lib/insika/turn_state.rb +31 -1
- data/lib/insika/turn_timing.rb +51 -14
- data/lib/insika/version.rb +1 -1
- data/lib/insika/wiring/graph.rb +88 -16
- data/lib/insika/wiring/graph_chat.rb +4 -0
- data/lib/insika/workflow.rb +25 -64
- data/lib/insika.rb +23 -3
- metadata +74 -13
- data/lib/insika/commands/import_mcp_tools.rb +0 -48
- data/lib/insika/mcp_http_client.rb +0 -67
- data/lib/insika/mcp_tool_ingestor.rb +0 -165
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "securerandom"
|
|
4
|
+
|
|
5
|
+
module Insika
|
|
6
|
+
module Telemetry
|
|
7
|
+
# One instance belongs to one chat context, never to a graph or current fiber.
|
|
8
|
+
class RubyLLMInstrumenter
|
|
9
|
+
def initialize(emit:, delegate: nil, operation: nil, model: nil, pricing: nil)
|
|
10
|
+
@emit, @delegate, @operation, @model, @pricing = emit, delegate, operation, model, pricing
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def instrument(name, payload = {}, &block)
|
|
14
|
+
return chat(payload, &block) if name == "chat.ruby_llm" && block
|
|
15
|
+
return request(payload, &block) if name == "request.ruby_llm" && block
|
|
16
|
+
|
|
17
|
+
result = delegate(name, payload, &block)
|
|
18
|
+
record(:llm_usage, payload) if name == "usage.ruby_llm"
|
|
19
|
+
result
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def chat(payload, &block)
|
|
25
|
+
previous_model, previous_request = @model, @request_id
|
|
26
|
+
@model, @request_id = scalar(payload[:model]), nil
|
|
27
|
+
delegate("chat.ruby_llm", payload, &block)
|
|
28
|
+
ensure
|
|
29
|
+
@model, @request_id = previous_model, previous_request
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def request(payload, &block)
|
|
33
|
+
@request_id = SecureRandom.uuid
|
|
34
|
+
started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
35
|
+
error = nil
|
|
36
|
+
begin
|
|
37
|
+
delegate("request.ruby_llm", payload, &block)
|
|
38
|
+
rescue Exception => error # Preserve cancellation and the original model exception too.
|
|
39
|
+
raise
|
|
40
|
+
ensure
|
|
41
|
+
record(:llm_request, payload, error: error,
|
|
42
|
+
duration_ms: (Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) * 1000)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Host notifications still wrap the operation. A broken subscriber must not
|
|
47
|
+
# retry the model, replace its result, or swallow/replace its exception.
|
|
48
|
+
def delegate(name, payload, &block)
|
|
49
|
+
return block&.call(payload) unless @delegate.respond_to?(:instrument)
|
|
50
|
+
|
|
51
|
+
called = false
|
|
52
|
+
result = error = nil
|
|
53
|
+
operation = proc do |*args|
|
|
54
|
+
called = true
|
|
55
|
+
begin
|
|
56
|
+
result = block.call(*args)
|
|
57
|
+
rescue Exception => caught
|
|
58
|
+
error = caught
|
|
59
|
+
raise
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
begin
|
|
63
|
+
block ? @delegate.instrument(name, payload, &operation) : @delegate.instrument(name, payload)
|
|
64
|
+
rescue StandardError
|
|
65
|
+
# Telemetry is best effort; the operation's outcome is authoritative.
|
|
66
|
+
end
|
|
67
|
+
raise error if error
|
|
68
|
+
return unless block
|
|
69
|
+
|
|
70
|
+
called ? result : block.call(payload)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def record(type, payload, error: nil, duration_ms: nil)
|
|
74
|
+
data = {
|
|
75
|
+
"operation" => scalar(payload[:operation] || @operation),
|
|
76
|
+
"provider" => scalar(payload[:provider]),
|
|
77
|
+
"model" => scalar(payload[:model] || @model),
|
|
78
|
+
"status" => type == :llm_request ? (error ? "failed" : "succeeded") : scalar(payload[:status]),
|
|
79
|
+
"request_id" => @request_id
|
|
80
|
+
}
|
|
81
|
+
if type == :llm_request
|
|
82
|
+
data["duration_ms"] = duration_ms
|
|
83
|
+
data["exception_class"] = error.class.name if error
|
|
84
|
+
else
|
|
85
|
+
%i[input output cache_read cache_write thinking].each do |field|
|
|
86
|
+
value = payload[:tokens]&.public_send(field)
|
|
87
|
+
data["#{field}_tokens"] = value.is_a?(Numeric) ? value : nil
|
|
88
|
+
end
|
|
89
|
+
value = payload[:cost]&.total
|
|
90
|
+
value = priced(data) unless value.is_a?(Numeric)
|
|
91
|
+
data["cost"] = value.is_a?(Numeric) ? value : nil
|
|
92
|
+
end
|
|
93
|
+
@emit.call(type, data)
|
|
94
|
+
rescue StandardError
|
|
95
|
+
nil
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# The operator's rates fill in only what the library could not price (Pricing.from_env).
|
|
99
|
+
def priced(data)
|
|
100
|
+
@pricing&.cost(model: data["model"], input_tokens: data["input_tokens"],
|
|
101
|
+
output_tokens: data["output_tokens"], cached_tokens: data["cache_read_tokens"],
|
|
102
|
+
cache_creation_tokens: data["cache_write_tokens"])
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def scalar(value)
|
|
106
|
+
value.to_s if value.is_a?(String) || value.is_a?(Symbol)
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
data/lib/insika/telemetry.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "logger"
|
|
4
|
+
|
|
3
5
|
require "async"
|
|
4
6
|
require_relative "telemetry/pricing"
|
|
5
7
|
require_relative "telemetry/recorder"
|
|
@@ -15,7 +17,7 @@ module Insika
|
|
|
15
17
|
# (`OTEL_EXPORTER_OTLP_ENDPOINT`, `OTEL_TRACES_EXPORTER`). The destination/protocol
|
|
16
18
|
# follows the OTEL SDK's default config (env) — SigNoz/Tempo/Jaeger/Collector. The
|
|
17
19
|
# OTEL_* keys are the OpenTelemetry SDK's own env and stay verbatim; only our opt-in
|
|
18
|
-
# toggle is
|
|
20
|
+
# toggle is INSIKA_OTEL.
|
|
19
21
|
#
|
|
20
22
|
# Metrics ride the same switch and the SAME standard env: the SDK
|
|
21
23
|
# registers a periodic metric reader when the OTLP metrics exporter is loadable,
|
|
@@ -39,6 +41,7 @@ module Insika
|
|
|
39
41
|
require "opentelemetry/sdk"
|
|
40
42
|
require "opentelemetry/exporter/otlp"
|
|
41
43
|
load_metrics_sdk
|
|
44
|
+
OpenTelemetry.logger = sdk_logger
|
|
42
45
|
unless @configured
|
|
43
46
|
OpenTelemetry::SDK.configure { |c| c.service_name = service_name }
|
|
44
47
|
@configured = true
|
|
@@ -49,6 +52,13 @@ module Insika
|
|
|
49
52
|
meter: meter, pricing: pricing(env))
|
|
50
53
|
end
|
|
51
54
|
|
|
55
|
+
# The SDK's default logger writes to $stdout, which Ruby buffers when it is a
|
|
56
|
+
# pipe (Falcon workers, a container log): a rejected export (404 from an
|
|
57
|
+
# ingest that wants a header) never reached the log. $stderr is unbuffered.
|
|
58
|
+
def sdk_logger(io = $stderr)
|
|
59
|
+
Logger.new(io, progname: "otel", level: ENV.fetch("OTEL_LOG_LEVEL", "warn"))
|
|
60
|
+
end
|
|
61
|
+
|
|
52
62
|
# Did `setup` wire the metric instruments too (SDK present, at least one reader)?
|
|
53
63
|
# Only meaningful after `setup` — it exists for the boot banners.
|
|
54
64
|
def metrics? = @metrics == true
|
|
@@ -68,7 +78,7 @@ module Insika
|
|
|
68
78
|
# Operator-declared rates (USD per million tokens) as JSON in
|
|
69
79
|
# INSIKA_MODEL_PRICING. Unset/malformed -> an empty table -> no cost is reported.
|
|
70
80
|
def pricing(env = ENV)
|
|
71
|
-
table = Pricing.
|
|
81
|
+
table = Pricing.from_env(env)
|
|
72
82
|
table.empty? ? nil : table
|
|
73
83
|
end
|
|
74
84
|
|
|
@@ -20,6 +20,19 @@ Then open `/studio`, log in with the printed token, and either wait for the
|
|
|
20
20
|
schedule (22:00 America/Sao_Paulo) or send a message to the "reporter" agent
|
|
21
21
|
in the Playground — the artifact lands on the Artifacts tab either way.
|
|
22
22
|
|
|
23
|
+
## Report design
|
|
24
|
+
|
|
25
|
+
The skill asks for a dated executive takeaway, consistent KPI cards, warm neutrals
|
|
26
|
+
with one amber accent, labeled charts, sources and limitations. Reports adapt to
|
|
27
|
+
phone widths and the system light/dark preference. Native Chart/Table radio choices,
|
|
28
|
+
CSS hover/focus highlights and expandable details work without JavaScript. Exact
|
|
29
|
+
values remain accessible on touch and in a table; focus is visible, 140ms transitions
|
|
30
|
+
respect reduced motion, and print includes the full table and details.
|
|
31
|
+
|
|
32
|
+
See the [fictional-data example](https://github.com/guizaols/insika/blob/main/examples/scheduled-report/interactive-report.html).
|
|
33
|
+
These interactions explore a saved snapshot, not live data. This guidance applies
|
|
34
|
+
to newly generated reports; existing artifacts stay unchanged.
|
|
35
|
+
|
|
23
36
|
## What's real here
|
|
24
37
|
|
|
25
38
|
- The **schedule** is a live engine feature: the tick actually fires this
|
|
@@ -47,21 +47,32 @@ reporter = Insika.agent("reporter") do
|
|
|
47
47
|
overrides: { turn_timeout: 600, max_tool_calls: 120 }
|
|
48
48
|
|
|
49
49
|
# The generic, store-free skill: how a report is BUILT — the inline-SVG
|
|
50
|
-
# pattern (palette, table,
|
|
50
|
+
# pattern (palette, table, native controls, light/dark). No store ids, no
|
|
51
51
|
# queries — that half belongs in a merchant's pack, not here.
|
|
52
52
|
skill "daily-digest",
|
|
53
53
|
description: "How to build the daily sales digest as a self-contained HTML report",
|
|
54
54
|
instructions: <<~MD
|
|
55
|
-
Build
|
|
56
|
-
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
55
|
+
Build ONE self-contained executive HTML report with a <style> block:
|
|
56
|
+
- Lead with a clear title, period, generation date and concise,
|
|
57
|
+
evidence-backed takeaway. Name sources and limitations. Use only
|
|
58
|
+
supplied numbers; show missing data honestly and label demo data.
|
|
59
|
+
- Use warm neutrals and one amber accent (or the merchant's accent),
|
|
60
|
+
consistent type, spacing and KPI cards, and system dark mode.
|
|
61
|
+
- Include a responsive inline SVG or HTML bar chart with clear labels,
|
|
62
|
+
units and exact values, plus a captioned table with column headers.
|
|
63
|
+
- Offer Chart/Table through labeled, focusable native radios grouped
|
|
64
|
+
in a fieldset with a legend; use CSS :checked, not ARIA tabs.
|
|
65
|
+
- Use hover/focus tooltips in reserved space; keep them visible when
|
|
66
|
+
hovered. Add CSS highlights, a visible 2px focus outline, and
|
|
67
|
+
details/summary for supporting evidence. Exact values
|
|
68
|
+
must remain available on touch without hovering.
|
|
69
|
+
- Use 140ms color/opacity transitions; disable them for reduced motion.
|
|
70
|
+
No decorative animation. Print the full table and supporting details
|
|
71
|
+
regardless of the selected view or collapsed state; hide controls.
|
|
72
|
+
- This is a dated data snapshot, not a live feed. No scripts, event
|
|
73
|
+
handlers, forms, iframe, external fonts or fetches. The report's
|
|
74
|
+
default-src 'none' policy blocks external resources.
|
|
75
|
+
Keep it readable on a phone. The channel message is just the link.
|
|
65
76
|
MD
|
|
66
77
|
end
|
|
67
78
|
|
|
@@ -117,6 +117,12 @@ RSpec.shared_examples "an Insika store" do
|
|
|
117
117
|
expect(store.list("s", "task:")).to eq(%w[task:1 task:2])
|
|
118
118
|
end
|
|
119
119
|
|
|
120
|
+
it "keeps the prefix boundary exact, ASCII or not" do
|
|
121
|
+
%w[task task: task:1 task:é task; tasks sessão:1 sessão:2 sessãoX run9 run9a run: taz taz1 ta{].each { |k| store.set("s", k, 1) }
|
|
122
|
+
expect([store.list("s", "task:"), store.list("s", "sessão:"), store.list("s", "run9"), store.list("s", "taz")])
|
|
123
|
+
.to eq([%w[task: task:1 task:é], %w[sessão:1 sessão:2], %w[run9 run9a], %w[taz taz1]])
|
|
124
|
+
end
|
|
125
|
+
|
|
120
126
|
it " returns [] for an empty scope" do #
|
|
121
127
|
expect(store.list("s")).to eq([])
|
|
122
128
|
end
|
|
@@ -128,6 +134,65 @@ RSpec.shared_examples "an Insika store" do
|
|
|
128
134
|
end
|
|
129
135
|
end
|
|
130
136
|
|
|
137
|
+
# Bulk read: what a caller that lists keys and reads each one needs, in one
|
|
138
|
+
# call. A backend may answer it in one query; the default is list + get.
|
|
139
|
+
describe "#entries" do
|
|
140
|
+
it "returns [key, value] pairs sorted by key, values deserialized" do
|
|
141
|
+
store.set("s", "b", { "n" => 2 })
|
|
142
|
+
store.set("s", "a", [1])
|
|
143
|
+
store.set("other", "a", "x")
|
|
144
|
+
expect(store.entries("s")).to eq([["a", [1]], ["b", { "n" => 2 }]])
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
it "filters by prefix with start_with?, ASCII or not" do
|
|
148
|
+
store.set("s", "ab", 1)
|
|
149
|
+
store.set("s", "abc", 2)
|
|
150
|
+
store.set("s", "b", 3)
|
|
151
|
+
store.set("s", "é1", 4)
|
|
152
|
+
expect(store.entries("s", "ab")).to eq([["ab", 1], ["abc", 2]])
|
|
153
|
+
expect(store.entries("s", "é")).to eq([["é1", 4]])
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
it "returns [] for an empty scope" do
|
|
157
|
+
expect(store.entries("nothing")).to eq([])
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# The last-written keys, newest first: what a "recent" list needs without
|
|
162
|
+
# reading the whole scope.
|
|
163
|
+
describe "#recent" do
|
|
164
|
+
it "returns the most recently written pairs first, capped at limit" do
|
|
165
|
+
store.set("s", "a", { "n" => 1 })
|
|
166
|
+
store.set("s", "b", { "n" => 2 })
|
|
167
|
+
store.set("s", "c", { "n" => 3 })
|
|
168
|
+
store.set("s", "a", { "n" => 4 }) # rewritten: now the newest
|
|
169
|
+
expect(store.recent("s", nil, 2)).to eq([["a", { "n" => 4 }], ["c", { "n" => 3 }]])
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
it "skips the newest `offset` pairs (the next page)" do
|
|
173
|
+
%w[a b c d e].each_with_index { |k, i| store.set("s", "p:#{k}", i) }
|
|
174
|
+
expect(store.recent("s", "p:", 2, 0).map(&:first)).to eq(%w[p:e p:d])
|
|
175
|
+
expect(store.recent("s", "p:", 2, 2).map(&:first)).to eq(%w[p:c p:b])
|
|
176
|
+
expect(store.recent("s", "p:", 2, 4).map(&:first)).to eq(%w[p:a])
|
|
177
|
+
expect(store.recent("s", nil, 10, 5)).to eq([])
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
it "an amended key keeps its place; an absent one is written" do
|
|
181
|
+
store.set("s", "a", { "n" => 1 })
|
|
182
|
+
store.set("s", "b", { "n" => 2 })
|
|
183
|
+
store.amend("s", "a", { "n" => 3 })
|
|
184
|
+
store.amend("s", "c", { "n" => 4 })
|
|
185
|
+
expect(store.recent("s", nil, 3)).to eq([["c", { "n" => 4 }], ["b", { "n" => 2 }], ["a", { "n" => 3 }]])
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
it "keeps to the scope and the prefix" do
|
|
189
|
+
store.set("s", "x:1", 1)
|
|
190
|
+
store.set("other", "x:2", 2)
|
|
191
|
+
store.set("s", "y:1", 3)
|
|
192
|
+
expect(store.recent("s", "x:", 10)).to eq([["x:1", 1]])
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
131
196
|
describe "#scopes" do
|
|
132
197
|
it " returns scope names sorted lexicographically" do #
|
|
133
198
|
store.set("zeta", "k", 1)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Insika
|
|
6
|
+
# A tool call the model wrote as TEXT instead of through the API's `tool_calls`.
|
|
7
|
+
#
|
|
8
|
+
# deepseek-v4-flash does it now and then: the message comes back with no tool call
|
|
9
|
+
# and a body like
|
|
10
|
+
#
|
|
11
|
+
# <tool_call>
|
|
12
|
+
# search_faq
|
|
13
|
+
# <parameter name="query">quem é a loja Exemplo</parameter>
|
|
14
|
+
# </tool_call>
|
|
15
|
+
#
|
|
16
|
+
# Left alone that is the turn's ANSWER: the tool never runs, the consumer's leak
|
|
17
|
+
# guard swallows the text and the customer gets silence — and the markup stays in
|
|
18
|
+
# the session history, where the model reads itself calling tools that way and does
|
|
19
|
+
# it again (Loja Exemplo, 23/10/2026). The Executor rewrites such a message
|
|
20
|
+
# into a real tool call before the next step, so the loop runs the tool and the
|
|
21
|
+
# history holds what a normal call would have left.
|
|
22
|
+
#
|
|
23
|
+
# Two spellings: the name-then-<parameter> one above (the one seen in production)
|
|
24
|
+
# and the Hermes/Qwen JSON one (`<tool_call>{"name": …, "arguments": {…}}`).
|
|
25
|
+
# Anything else — or a name the chat does not offer — is NOT recovered: inventing a
|
|
26
|
+
# call is worse than the silence the consumer already guards.
|
|
27
|
+
module TextToolCall
|
|
28
|
+
BLOCK = %r{<tool_call>\s*(.*?)\s*</tool_call>}m
|
|
29
|
+
PARAM = %r{<parameter\s+name\s*=\s*"([^"]+)"\s*>(.*?)</parameter>}m
|
|
30
|
+
NAME = /\A([A-Za-z_][\w.-]*)\s*(?:<|\z)/
|
|
31
|
+
|
|
32
|
+
module_function
|
|
33
|
+
|
|
34
|
+
# content: the assistant message text. known: the tool names the chat offers.
|
|
35
|
+
# -> { calls: [{ name:, arguments: }], text: what is left outside the blocks } | nil
|
|
36
|
+
def parse(content, known)
|
|
37
|
+
text = content.to_s
|
|
38
|
+
return nil unless text.include?("<tool_call>")
|
|
39
|
+
|
|
40
|
+
calls = text.scan(BLOCK).map { |(body)| call_from(body) }
|
|
41
|
+
return nil if calls.empty? || calls.any? { |call| call.nil? || !known.include?(call[:name]) }
|
|
42
|
+
|
|
43
|
+
{ calls: calls, text: text.gsub(BLOCK, "").strip }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def call_from(body)
|
|
47
|
+
return json_call(body) if body.start_with?("{")
|
|
48
|
+
|
|
49
|
+
name = body[NAME, 1] or return nil
|
|
50
|
+
# ponytail: parameter values stay strings (the spelling carries no types);
|
|
51
|
+
# every tool seen leaking takes strings. Coerce by schema if one ever doesn't.
|
|
52
|
+
{ name: name, arguments: body.scan(PARAM).to_h { |key, value| [key, value.strip] } }
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def json_call(body)
|
|
56
|
+
data = JSON.parse(body)
|
|
57
|
+
return nil unless data.is_a?(Hash) && data["name"].is_a?(String)
|
|
58
|
+
|
|
59
|
+
arguments = data["arguments"]
|
|
60
|
+
arguments = JSON.parse(arguments) if arguments.is_a?(String)
|
|
61
|
+
{ name: data["name"], arguments: arguments.is_a?(Hash) ? arguments : {} }
|
|
62
|
+
rescue JSON::ParserError
|
|
63
|
+
nil
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
data/lib/insika/tick.rb
CHANGED
|
@@ -34,7 +34,7 @@ module Insika
|
|
|
34
34
|
def initialize(store:, recovery:, channel_delivery:, logger: nil,
|
|
35
35
|
interval: DEFAULT_INTERVAL, stale_after: DEFAULT_STALE_AFTER,
|
|
36
36
|
sleeper: nil, retention: nil, funnel: nil, followup: nil,
|
|
37
|
-
schedule: nil)
|
|
37
|
+
schedule: nil, model_registry: nil)
|
|
38
38
|
@store = store
|
|
39
39
|
@recovery = recovery
|
|
40
40
|
@channel_delivery = channel_delivery
|
|
@@ -46,6 +46,7 @@ module Insika
|
|
|
46
46
|
@funnel = funnel # the tick-driven outcome fold; nil = none
|
|
47
47
|
@followup = followup # the tick-driven follow-up firer; nil = none
|
|
48
48
|
@schedule = schedule # the recurring-schedule firer; nil = none
|
|
49
|
+
@model_registry = model_registry # the daily model-catalog refresh; nil = none
|
|
49
50
|
end
|
|
50
51
|
|
|
51
52
|
# the fold is wired after the Tick is built (the graph passes
|
|
@@ -61,6 +62,9 @@ module Insika
|
|
|
61
62
|
# (same shape — the stores come from the spine).
|
|
62
63
|
attr_accessor :schedule
|
|
63
64
|
|
|
65
|
+
# the daily RubyLLM model-catalog refresh (ModelRegistry), same shape.
|
|
66
|
+
attr_accessor :model_registry
|
|
67
|
+
|
|
64
68
|
def enabled? = @interval.positive?
|
|
65
69
|
|
|
66
70
|
# One pass, pure (no reactor needed): the serving loop calls it on a timer,
|
|
@@ -85,6 +89,8 @@ module Insika
|
|
|
85
89
|
# the same claim-window discipline as the follow-up firer.
|
|
86
90
|
schedule_summary = @schedule&.run
|
|
87
91
|
summary[:schedule] = schedule_summary if schedule_summary
|
|
92
|
+
registry_summary = @model_registry&.run
|
|
93
|
+
summary[:model_registry] = registry_summary if registry_summary
|
|
88
94
|
return summary unless claim_window
|
|
89
95
|
|
|
90
96
|
result = @recovery.run(stale_after: @stale_after)
|
data/lib/insika/token_store.rb
CHANGED
|
@@ -112,6 +112,10 @@ module Insika
|
|
|
112
112
|
# NOTHING valid. -> { revoked: n, issue: Issue }.
|
|
113
113
|
def rotate(tenant_id:, label: "default", now: Time.now)
|
|
114
114
|
@store.transaction do
|
|
115
|
+
# A key no one writes, read first: on a backend that locks per key read,
|
|
116
|
+
# two rotations of one tenant run one after the other, so the second
|
|
117
|
+
# revokes the token the first issued.
|
|
118
|
+
@store.get(SCOPE, "#{ROTATE_PREFIX}#{tenant_id}")
|
|
115
119
|
revoked = revoke_all(tenant_id: tenant_id)
|
|
116
120
|
{ revoked: revoked, issue: issue(tenant_id: tenant_id, label: label) }
|
|
117
121
|
end
|
|
@@ -145,6 +149,7 @@ module Insika
|
|
|
145
149
|
end
|
|
146
150
|
|
|
147
151
|
RECORD_PREFIX = "r:"
|
|
152
|
+
ROTATE_PREFIX = "rotate:" # never written: only read, to serialize rotations
|
|
148
153
|
HASH_PREFIX = "h:"
|
|
149
154
|
|
|
150
155
|
def record_key(id) = "#{RECORD_PREFIX}#{id}"
|
data/lib/insika/tool_assembly.rb
CHANGED
|
@@ -58,14 +58,13 @@ module Insika
|
|
|
58
58
|
# capability alias.
|
|
59
59
|
def assemble_tool_instances(allowed, state)
|
|
60
60
|
names = state.respond_to?(:capability_names) ? (state.capability_names || {}) : {}
|
|
61
|
-
|
|
62
|
-
return instantiate_tools(allowed, ctx) if names.empty?
|
|
61
|
+
return instantiate_tools(allowed, state) if names.empty?
|
|
63
62
|
|
|
64
63
|
# Dedup by the ENTRY NAME (registry key = impl_name) BEFORE
|
|
65
64
|
# instantiating — the INSTANCE's `.name` (RubyLLM) is not the registration
|
|
66
65
|
# name.
|
|
67
66
|
direct = Array(allowed).reject { |e| e.respond_to?(:name) && names.key?(e.name.to_s) }
|
|
68
|
-
instantiate_tools(direct,
|
|
67
|
+
instantiate_tools(direct, state) + capability_tool_instances(names, state)
|
|
69
68
|
end
|
|
70
69
|
|
|
71
70
|
# Envelopes each allowed tool (per-call timeout + side-effect recording).
|
|
@@ -78,7 +77,7 @@ module Insika
|
|
|
78
77
|
ToolEnvelope.new(tool, state: state, checkpoint_store: @checkpoint_store,
|
|
79
78
|
tool_registry: @tool_registry, timeout: timeout,
|
|
80
79
|
skip_side_effects: skip_side_effects,
|
|
81
|
-
trace_recorder: @tool_trace_store)
|
|
80
|
+
trace_recorder: @tool_trace_store, event_stream: @event_stream)
|
|
82
81
|
end
|
|
83
82
|
end
|
|
84
83
|
|
|
@@ -99,16 +98,19 @@ module Insika
|
|
|
99
98
|
return unless state.respond_to?(:tool_gate) && state.respond_to?(:tool_concurrency)
|
|
100
99
|
|
|
101
100
|
cap = state.tool_concurrency
|
|
102
|
-
state.tool_gate = cap ? Async::Semaphore.new(cap) : nil
|
|
101
|
+
state.tool_gate = cap && cap > 1 ? Async::Semaphore.new(cap) : nil
|
|
102
|
+
if state.respond_to?(:side_effect_gate=)
|
|
103
|
+
state.side_effect_gate = state.tool_gate ? Async::Semaphore.new(1) : nil
|
|
104
|
+
end
|
|
103
105
|
end
|
|
104
106
|
|
|
105
107
|
# Real Engine -> Entries (respond to factory); fakes -> ready instances.
|
|
106
108
|
# `turn_context` is deposited into the instances that expose it
|
|
107
109
|
# (data-tools); the rest ignore it (parity).
|
|
108
|
-
def instantiate_tools(allowed,
|
|
110
|
+
def instantiate_tools(allowed, state = nil)
|
|
109
111
|
Array(allowed).map do |t|
|
|
110
112
|
tool = t.respond_to?(:factory) ? t.factory.call : t
|
|
111
|
-
inject_turn_context(tool,
|
|
113
|
+
inject_turn_context(tool, state)
|
|
112
114
|
tool
|
|
113
115
|
end
|
|
114
116
|
end
|
|
@@ -116,22 +118,28 @@ module Insika
|
|
|
116
118
|
# seam: deposits the turn context into the freshly created instance
|
|
117
119
|
# (same idea as `remember`, which receives tenant/state) BEFORE the
|
|
118
120
|
# ToolEnvelope. Duck-typed: only what exposes `turn_context=` (DataDefinedTool)
|
|
119
|
-
# receives it
|
|
120
|
-
|
|
121
|
-
|
|
121
|
+
# receives it; a tool that exposes `turn_state=` (a presentation tool, which
|
|
122
|
+
# reads the ledger and the hoarded cards) receives the state itself. nil (a
|
|
123
|
+
# state with no turn_context, e.g. a test stub) -> no-op.
|
|
124
|
+
def inject_turn_context(tool, state)
|
|
125
|
+
return if state.nil?
|
|
126
|
+
|
|
127
|
+
tool.turn_state = state if tool.respond_to?(:turn_state=)
|
|
128
|
+
ctx = state.respond_to?(:turn_context) ? state.turn_context : nil
|
|
129
|
+
return if ctx.nil?
|
|
122
130
|
|
|
123
|
-
tool.turn_context =
|
|
131
|
+
tool.turn_context = ctx if tool.respond_to?(:turn_context=)
|
|
124
132
|
end
|
|
125
133
|
|
|
126
134
|
# impl_name -> Capability::ResolvedTool(capability_name:), STILL without
|
|
127
135
|
# ToolEnvelope (the call site's wrap_tools wraps the whole set — same
|
|
128
136
|
# order impl -> ResolvedTool -> ToolEnvelope). entry already validated in
|
|
129
137
|
# resolve_capabilities.
|
|
130
|
-
def capability_tool_instances(names,
|
|
138
|
+
def capability_tool_instances(names, state = nil)
|
|
131
139
|
names.map do |impl_name, capability_name|
|
|
132
140
|
entry = @tool_registry.entries.find { |e| e.name == impl_name }
|
|
133
141
|
tool = entry.factory.call
|
|
134
|
-
inject_turn_context(tool,
|
|
142
|
+
inject_turn_context(tool, state)
|
|
135
143
|
Capability::ResolvedTool.new(tool, capability_name: capability_name,
|
|
136
144
|
impl_name: impl_name)
|
|
137
145
|
end
|
data/lib/insika/tool_batch.rb
CHANGED
|
@@ -39,7 +39,7 @@ module Insika
|
|
|
39
39
|
# From after_tool_result, with the RAW result: a Tool::Halt is only
|
|
40
40
|
# recognizable there.
|
|
41
41
|
def halt!(result)
|
|
42
|
-
@halted = true if defined?(
|
|
42
|
+
@halted = true if defined?(Insika::ToolDefinition::Halt) && result.is_a?(Insika::ToolDefinition::Halt)
|
|
43
43
|
end
|
|
44
44
|
|
|
45
45
|
def halted? = @halted
|
data/lib/insika/tool_catalog.rb
CHANGED
|
@@ -14,13 +14,20 @@ module Insika
|
|
|
14
14
|
# no instantiation at all; a broken factory surfaces on first use (where the
|
|
15
15
|
# Executor would also catch it at stage 3), not at construction.
|
|
16
16
|
class ToolCatalog
|
|
17
|
-
Entry = Data.define(:name, :description)
|
|
17
|
+
Entry = Data.define(:name, :description, :plugin) do
|
|
18
|
+
def initialize(name:, description:, plugin: nil)
|
|
19
|
+
super
|
|
20
|
+
end
|
|
21
|
+
end
|
|
18
22
|
|
|
19
23
|
def initialize(tool_registry:)
|
|
20
24
|
@tool_registry = tool_registry
|
|
21
25
|
end
|
|
22
26
|
|
|
23
27
|
def all
|
|
28
|
+
gen = registry_generation
|
|
29
|
+
@entries = nil if gen != @generation
|
|
30
|
+
@generation = gen
|
|
24
31
|
@entries ||= build_entries
|
|
25
32
|
end
|
|
26
33
|
|
|
@@ -29,6 +36,7 @@ module Insika
|
|
|
29
36
|
# restart. An in-flight turn has already captured `all`.
|
|
30
37
|
def reload
|
|
31
38
|
@entries = build_entries
|
|
39
|
+
@generation = registry_generation
|
|
32
40
|
self
|
|
33
41
|
end
|
|
34
42
|
|
|
@@ -76,7 +84,7 @@ module Insika
|
|
|
76
84
|
|
|
77
85
|
def build_entries
|
|
78
86
|
@tool_registry.entries.map do |entry|
|
|
79
|
-
Entry.new(name: entry.name, description: entry.factory.call.description.to_s)
|
|
87
|
+
Entry.new(name: entry.name, description: entry.factory.call.description.to_s, plugin: entry.plugin)
|
|
80
88
|
end
|
|
81
89
|
end
|
|
82
90
|
|
|
@@ -85,5 +93,7 @@ module Insika
|
|
|
85
93
|
desc = entry.description.downcase
|
|
86
94
|
terms.sum { |term| (name.include?(term) ? 2 : 0) + (desc.include?(term) ? 1 : 0) }
|
|
87
95
|
end
|
|
96
|
+
|
|
97
|
+
def registry_generation = @tool_registry.respond_to?(:generation) ? @tool_registry.generation : nil
|
|
88
98
|
end
|
|
89
99
|
end
|