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
|
@@ -72,7 +72,7 @@ module Insika
|
|
|
72
72
|
stats = tool_stats(sessions[id] || [], cutoff)
|
|
73
73
|
never_called_rows(id, record, stats) +
|
|
74
74
|
error_rate_rows(id, stats, days) +
|
|
75
|
-
stale_rows(id, stats)
|
|
75
|
+
stale_rows(id, stats) + blocked_rows(id, stats, days)
|
|
76
76
|
end
|
|
77
77
|
|
|
78
78
|
Report.new(generated_at: now.iso8601, days: days,
|
|
@@ -87,8 +87,8 @@ module Insika
|
|
|
87
87
|
# workflows without a chat) contributes nothing.
|
|
88
88
|
def sessions_by_agent
|
|
89
89
|
acc = Hash.new { |h, k| h[k] = [] }
|
|
90
|
-
@task_store.each_id
|
|
91
|
-
|
|
90
|
+
tasks = @task_store.respond_to?(:all) ? @task_store.all : @task_store.each_id.filter_map { |id| @task_store.find(id) }
|
|
91
|
+
tasks.each do |task|
|
|
92
92
|
agent = task.command.is_a?(Hash) ? task.command.dig("payload", "agent") : nil
|
|
93
93
|
next if agent.to_s.empty? || task.session_id.to_s.empty?
|
|
94
94
|
|
|
@@ -100,7 +100,7 @@ module Insika
|
|
|
100
100
|
# tool name -> { calls:, errors:, window_calls:, window_errors:, last_at: }
|
|
101
101
|
# over every stored trace entry of the agent's sessions.
|
|
102
102
|
def tool_stats(session_ids, cutoff)
|
|
103
|
-
stats = Hash.new { |h, k| h[k] = { calls: 0, errors: 0, window_calls: 0, window_errors: 0, last_at: nil } }
|
|
103
|
+
stats = Hash.new { |h, k| h[k] = { calls: 0, errors: 0, window_calls: 0, window_errors: 0, blocked: Hash.new(0), last_at: nil } }
|
|
104
104
|
session_ids.each do |sid|
|
|
105
105
|
@tool_trace_store.for_session(sid).each do |entry|
|
|
106
106
|
s = stats[entry["tool"].to_s]
|
|
@@ -113,6 +113,7 @@ module Insika
|
|
|
113
113
|
|
|
114
114
|
s[:window_calls] += 1
|
|
115
115
|
s[:window_errors] += 1 if error
|
|
116
|
+
s[:blocked][entry["gate"]] += 1 if entry["gate"]
|
|
116
117
|
end
|
|
117
118
|
end
|
|
118
119
|
stats
|
|
@@ -142,6 +143,15 @@ module Insika
|
|
|
142
143
|
end
|
|
143
144
|
end
|
|
144
145
|
|
|
146
|
+
def blocked_rows(agent, stats, days)
|
|
147
|
+
stats.flat_map do |tool, s|
|
|
148
|
+
s[:blocked].map do |gate, count|
|
|
149
|
+
Row.new(agent: agent, tool: tool, kind: "blocked",
|
|
150
|
+
detail: "#{count} call(s) blocked by #{gate} in the last #{days} day(s)")
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
145
155
|
def stale_rows(agent, stats)
|
|
146
156
|
stats.filter_map do |tool, s|
|
|
147
157
|
next if s[:window_calls].positive? || s[:last_at].nil?
|
|
@@ -9,7 +9,7 @@ module Insika
|
|
|
9
9
|
# enter lib/insika.rb; it is loaded in the registration block (wiring) on the 1st
|
|
10
10
|
# instance. One instance per remote agent (its own name/description).
|
|
11
11
|
class A2ARemote < RubyLLM::Tool
|
|
12
|
-
|
|
12
|
+
parameter :message, description: "The message/task for the remote agent"
|
|
13
13
|
|
|
14
14
|
def initialize(client:, url:, tool_name:, description:, event_stream: nil)
|
|
15
15
|
@client = client
|
|
@@ -11,7 +11,7 @@ module Insika
|
|
|
11
11
|
# the provider sees.
|
|
12
12
|
#
|
|
13
13
|
# Tolerant by construction: JSON-schema hashes reach us with symbol keys (the
|
|
14
|
-
# class-level `
|
|
14
|
+
# class-level `parameters(...)` form) or string keys (anything that round-tripped
|
|
15
15
|
# through JSON), and a shape we do not recognize is returned UNTOUCHED — a
|
|
16
16
|
# schema we failed to annotate still works, one we corrupted would break every
|
|
17
17
|
# delegation.
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ruby_llm"
|
|
4
|
+
|
|
5
|
+
module Insika
|
|
6
|
+
module Tools
|
|
7
|
+
# What the two confirmation tools share: finding the customer's hold and
|
|
8
|
+
# refusing anyone else's — another conversation's, the operator's, or one
|
|
9
|
+
# already decided. Errors go back to the MODEL as bodies, never as
|
|
10
|
+
# exceptions: a tool exception never ends the turn.
|
|
11
|
+
module Confirmation
|
|
12
|
+
CUSTOMER = PendingActionStore::CUSTOMER
|
|
13
|
+
|
|
14
|
+
module_function
|
|
15
|
+
|
|
16
|
+
# -> [PendingAction, nil] | [nil, String]
|
|
17
|
+
def open_hold(state, pending_id)
|
|
18
|
+
store = state.pending_action_store
|
|
19
|
+
pending = store&.find(pending_id.to_s)
|
|
20
|
+
return [nil, "no held action with pending_id #{pending_id}"] if pending.nil?
|
|
21
|
+
return [nil, "#{pending_id} is not a customer confirmation"] unless pending.kind == CUSTOMER
|
|
22
|
+
return [nil, "#{pending_id} belongs to another conversation"] unless pending.session_id.to_s == state.task.session_id.to_s
|
|
23
|
+
# The hold and the answer to it are two customer messages, which is two
|
|
24
|
+
# TASKS. A hold THIS task created has not been put to the customer yet —
|
|
25
|
+
# the reply carrying it has not even been sent. Deciding it here would be
|
|
26
|
+
# the model confirming itself, and the write would land with nobody's word
|
|
27
|
+
# behind it.
|
|
28
|
+
return [nil, "#{pending_id} was held on this same turn; the customer has not answered it yet"] if pending.task_id.to_s == state.task.id.to_s
|
|
29
|
+
return [nil, "#{pending_id} was already #{pending.status}"] unless pending.status == :pending
|
|
30
|
+
|
|
31
|
+
[pending, nil]
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# The turn's enveloped instance of the held tool — the same wrapper the
|
|
35
|
+
# hold came from, so the confirmed run pays every check but the hold.
|
|
36
|
+
def held_tool(state, name)
|
|
37
|
+
Array(state.allowed_tools).find do |tool|
|
|
38
|
+
tool.respond_to?(:call_confirmed) &&
|
|
39
|
+
(tool.name.to_s == name || (tool.respond_to?(:impl_name) && tool.impl_name.to_s == name))
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def emit(event_stream, state, type, pending)
|
|
44
|
+
event_stream&.emit(Insika::Event.new(
|
|
45
|
+
type: type,
|
|
46
|
+
data: { pending_id: pending.id, tool: pending.tool },
|
|
47
|
+
meta: { task_id: state.task.id, session_id: state.task.session_id }
|
|
48
|
+
))
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Runs an action the engine held for the customer's confirmation. The
|
|
53
|
+
# decision is written FIRST (durable — a crash between the two re-executes
|
|
54
|
+
# the turn and finds it approved), the original tool runs SECOND through its
|
|
55
|
+
# own envelope with the arguments recorded at the hold: there is no path to
|
|
56
|
+
# confirm different arguments than the ones the customer was shown.
|
|
57
|
+
# System builtin (like remember): `require "ruby_llm"` stays in THIS file,
|
|
58
|
+
# loaded lazily by the Executor in create_chat.
|
|
59
|
+
class ConfirmPending < RubyLLM::Tool
|
|
60
|
+
description "Runs an action the customer has just confirmed. Call it only when the customer's " \
|
|
61
|
+
"message agrees to exactly what was proposed; a change, a question or a new request " \
|
|
62
|
+
"is cancel_pending instead."
|
|
63
|
+
parameter :pending_id, description: "The pending_id the held action returned"
|
|
64
|
+
|
|
65
|
+
def name = "confirm_pending"
|
|
66
|
+
|
|
67
|
+
def initialize(event_stream:, state:)
|
|
68
|
+
@event_stream = event_stream
|
|
69
|
+
@state = state
|
|
70
|
+
super()
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def execute(pending_id:)
|
|
74
|
+
pending, error = Confirmation.open_hold(@state, pending_id)
|
|
75
|
+
return { error: error } if error
|
|
76
|
+
|
|
77
|
+
tool = Confirmation.held_tool(@state, pending.tool)
|
|
78
|
+
return { error: "#{pending.tool} is not available on this turn" } unless tool
|
|
79
|
+
|
|
80
|
+
@state.pending_action_store.resolve(pending.id, decision: :approved, operator: Confirmation::CUSTOMER)
|
|
81
|
+
Confirmation.emit(@event_stream, @state, :confirmation_confirmed, pending)
|
|
82
|
+
tool.call_confirmed(pending.args || {})
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Drops a held action: the customer declined, changed something, or asked
|
|
87
|
+
# for anything else. The hold would expire on its own at the end of this
|
|
88
|
+
# turn; cancelling says so now, and lets the model answer accordingly.
|
|
89
|
+
class CancelPending < RubyLLM::Tool
|
|
90
|
+
description "Drops an action that was waiting for the customer's confirmation, because they " \
|
|
91
|
+
"declined, changed something, or asked for something else."
|
|
92
|
+
parameter :pending_id, description: "The pending_id the held action returned"
|
|
93
|
+
|
|
94
|
+
def name = "cancel_pending"
|
|
95
|
+
|
|
96
|
+
def initialize(event_stream:, state:)
|
|
97
|
+
@event_stream = event_stream
|
|
98
|
+
@state = state
|
|
99
|
+
super()
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def execute(pending_id:)
|
|
103
|
+
pending, error = Confirmation.open_hold(@state, pending_id)
|
|
104
|
+
return { error: error } if error
|
|
105
|
+
|
|
106
|
+
@state.pending_action_store.resolve(pending.id, decision: :rejected, operator: Confirmation::CUSTOMER)
|
|
107
|
+
Confirmation.emit(@event_stream, @state, :confirmation_cancelled, pending)
|
|
108
|
+
{ "status" => "cancelled", "tool" => pending.tool }
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
@@ -12,7 +12,7 @@ module Insika
|
|
|
12
12
|
# it is NOT required in lib/insika.rb; the overlay loads it lazily at registration
|
|
13
13
|
#
|
|
14
14
|
# Contract preserved by duck-typing: it overrides name/description/parameters/
|
|
15
|
-
# execute; RubyLLM's
|
|
15
|
+
# execute; RubyLLM's parameters_schema derives from #parameters automatically.
|
|
16
16
|
# execute NEVER raises — an error (missing param, blocked egress, HTTP, parse)
|
|
17
17
|
# becomes `{ error: }` to the model, like the other tools.
|
|
18
18
|
class DataDefinedTool < RubyLLM::Tool
|
|
@@ -47,21 +47,13 @@ module Insika
|
|
|
47
47
|
# envelope's duck-typed resolution checks this FIRST — a data-tool declares
|
|
48
48
|
# its evidence on its definition, never in the registry metadata.
|
|
49
49
|
def evidence = @definition.evidence
|
|
50
|
+
def requires_evidence = @definition.requires_evidence
|
|
50
51
|
|
|
51
|
-
# FULL (nested) JSON Schema straight into RubyLLM's
|
|
52
|
+
# FULL (nested) JSON Schema straight into RubyLLM's parameters_schema — it is what
|
|
52
53
|
# the providers serialize (OpenAI/Anthropic/Gemini/Bedrock prefer
|
|
53
|
-
#
|
|
54
|
+
# parameters_schema; parameters is just a fallback). Provider-agnostic and
|
|
54
55
|
# the only form that expresses nesting (object/array/enum).,.
|
|
55
|
-
def
|
|
56
|
-
|
|
57
|
-
# FLAT top-level view for discovery (tool_search calls #parameters on the resolved
|
|
58
|
-
# tool). The real nested schema goes through #params_schema above.
|
|
59
|
-
def parameters
|
|
60
|
-
@parameters ||= @definition.top_level_params.each_with_object({}) do |p, acc|
|
|
61
|
-
sym = p[:name].to_sym
|
|
62
|
-
acc[sym] = RubyLLM::Parameter.new(sym, type: p[:type], desc: p[:description], required: p[:required])
|
|
63
|
-
end
|
|
64
|
-
end
|
|
56
|
+
def parameters_schema = @definition.parameters
|
|
65
57
|
|
|
66
58
|
# The args are checked against the tool's own JSON Schema BEFORE the request is
|
|
67
59
|
# built: a call the schema does not allow becomes `{ error: }` the model can act
|
|
@@ -71,16 +63,19 @@ module Insika
|
|
|
71
63
|
return { error: bad }
|
|
72
64
|
end
|
|
73
65
|
|
|
66
|
+
t0 = monotonic
|
|
74
67
|
req = build_request(kwargs)
|
|
75
68
|
reason = @egress.violation(req[:url], **@egress_options)
|
|
76
69
|
return { error: "destination blocked: #{reason}" } if reason
|
|
77
70
|
|
|
71
|
+
t1 = monotonic
|
|
78
72
|
result = @http.request(**req)
|
|
73
|
+
record_timing(t0, t1, result)
|
|
79
74
|
emit(result[:status])
|
|
80
75
|
payload = extract(result)
|
|
81
76
|
# The RESPONSE says the turn is over (`halt_when`): the backend already
|
|
82
77
|
# answered the customer, so letting the model comment would deliver the
|
|
83
|
-
# message twice.
|
|
78
|
+
# message twice. Insika::ToolDefinition::Halt ends its loop right here — no second
|
|
84
79
|
# provider call, and the decision is the engine's, not a request in a prompt.
|
|
85
80
|
# Only on a 2xx: an error body that happens to carry the value is a failure,
|
|
86
81
|
# and a failure must reach the model.
|
|
@@ -89,7 +84,7 @@ module Insika
|
|
|
89
84
|
# the model wrote no lead-in. Wrapped only when there is one, so every tool
|
|
90
85
|
# that declares no `say` keeps producing exactly the payload it always did.
|
|
91
86
|
say = @definition.halt_say(result[:body])
|
|
92
|
-
return
|
|
87
|
+
return Insika::ToolDefinition::Halt.new(say ? Insika::ToolDefinition.wrap_halt(payload, say) : payload)
|
|
93
88
|
end
|
|
94
89
|
|
|
95
90
|
payload
|
|
@@ -219,6 +214,20 @@ module Insika
|
|
|
219
214
|
|
|
220
215
|
# No task correlation (registry tool does not receive TurnState) -> meta {}.
|
|
221
216
|
# Emits only name + status: NEVER body/headers (0 secret leakage, R2).
|
|
217
|
+
def monotonic = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
218
|
+
|
|
219
|
+
# Where the call's time went, for the ToolEnvelope's trace (same fiber): the
|
|
220
|
+
# request build + egress check, the HTTP round trip as this process saw it, and
|
|
221
|
+
# the server's own runtime when it reports one. A wide gap between http_ms and
|
|
222
|
+
# server_ms is network or this process being slow to resume the call.
|
|
223
|
+
def record_timing(t0, t1, result)
|
|
224
|
+
Thread.current[Insika::ToolEnvelope::TIMING_KEY] = {
|
|
225
|
+
"pre_ms" => ((t1 - t0) * 1000).round,
|
|
226
|
+
"http_ms" => ((monotonic - t1) * 1000).round,
|
|
227
|
+
"server_ms" => result[:server_ms]
|
|
228
|
+
}.compact
|
|
229
|
+
end
|
|
230
|
+
|
|
222
231
|
def emit(status)
|
|
223
232
|
@event_stream&.emit(Insika::Event.new(
|
|
224
233
|
type: :data_tool_call, data: { tool: @definition.name, status: status }, meta: {}
|
|
@@ -27,9 +27,9 @@ module Insika
|
|
|
27
27
|
"prompt alone — UNLESS this turn carries an inbound photo, in which case " \
|
|
28
28
|
"that photo is edited by default (pass source_image_urls explicitly to " \
|
|
29
29
|
"generate from scratch instead)."
|
|
30
|
-
# explicit JSON-schema form (the `
|
|
30
|
+
# explicit JSON-schema form (the `parameter` DSL only reaches strings/scalars,
|
|
31
31
|
# and source_image_urls needs a typed array — the bare-array gotcha, #128).
|
|
32
|
-
|
|
32
|
+
parameters(
|
|
33
33
|
type: "object",
|
|
34
34
|
properties: {
|
|
35
35
|
prompt: { type: "string", description: "What to draw, or what edit to make, in detail" },
|
|
@@ -16,7 +16,7 @@ module Insika
|
|
|
16
16
|
# loads it lazily inside create_chat, same as load_skill.
|
|
17
17
|
class LoadKnowledge < RubyLLM::Tool
|
|
18
18
|
description "Loads the complete content of a learned concept by name"
|
|
19
|
-
|
|
19
|
+
parameter :name, description: "Exact concept name, as listed in <knowledge>"
|
|
20
20
|
|
|
21
21
|
# RubyLLM::Tool#name derives from self.class.name — for a nested class it
|
|
22
22
|
# produces "insika--tools--load_knowledge", not "load_knowledge" (which
|
|
@@ -47,7 +47,11 @@ module Insika
|
|
|
47
47
|
private
|
|
48
48
|
|
|
49
49
|
def load(name)
|
|
50
|
-
content = @
|
|
50
|
+
content = if @state&.respond_to?(:shared_conversations) && @state.shared_conversations
|
|
51
|
+
@state.shared_conversations.memory_get(task:@state.task,id:name.to_s,kind:'knowledge')&.fetch('value')
|
|
52
|
+
else
|
|
53
|
+
@store.get(@agent_id, name.to_s, tenant: @tenant)
|
|
54
|
+
end
|
|
51
55
|
return { error: "concept '#{name}' not found" } unless content
|
|
52
56
|
|
|
53
57
|
content
|
|
@@ -61,7 +65,7 @@ module Insika
|
|
|
61
65
|
|
|
62
66
|
@trace_recorder.record(
|
|
63
67
|
session_id: @state.task.session_id,
|
|
64
|
-
entry: { "turn" => @state.turn, "tool" => "load_knowledge", "call_id" => "",
|
|
68
|
+
entry: { "task_id" => @state.task.id.to_s, "turn" => @state.turn, "tool" => "load_knowledge", "call_id" => "",
|
|
65
69
|
"args" => { "name" => name.to_s }, "result" => result,
|
|
66
70
|
"ms" => ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) * 1000).round,
|
|
67
71
|
"at" => Time.now.utc.iso8601 }
|
|
@@ -14,13 +14,13 @@ module Insika
|
|
|
14
14
|
# loads it lazily inside create_chat.
|
|
15
15
|
class LoadSkill < RubyLLM::Tool
|
|
16
16
|
description "Loads the complete instructions (SKILL.md) of a skill by name"
|
|
17
|
-
|
|
17
|
+
parameter :name, description: "Exact skill name, as listed in <available_skills>"
|
|
18
18
|
|
|
19
19
|
# RubyLLM::Tool#name derives from self.class.name — for a nested class it produces
|
|
20
20
|
# "insika--tools--load_skill", not "load_skill" (which wire_callbacks/
|
|
21
21
|
# :skill_activated and SkillCatalog#format_for_prompt assume). Explicit
|
|
22
22
|
# override. Coexists with
|
|
23
|
-
# `
|
|
23
|
+
# `parameter :name` (verified: the param is still present).
|
|
24
24
|
def name = "load_skill"
|
|
25
25
|
|
|
26
26
|
# trace_recorder/state are OPTIONAL (nil = no trace, parity): this tool is
|
|
@@ -86,7 +86,7 @@ module Insika
|
|
|
86
86
|
|
|
87
87
|
@trace_recorder.record(
|
|
88
88
|
session_id: @state.task.session_id,
|
|
89
|
-
entry: { "turn" => @state.turn, "tool" => "load_skill", "call_id" => "",
|
|
89
|
+
entry: { "task_id" => @state.task.id.to_s, "turn" => @state.turn, "tool" => "load_skill", "call_id" => "",
|
|
90
90
|
"args" => { "name" => name.to_s }, "result" => result,
|
|
91
91
|
"ms" => ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) * 1000).round,
|
|
92
92
|
"at" => Time.now.utc.iso8601 }
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ruby_llm"
|
|
4
|
+
|
|
5
|
+
module Insika
|
|
6
|
+
module Tools
|
|
7
|
+
# PRESENTATION tool: the model says WHICH products to show, the engine decides
|
|
8
|
+
# WHAT is shown. Built by the overlay registry from a ToolDefinition that
|
|
9
|
+
# declares `presentation` (one class, N instances — like DataDefinedTool).
|
|
10
|
+
#
|
|
11
|
+
# Cards used to be a side effect of evidence: every attachment a search tool
|
|
12
|
+
# returned rode the channel delivery, all of them, and the model had no way to
|
|
13
|
+
# say "these three, not the ten I searched". The deployments compensated with
|
|
14
|
+
# inline markers in the text, parsed by regex on their side — where most of
|
|
15
|
+
# the hallucinated ids came from. Here text never carries product data: the
|
|
16
|
+
# model picks ids, and the engine
|
|
17
|
+
# 1. keeps only ids the session's evidence ledger has seen (provenance),
|
|
18
|
+
# 2. joins each to the card the evidence tool already hoarded this turn,
|
|
19
|
+
# 3. caps at the declared `max`,
|
|
20
|
+
# 4. records the selection on the turn (the delivery sends THAT),
|
|
21
|
+
# 5. emits `:ui` on the stream (the edge publishes it as `insika.ui`),
|
|
22
|
+
# 6. tells the model what was shown and what was dropped, and why.
|
|
23
|
+
# No HTTP, no model call, deterministic. `execute` never raises.
|
|
24
|
+
class Present < RubyLLM::Tool
|
|
25
|
+
# The one sentence for the model when NOTHING could be shown. Engine-generic:
|
|
26
|
+
# the pack tunes vocabulary through the tool's `description`.
|
|
27
|
+
NOTHING_SHOWN = "no card could be shown; name the products in text or search again"
|
|
28
|
+
|
|
29
|
+
def initialize(definition:, event_stream: nil)
|
|
30
|
+
@definition = definition
|
|
31
|
+
@event_stream = event_stream
|
|
32
|
+
@state = nil
|
|
33
|
+
super()
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Deposited by ToolAssembly before the envelope wraps the instance — the
|
|
37
|
+
# ledger and the hoarded cards live on the turn, never on the registry.
|
|
38
|
+
def turn_state=(state)
|
|
39
|
+
@state = state
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def name = @definition.name
|
|
43
|
+
def description = @definition.description
|
|
44
|
+
def parameters_schema = @definition.parameters
|
|
45
|
+
|
|
46
|
+
def execute(**kwargs)
|
|
47
|
+
spec = @definition.presentation
|
|
48
|
+
ids = Array(kwargs[spec[:ids].to_sym]).map(&:to_s).reject(&:empty?).uniq
|
|
49
|
+
title = Coercion.presence(kwargs[:title])
|
|
50
|
+
|
|
51
|
+
shown, dropped = select(ids, spec[:max])
|
|
52
|
+
record(spec[:component], title, shown)
|
|
53
|
+
emit(spec[:component], title, shown, dropped)
|
|
54
|
+
|
|
55
|
+
out = { "shown" => shown.map { |c| c["id"] }, "dropped" => dropped }
|
|
56
|
+
out["instruction"] = NOTHING_SHOWN if shown.empty?
|
|
57
|
+
out
|
|
58
|
+
rescue StandardError => e
|
|
59
|
+
{ error: "presentation failed: #{e.message}" }
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
# -> [shown cards, dropped [{id, reason}]]. Order preserved. Reasons:
|
|
65
|
+
# unknown — the ledger never saw the id (the model made it up, or the
|
|
66
|
+
# customer typed it); no ledger on the state reads as unknown too —
|
|
67
|
+
# a check that cannot verify does not pass.
|
|
68
|
+
# no_card — the id is known but no evidence tool returned a card for it
|
|
69
|
+
# this session (a text-only search result, or a seed of ids alone).
|
|
70
|
+
# max — beyond the declared cap.
|
|
71
|
+
# Cards come from this turn first, then from the ledger (the last few
|
|
72
|
+
# searches of the session) — "show me the second one" a turn later works.
|
|
73
|
+
def select(ids, max)
|
|
74
|
+
ledger = @state.respond_to?(:evidence_ledger) ? @state.evidence_ledger : nil
|
|
75
|
+
known = Array(ledger&.ids)
|
|
76
|
+
turn_cards = @state.respond_to?(:evidence_attachments) ? Array(@state.evidence_attachments) : []
|
|
77
|
+
session_cards = ledger.respond_to?(:cards) ? Array(ledger.cards) : []
|
|
78
|
+
by_id = (turn_cards + session_cards).each_with_object({}) { |c, h| h[c["id"]] ||= c if c["id"] }
|
|
79
|
+
|
|
80
|
+
shown = []
|
|
81
|
+
dropped = []
|
|
82
|
+
ids.each do |id|
|
|
83
|
+
if !known.include?(id) then dropped << { "id" => id, "reason" => "unknown" }
|
|
84
|
+
elsif (card = by_id[id]).nil? then dropped << { "id" => id, "reason" => "no_card" }
|
|
85
|
+
elsif shown.size >= max then dropped << { "id" => id, "reason" => "max" }
|
|
86
|
+
else shown << card
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
[shown, dropped]
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def record(component, title, shown)
|
|
93
|
+
return unless @state.respond_to?(:presentations)
|
|
94
|
+
|
|
95
|
+
@state.presentations ||= []
|
|
96
|
+
@state.presentations << { "component" => component, "title" => title,
|
|
97
|
+
"items" => shown.map { |c| c.slice("id", "type", "url", "caption") } }
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def emit(component, title, shown, dropped)
|
|
101
|
+
return unless @event_stream
|
|
102
|
+
|
|
103
|
+
task = @state.respond_to?(:task) ? @state.task : nil
|
|
104
|
+
meta = task ? { task_id: task.id, session_id: task.session_id } : {}
|
|
105
|
+
@event_stream.emit(Insika::Event.new(
|
|
106
|
+
type: :ui,
|
|
107
|
+
data: { component: component, title: title,
|
|
108
|
+
items: shown.map { |c| c.slice("id", "type", "url", "caption") },
|
|
109
|
+
count: shown.size, dropped: dropped },
|
|
110
|
+
meta: meta
|
|
111
|
+
))
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
@@ -13,8 +13,8 @@ module Insika
|
|
|
13
13
|
description "Stores information to remember in future conversations. Use `key` " \
|
|
14
14
|
"for a durable key-value fact (overwrites the previous one); omit " \
|
|
15
15
|
"`key` for a free-form note."
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
parameter :value, description: "The content to remember"
|
|
17
|
+
parameter :key, description: "Fact key (e.g.: 'plan', 'name'); omit for a note", required: false
|
|
18
18
|
|
|
19
19
|
# otherwise RubyLLM derives "insika--tools--remember" from the class name.
|
|
20
20
|
def name = "remember"
|
|
@@ -28,6 +28,11 @@ module Insika
|
|
|
28
28
|
end
|
|
29
29
|
|
|
30
30
|
def execute(value:, key: nil)
|
|
31
|
+
if @state.shared_conversations
|
|
32
|
+
kind = key.to_s.strip.empty? ? 'note' : 'fact'
|
|
33
|
+
record = @state.shared_conversations.propose_memory(task:@state.task,id:kind == 'note' ? SecureRandom.uuid : key.to_s,value:value.to_s,kind:kind)
|
|
34
|
+
return {proposed:kind,id:record['id'],revision:record['revision']}
|
|
35
|
+
end
|
|
31
36
|
if key.to_s.strip.empty?
|
|
32
37
|
note = @store.add_note(tenant: @tenant, text: value.to_s)
|
|
33
38
|
emit("note", note.id)
|
|
@@ -45,7 +45,7 @@ module Insika
|
|
|
45
45
|
"its target agent and score the whole conversation with the configured " \
|
|
46
46
|
"judge panel. Refuses if the target exposes a tool that could write for " \
|
|
47
47
|
"real."
|
|
48
|
-
|
|
48
|
+
parameter :case_id, description: "Id of the persona case to run"
|
|
49
49
|
|
|
50
50
|
def name = "run_persona_eval"
|
|
51
51
|
|
|
@@ -111,7 +111,7 @@ module Insika
|
|
|
111
111
|
"#{super} Cases you may run: #{ids.join(', ')}."
|
|
112
112
|
end
|
|
113
113
|
|
|
114
|
-
def
|
|
114
|
+
def parameters_schema
|
|
115
115
|
Insika::Tools::AgentEnum.inject(super, case_ids, path: %i[case_id])
|
|
116
116
|
end
|
|
117
117
|
|
|
@@ -161,7 +161,7 @@ module Insika
|
|
|
161
161
|
simulator = Insika::Evals::Simulator.new(transport: transport, ask: persona_ask, safety: safety)
|
|
162
162
|
|
|
163
163
|
conv = "eval-#{golden.id}-#{SecureRandom.hex(4)}" # a fresh session every run (never reused)
|
|
164
|
-
run = simulator.run(persona: golden.persona, agent: golden.agent, conv: conv)
|
|
164
|
+
run = simulator.run(persona: golden.persona, agent: golden.agent, conv: conv, state: golden.state)
|
|
165
165
|
verdict = judge.score_conversation(
|
|
166
166
|
rubric: golden.rubric, transcript: run.transcript, policy: golden.policy,
|
|
167
167
|
min_score: golden.min_score || Insika::Evals::Judge::DEFAULT_MIN_SCORE
|
|
@@ -199,6 +199,11 @@ module Insika
|
|
|
199
199
|
llm: llm_context
|
|
200
200
|
)
|
|
201
201
|
bus = Insika::CommandBus.new
|
|
202
|
+
# a golden with `state:` seeds through the same bus the turn runs on
|
|
203
|
+
bus.register(:seed_session, Insika::Commands::SeedSession.new(
|
|
204
|
+
session_store: @graph.session_store, memory_store: @graph.memory_store,
|
|
205
|
+
event_stream: @graph.event_stream
|
|
206
|
+
))
|
|
202
207
|
bus.register(:send_message, Insika::Commands::SendMessage.new(
|
|
203
208
|
profiles: @graph.profiles, session_store: @graph.session_store,
|
|
204
209
|
task_store: @graph.task_store, executor: executor,
|
|
@@ -314,9 +319,7 @@ module Insika
|
|
|
314
319
|
return if @budget_ledger.nil? || meter.empty?
|
|
315
320
|
|
|
316
321
|
tokens = meter.sum do |m|
|
|
317
|
-
m.
|
|
318
|
-
(m.respond_to?(:cached_tokens) ? m.cached_tokens.to_i : 0) +
|
|
319
|
-
(m.respond_to?(:cache_creation_tokens) ? m.cache_creation_tokens.to_i : 0)
|
|
322
|
+
m.tokens.input.to_i + m.tokens.output.to_i + m.tokens.cache_read.to_i + m.tokens.cache_write.to_i
|
|
320
323
|
end
|
|
321
324
|
return if tokens.zero?
|
|
322
325
|
|
|
@@ -17,11 +17,21 @@ module Insika
|
|
|
17
17
|
class SaveArtifact < RubyLLM::Tool
|
|
18
18
|
description "Save an HTML (or markdown / SVG) page — a report, a digest, " \
|
|
19
19
|
"a summary — and get back a URL to share. Use when the answer " \
|
|
20
|
-
"is a document the channel message cannot carry."
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
20
|
+
"is a document the channel message cannot carry. Executive reports " \
|
|
21
|
+
"need a clear title, period, generation date, evidence-backed takeaway, " \
|
|
22
|
+
"sources and limitations."
|
|
23
|
+
parameter :title, description: "Short human-readable title (<= 200 chars)"
|
|
24
|
+
parameter :content, description: "HTML, markdown, or SVG. HTML reports: use warm neutrals " \
|
|
25
|
+
"and one amber or merchant accent, consistent type/spacing/KPI cards, " \
|
|
26
|
+
"responsive SVG or HTML charts with labels, units and exact values/table. " \
|
|
27
|
+
"Use CSS hover/focus highlights and hoverable tooltips in reserved space, " \
|
|
28
|
+
"labeled native radio chart/table " \
|
|
29
|
+
"choices and details; keep values accessible on touch and focus " \
|
|
30
|
+
"visible. Support system dark mode, print with full tables/details, " \
|
|
31
|
+
"and 140ms transitions disabled for reduced motion. Date the data " \
|
|
32
|
+
"snapshot; never imply live updates. No scripts, event handlers, " \
|
|
33
|
+
"forms, external resources or decorative animation."
|
|
34
|
+
parameter :mime, description: "text/html (default), text/markdown, or image/svg+xml",
|
|
25
35
|
required: false
|
|
26
36
|
|
|
27
37
|
def name = "save_artifact"
|
|
@@ -92,4 +102,4 @@ module Insika
|
|
|
92
102
|
end
|
|
93
103
|
end
|
|
94
104
|
end
|
|
95
|
-
end
|
|
105
|
+
end
|
|
@@ -20,8 +20,8 @@ module Insika
|
|
|
20
20
|
"when the customer agrees to be contacted again (a product, a " \
|
|
21
21
|
"cart, a pending payment). The cancellation policy is permanent: " \
|
|
22
22
|
"a customer who opted out can never be rescheduled."
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
parameter :at, description: "ISO 8601 (absolute) or relative '+6h' / '+2d'"
|
|
24
|
+
parameter :reason, description: "Short machine-readable reason, e.g. 'pix pending, " \
|
|
25
25
|
"customer said she would pay tonight'"
|
|
26
26
|
|
|
27
27
|
def name = "schedule"
|
|
@@ -127,7 +127,7 @@ module Insika
|
|
|
127
127
|
class CancelFollowup < RubyLLM::Tool
|
|
128
128
|
description "Cancel a previously scheduled follow-up by its id. A follow-up " \
|
|
129
129
|
"that already fired cannot be cancelled."
|
|
130
|
-
|
|
130
|
+
parameter :id, description: "The id returned by the schedule tool"
|
|
131
131
|
|
|
132
132
|
def name = "cancel_followup"
|
|
133
133
|
|
|
@@ -19,8 +19,8 @@ module Insika
|
|
|
19
19
|
description "Signal that you cannot proceed and end the turn. Use when the " \
|
|
20
20
|
"request is out of your scope, you lack the data to help, or a human " \
|
|
21
21
|
"must take over. Write your final sentence to the customer first."
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
parameter :reason, description: "Why you cannot proceed (goes to the operator, not the customer)"
|
|
23
|
+
parameter :message, description: "Optional final message if you wrote none", required: false
|
|
24
24
|
|
|
25
25
|
def name = "signal_stuck"
|
|
26
26
|
|
|
@@ -34,7 +34,7 @@ module Insika
|
|
|
34
34
|
# A Halt ends the tool loop here, so the turn cannot continue after declaring
|
|
35
35
|
# stuck. The payload's `say` is the fallback final message when the model
|
|
36
36
|
# wrote no lead-in (the executor's halt_answer already prefers the lead-in).
|
|
37
|
-
|
|
37
|
+
Insika::ToolDefinition::Halt.new(Insika::ToolDefinition.wrap_halt(
|
|
38
38
|
{ "reason" => reason.to_s },
|
|
39
39
|
message.to_s
|
|
40
40
|
))
|
|
@@ -24,10 +24,10 @@ module Insika
|
|
|
24
24
|
"it BLOCKS and returns the child's final answer. Set async:true to " \
|
|
25
25
|
"fire-and-forget a long task: it returns immediately and the child's " \
|
|
26
26
|
"result arrives later as a new message on this conversation."
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
27
|
+
parameter :agent, description: "Id of the child agent to delegate to (must be one this agent may spawn)"
|
|
28
|
+
parameter :message, description: "The self-contained task/prompt for the child agent"
|
|
29
|
+
parameter :async, type: :boolean, required: false,
|
|
30
|
+
description: "true = dispatch and continue (result delivered later); default false = wait for the answer"
|
|
31
31
|
|
|
32
32
|
# otherwise RubyLLM derives "insika--tools--subagent" from the class name.
|
|
33
33
|
def name = "spawn_subagent"
|
|
@@ -51,7 +51,7 @@ module Insika
|
|
|
51
51
|
"#{super} Agents you may spawn: #{@allowed.join(', ')}."
|
|
52
52
|
end
|
|
53
53
|
|
|
54
|
-
def
|
|
54
|
+
def parameters_schema
|
|
55
55
|
@agent_enum_schema ||= Insika::Tools::AgentEnum.inject(super, @allowed, path: %i[agent])
|
|
56
56
|
end
|
|
57
57
|
|
|
@@ -20,9 +20,9 @@ module Insika
|
|
|
20
20
|
"subtasks whose results you'll combine: it runs them concurrently " \
|
|
21
21
|
"(much faster). Each child runs in an ISOLATED context, so put " \
|
|
22
22
|
"everything it needs in its `message`."
|
|
23
|
-
# array-of-objects param via the explicit JSON-schema form (the `
|
|
23
|
+
# array-of-objects param via the explicit JSON-schema form (the `parameter` DSL
|
|
24
24
|
# only reaches strings/scalars). Top-level `tasks` arrives as a kwarg to execute.
|
|
25
|
-
|
|
25
|
+
parameters(
|
|
26
26
|
type: "object",
|
|
27
27
|
properties: {
|
|
28
28
|
tasks: {
|
|
@@ -60,7 +60,7 @@ module Insika
|
|
|
60
60
|
"#{super} Agents you may spawn: #{@allowed.join(', ')}."
|
|
61
61
|
end
|
|
62
62
|
|
|
63
|
-
def
|
|
63
|
+
def parameters_schema
|
|
64
64
|
@agent_enum_schema ||= Insika::Tools::AgentEnum.inject(super, @allowed, path: %i[tasks agent])
|
|
65
65
|
end
|
|
66
66
|
|