insika 0.1.0 → 0.3.0
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 +199 -5
- data/README.md +8 -2
- data/bin/insika +231 -13
- data/docs/AGENTS.md +505 -6
- data/docs/API.md +56 -0
- data/docs/CHANNELS.md +100 -10
- data/docs/CONTEXT.md +147 -19
- data/docs/DEPLOY.md +34 -11
- data/docs/EMBEDDING.md +11 -7
- data/docs/EVALS.md +20 -1
- data/docs/FACTS.md +135 -0
- data/docs/HARVEST.md +117 -0
- data/docs/LOADTEST.md +17 -10
- data/docs/OBSERVABILITY.md +65 -2
- data/docs/REFINEMENT.md +9 -9
- data/docs/RELEASING.md +34 -7
- data/docs/RUNNING-LOCAL.md +4 -4
- data/docs/SECURITY.md +85 -11
- data/docs/SKILLS.md +189 -3
- data/docs/SOAK.md +127 -0
- data/docs/TOOLS.md +70 -2
- data/docs/WHY.md +1 -1
- data/docs/WORKFLOWS.md +2 -2
- data/docs/domain.md +115 -0
- data/docs/index.md +2 -2
- data/docs/onboarding/start.md +1 -1
- data/lib/insika/agent_profile.rb +228 -26
- data/lib/insika/alert_dispatcher.rb +139 -0
- data/lib/insika/balloon_splitter.rb +102 -0
- data/lib/insika/baseline_store.rb +2 -2
- data/lib/insika/budget_ledger.rb +166 -0
- data/lib/insika/cache_series_store.rb +49 -0
- data/lib/insika/channel_delivery.rb +132 -24
- data/lib/insika/channel_registry.rb +1 -1
- data/lib/insika/channels/relay.rb +80 -6
- data/lib/insika/channels/web/widget.js +2 -2
- data/lib/insika/channels/web.rb +9 -9
- data/lib/insika/channels/webhook.rb +58 -0
- data/lib/insika/chat_builder.rb +145 -13
- data/lib/insika/checkpoint_store.rb +16 -0
- data/lib/insika/circuit_state.rb +114 -0
- data/lib/insika/coercion.rb +8 -0
- data/lib/insika/commands/agent_payload.rb +6 -4
- data/lib/insika/commands/cancel_followup.rb +49 -0
- data/lib/insika/commands/create_agent.rb +2 -2
- data/lib/insika/commands/create_session.rb +1 -1
- data/lib/insika/commands/delete_llm_provider.rb +1 -1
- data/lib/insika/commands/delete_skill.rb +43 -0
- data/lib/insika/commands/delete_tenant_data.rb +95 -0
- data/lib/insika/commands/export_customer_memory.rb +48 -0
- data/lib/insika/commands/forget_customer.rb +117 -0
- data/lib/insika/commands/freeze_funnel_baseline.rb +113 -0
- data/lib/insika/commands/gate_harvest.rb +138 -0
- data/lib/insika/commands/gate_refinement.rb +12 -12
- data/lib/insika/commands/import_mcp_tools.rb +1 -1
- data/lib/insika/commands/import_tools.rb +4 -4
- data/lib/insika/commands/issue_tenant_token.rb +41 -0
- data/lib/insika/commands/judge_shadow_pairs.rb +124 -0
- data/lib/insika/commands/memory_forget_fact.rb +20 -4
- data/lib/insika/commands/memory_put_fact.rb +23 -4
- data/lib/insika/commands/promote_harvest.rb +130 -0
- data/lib/insika/commands/record_outcome.rb +46 -0
- data/lib/insika/commands/record_shadow_reply.rb +68 -0
- data/lib/insika/commands/reject_harvest.rb +38 -0
- data/lib/insika/commands/resolve_proposal.rb +108 -0
- data/lib/insika/commands/resolve_refinement.rb +1 -1
- data/lib/insika/commands/revoke_contact.rb +49 -0
- data/lib/insika/commands/revoke_token.rb +39 -0
- data/lib/insika/commands/rollback_harvest.rb +86 -0
- data/lib/insika/commands/rotate_tenant_token.rb +43 -0
- data/lib/insika/commands/run_distillation.rb +186 -0
- data/lib/insika/commands/run_harvest.rb +393 -0
- data/lib/insika/commands/run_refinement.rb +5 -5
- data/lib/insika/commands/send_message.rb +112 -15
- data/lib/insika/commands/session_purge.rb +67 -0
- data/lib/insika/commands/set_agent_tools.rb +1 -1
- data/lib/insika/commands/set_skill_agents.rb +60 -19
- data/lib/insika/commands/trigger_workflow.rb +1 -1
- data/lib/insika/commands/update_agent.rb +1 -1
- data/lib/insika/commands/write_data_tool.rb +1 -1
- data/lib/insika/commands/write_golden.rb +1 -1
- data/lib/insika/commands/write_skill.rb +19 -9
- data/lib/insika/config_store.rb +8 -4
- data/lib/insika/contact_store.rb +183 -0
- data/lib/insika/context/builder.rb +23 -5
- data/lib/insika/context/fragment.rb +31 -3
- data/lib/insika/context/priority.rb +6 -2
- data/lib/insika/context/provider.rb +17 -3
- data/lib/insika/context/providers/briefing.rb +96 -0
- data/lib/insika/context/providers/memory.rb +16 -7
- data/lib/insika/context/providers/prompt.rb +30 -2
- data/lib/insika/context/providers/request.rb +1 -1
- data/lib/insika/context/providers/session.rb +17 -2
- data/lib/insika/context/providers/skill.rb +7 -1
- data/lib/insika/context/providers/skill_trigger.rb +128 -0
- data/lib/insika/context/providers/tool_search.rb +2 -0
- data/lib/insika/context_trace_store.rb +128 -0
- data/lib/insika/delegation_store.rb +2 -2
- data/lib/insika/distill.rb +224 -0
- data/lib/insika/distill_engine.rb +169 -0
- data/lib/insika/doctor.rb +962 -7
- data/lib/insika/dsl/runtime.rb +20 -11
- data/lib/insika/dsl/server_boot.rb +74 -4
- data/lib/insika/dsl/system.rb +1 -1
- data/lib/insika/dsl.rb +152 -15
- data/lib/insika/edge_limiter.rb +167 -8
- data/lib/insika/egress_guard.rb +3 -3
- data/lib/insika/env_schema.rb +22 -12
- data/lib/insika/errors.rb +72 -5
- data/lib/insika/evals/assertions.rb +15 -14
- data/lib/insika/evals/baseline.rb +3 -3
- data/lib/insika/evals/golden.rb +8 -8
- data/lib/insika/evals/judge.rb +7 -7
- data/lib/insika/evals/pairwise.rb +21 -9
- data/lib/insika/evals/report.rb +2 -2
- data/lib/insika/evals/runner.rb +6 -6
- data/lib/insika/evals/transport.rb +2 -2
- data/lib/insika/event_stream.rb +23 -5
- data/lib/insika/evidence.rb +183 -0
- data/lib/insika/executor.rb +1092 -160
- data/lib/insika/followup_engine.rb +207 -0
- data/lib/insika/followup_policy.rb +221 -0
- data/lib/insika/followup_store.rb +306 -0
- data/lib/insika/frontmatter.rb +1 -1
- data/lib/insika/funnel_declaration.rb +106 -0
- data/lib/insika/funnel_fold.rb +179 -0
- data/lib/insika/funnel_store.rb +163 -0
- data/lib/insika/golden_store.rb +3 -3
- data/lib/insika/grounding/matcher.rb +69 -0
- data/lib/insika/grounding.rb +44 -0
- data/lib/insika/harvest/conversion_gate.rb +159 -0
- data/lib/insika/harvest/criterion.rb +98 -0
- data/lib/insika/harvest/gate.rb +194 -0
- data/lib/insika/harvest/negative_list.rb +199 -0
- data/lib/insika/harvest.rb +241 -0
- data/lib/insika/harvest_engine.rb +193 -0
- data/lib/insika/harvest_store.rb +548 -0
- data/lib/insika/http_client.rb +3 -3
- data/lib/insika/inbound_log.rb +1 -1
- data/lib/insika/llm_configurator.rb +3 -3
- data/lib/insika/loop_detector.rb +143 -0
- data/lib/insika/mcp_http_client.rb +4 -4
- data/lib/insika/mcp_tool_ingestor.rb +6 -6
- data/lib/insika/media.rb +298 -0
- data/lib/insika/memory_audit_store.rb +85 -0
- data/lib/insika/memory_store.rb +264 -23
- data/lib/insika/message_origin.rb +8 -3
- data/lib/insika/model_resolver.rb +1 -1
- data/lib/insika/model_selection.rb +5 -4
- data/lib/insika/model_visible.rb +87 -0
- data/lib/insika/model_visible_trace_store.rb +66 -0
- data/lib/insika/onboarding.rb +8 -3
- data/lib/insika/outbox_store.rb +44 -6
- data/lib/insika/outcome_store.rb +147 -0
- data/lib/insika/overlay_tool_registry.rb +3 -4
- data/lib/insika/pack.rb +3 -3
- data/lib/insika/pack_importer.rb +17 -15
- data/lib/insika/packaging.rb +163 -0
- data/lib/insika/parity/criterion.rb +79 -0
- data/lib/insika/parity/verdict.rb +318 -0
- data/lib/insika/pending_action_store.rb +1 -1
- data/lib/insika/plugin/loader.rb +2 -2
- data/lib/insika/policy/policy.rb +1 -1
- data/lib/insika/prefix_fingerprint.rb +58 -0
- data/lib/insika/profile_source.rb +34 -7
- data/lib/insika/proposal_store.rb +271 -0
- data/lib/insika/provider_error_classifier.rb +160 -0
- data/lib/insika/queue_policy.rb +6 -3
- data/lib/insika/recovery.rb +47 -6
- data/lib/insika/refinement/candidate.rb +4 -4
- data/lib/insika/refinement/evidence_collector.rb +6 -6
- data/lib/insika/refinement/gate.rb +7 -7
- data/lib/insika/refinement/panel.rb +7 -7
- data/lib/insika/refinement/proposer.rb +10 -10
- data/lib/insika/refinement_store.rb +12 -12
- data/lib/insika/reliability.rb +211 -0
- data/lib/insika/retention.rb +281 -0
- data/lib/insika/routing.rb +101 -0
- data/lib/insika/safety/config.rb +46 -6
- data/lib/insika/safety/corpus.rb +255 -0
- data/lib/insika/safety/detectors.rb +34 -115
- data/lib/insika/safety/factory.rb +18 -5
- data/lib/insika/safety/grounding_enforcer.rb +59 -0
- data/lib/insika/safety/grounding_validator.rb +49 -0
- data/lib/insika/safety/input_guardrail.rb +20 -5
- data/lib/insika/safety/moderator.rb +19 -11
- data/lib/insika/safety/output_filter.rb +10 -6
- data/lib/insika/safety/output_validator.rb +13 -7
- data/lib/insika/safety/safe_responses.rb +1 -1
- data/lib/insika/sandbox/boundary.rb +2 -2
- data/lib/insika/sandbox.rb +1 -1
- data/lib/insika/schema_guard.rb +35 -0
- data/lib/insika/server/app.rb +366 -54
- data/lib/insika/server/boot.rb +4 -4
- data/lib/insika/server/rack_app.rb +31 -7
- data/lib/insika/server/responses.rb +58 -9
- data/lib/insika/server/tenant_auth.rb +61 -0
- data/lib/insika/session_actor.rb +11 -7
- data/lib/insika/session_store.rb +66 -3
- data/lib/insika/settings_store.rb +15 -5
- data/lib/insika/shadow_pair_store.rb +258 -0
- data/lib/insika/shutdown.rb +4 -4
- data/lib/insika/skill_catalog.rb +131 -20
- data/lib/insika/skill_store.rb +70 -22
- data/lib/insika/soak/envelope.rb +140 -0
- data/lib/insika/soak/report.rb +392 -0
- data/lib/insika/soak/runner.rb +554 -0
- data/lib/insika/steer_injector.rb +1 -1
- data/lib/insika/store.rb +11 -2
- data/lib/insika/stores/memory.rb +6 -0
- data/lib/insika/stores/sqlite.rb +8 -0
- data/lib/insika/studio/app.rb +1058 -75
- data/lib/insika/studio/assets/dist/application.css +1 -1
- data/lib/insika/studio/assets/dist/application.js +27 -26
- data/lib/insika/studio/assets/dist/favicon.svg +6 -0
- data/lib/insika/studio/forms.rb +274 -22
- data/lib/insika/studio/nav_icons.rb +7 -2
- data/lib/insika/studio/views/_message.erb +2 -2
- data/lib/insika/studio/views/agent_detail.erb +629 -86
- data/lib/insika/studio/views/agents.erb +11 -7
- data/lib/insika/studio/views/approvals.erb +4 -1
- data/lib/insika/studio/views/chats.erb +4 -1
- data/lib/insika/studio/views/customer.erb +94 -0
- data/lib/insika/studio/views/customers.erb +32 -0
- data/lib/insika/studio/views/evals.erb +4 -1
- data/lib/insika/studio/views/facts.erb +133 -0
- data/lib/insika/studio/views/followups.erb +125 -0
- data/lib/insika/studio/views/funnel.erb +106 -0
- data/lib/insika/studio/views/harvest.erb +234 -0
- data/lib/insika/studio/views/home.erb +2 -1
- data/lib/insika/studio/views/layout.erb +1 -0
- data/lib/insika/studio/views/parity.erb +147 -0
- data/lib/insika/studio/views/playground.erb +7 -1
- data/lib/insika/studio/views/refinement.erb +4 -4
- data/lib/insika/studio/views/session.erb +133 -3
- data/lib/insika/studio/views/settings.erb +9 -12
- data/lib/insika/studio/views/skills.erb +66 -12
- data/lib/insika/studio/views/system_files.erb +1 -1
- data/lib/insika/studio/views/task.erb +13 -0
- data/lib/insika/studio/views/tasks.erb +4 -1
- data/lib/insika/studio/views/tools.erb +0 -1
- data/lib/insika/subagent_graph.rb +3 -3
- data/lib/insika/task_actor.rb +3 -3
- data/lib/insika/task_store.rb +22 -2
- data/lib/insika/telemetry/pricing.rb +3 -3
- data/lib/insika/telemetry/recorder.rb +1 -1
- data/lib/insika/telemetry.rb +2 -2
- data/lib/insika/testing/store_contract.rb +54 -33
- data/lib/insika/tick.rb +146 -0
- data/lib/insika/token_store.rb +168 -0
- data/lib/insika/tool_assembly.rb +5 -5
- data/lib/insika/tool_definition.rb +25 -15
- data/lib/insika/tool_envelope.rb +70 -1
- data/lib/insika/tool_manifest.rb +11 -7
- data/lib/insika/tool_output_compressor.rb +100 -0
- data/lib/insika/tool_store.rb +1 -1
- data/lib/insika/tool_trace_store.rb +1 -1
- data/lib/insika/tools/concurrency.rb +2 -2
- data/lib/insika/tools/data_defined_tool.rb +14 -5
- data/lib/insika/tools/generate_image.rb +44 -0
- data/lib/insika/tools/load_skill.rb +61 -3
- data/lib/insika/tools/schedule_followup.rb +164 -0
- data/lib/insika/tools/stuck_signal.rb +44 -0
- data/lib/insika/tools/subagent.rb +4 -4
- data/lib/insika/tools/subagents.rb +1 -1
- data/lib/insika/tools/tts.rb +47 -0
- data/lib/insika/tools/update_briefing.rb +126 -0
- data/lib/insika/turn_output.rb +2 -2
- data/lib/insika/turn_state.rb +54 -13
- data/lib/insika/turn_timing.rb +24 -4
- data/lib/insika/usage_ledger.rb +1 -1
- data/lib/insika/version.rb +1 -1
- data/lib/insika/vitals.rb +84 -0
- data/lib/insika/wiring/graph.rb +372 -34
- data/lib/insika/workflow.rb +1 -1
- data/lib/insika/workflow_registry.rb +1 -1
- data/lib/insika.rb +122 -16
- metadata +95 -2
- data/lib/insika/server/admin_auth.rb +0 -29
data/lib/insika/evals/judge.rb
CHANGED
|
@@ -4,7 +4,7 @@ require "json"
|
|
|
4
4
|
|
|
5
5
|
module Insika
|
|
6
6
|
module Evals
|
|
7
|
-
# The LLM-judge
|
|
7
|
+
# The LLM-judge. Scores a golden's `rubric` against the
|
|
8
8
|
# actual assistant reply — the subjective layer on top of the deterministic
|
|
9
9
|
# asserts. Pure over an injected `ask` callable (prompt -> raw model text), so it's
|
|
10
10
|
# unit-testable without an LLM; the real ask (RubyLLM on the utility_model, temp 0)
|
|
@@ -13,7 +13,7 @@ module Insika
|
|
|
13
13
|
# Conservative by construction: an unparseable judge reply scores 0 (fails) rather
|
|
14
14
|
# than silently passing.
|
|
15
15
|
#
|
|
16
|
-
# A PANEL, not a single voice
|
|
16
|
+
# A PANEL, not a single voice. `quorum: N` samples ONE model N
|
|
17
17
|
# times, which measures that model's variance and little else — at temperature 0 it
|
|
18
18
|
# mostly returns the same answer, including the same blind spot. Two DIFFERENT
|
|
19
19
|
# models disagreeing about a rubric is the signal worth having, so `asks:` takes one
|
|
@@ -95,7 +95,7 @@ module Insika
|
|
|
95
95
|
end
|
|
96
96
|
end
|
|
97
97
|
|
|
98
|
-
# The `policy` is the one thing a rubric cannot carry alone
|
|
98
|
+
# The `policy` is the one thing a rubric cannot carry alone: how
|
|
99
99
|
# much this store wants the agent to ask before acting is a per-store decision,
|
|
100
100
|
# and a judge that is not TOLD it will guess — half the time wrongly. The
|
|
101
101
|
# deterministic half is `Assertions.policy_checks`; this is the other half.
|
|
@@ -149,11 +149,11 @@ module Insika
|
|
|
149
149
|
end
|
|
150
150
|
end
|
|
151
151
|
|
|
152
|
-
# Builds the configured judge panel from `settings["evals"]
|
|
152
|
+
# Builds the configured judge panel from `settings["evals"]`.
|
|
153
153
|
#
|
|
154
154
|
# It lives HERE and not in `evals/run.rb` because the CLI is no longer the only
|
|
155
155
|
# caller: the refinement gate scores a candidate with the SAME judges the operator
|
|
156
|
-
# configured, and
|
|
156
|
+
# configured, and is explicit that a second copy of the judge would be the
|
|
157
157
|
# worst possible outcome — the gate would then be grading against a rubric nobody
|
|
158
158
|
# tuned. One builder, two callers.
|
|
159
159
|
module JudgePanel
|
|
@@ -164,7 +164,7 @@ module Insika
|
|
|
164
164
|
# -> [Judge, [model names]] | nil when nobody is configured to ask. NIL AND NOT
|
|
165
165
|
# a no-op judge: a rubric'd case with no judge reads as `judge_pending`, which is
|
|
166
166
|
# visible, where a judge that always passes would be silent.
|
|
167
|
-
# `llm
|
|
167
|
+
# `llm`: the graph's own RubyLLM context; nil = the global
|
|
168
168
|
# constant. Only the deployment root and the CLI build judges today, and both
|
|
169
169
|
# are one graph per process — the seam keeps the default honest for the day
|
|
170
170
|
# an embedded graph gates a candidate on its own credentials.
|
|
@@ -187,7 +187,7 @@ module Insika
|
|
|
187
187
|
# Sugar for the callers that only want the judge (the gate).
|
|
188
188
|
def judge(settings, **kw) = build(settings, **kw)&.first
|
|
189
189
|
|
|
190
|
-
# The SAME configured models, asked a different question
|
|
190
|
+
# The SAME configured models, asked a different question. One
|
|
191
191
|
# builder because "who judges here" is one operator decision: a pairwise panel
|
|
192
192
|
# configured apart from the rubric panel would let a run be graded by judges
|
|
193
193
|
# nobody chose. -> [Pairwise, [model names]] | nil when nobody is configured.
|
|
@@ -4,7 +4,7 @@ require "json"
|
|
|
4
4
|
|
|
5
5
|
module Insika
|
|
6
6
|
module Evals
|
|
7
|
-
# PAIRWISE AGAINST THE INCUMBENT
|
|
7
|
+
# PAIRWISE AGAINST THE INCUMBENT — the number that answers "can we
|
|
8
8
|
# replace it". An absolute 0.72 says a reply cleared a bar we invented; it says
|
|
9
9
|
# nothing about whether the system already answering 403,231 chats would have done
|
|
10
10
|
# better with the same customer.
|
|
@@ -29,7 +29,7 @@ module Insika
|
|
|
29
29
|
# 3. **A split panel stays split.** Averaging "better" and "worse" into
|
|
30
30
|
# "comparable" invents agreement that nobody expressed.
|
|
31
31
|
#
|
|
32
|
-
# Cost: 2 provider calls per judge per case
|
|
32
|
+
# Cost: 2 provider calls per judge per case. This is why it never runs as
|
|
33
33
|
# part of the gate and is opt-in on the CLI.
|
|
34
34
|
class Pairwise
|
|
35
35
|
BETTER = "better"
|
|
@@ -39,7 +39,7 @@ module Insika
|
|
|
39
39
|
UNKNOWN = "unknown"
|
|
40
40
|
|
|
41
41
|
# `vs` names WHO the reference half actually is: `agent` (model against model)
|
|
42
|
-
# or `human-assisted` (a person typed part of it,
|
|
42
|
+
# or `human-assisted` (a person typed part of it,'s `origin: operator`).
|
|
43
43
|
# It rides on the verdict rather than beside it so a report cannot print the
|
|
44
44
|
# outcome without the label.
|
|
45
45
|
Verdict = Struct.new(:outcome, :reason, :vs, :judges, :order_dependent, keyword_init: true) do
|
|
@@ -60,12 +60,19 @@ module Insika
|
|
|
60
60
|
def compare(golden:, turns:)
|
|
61
61
|
return nil unless golden.reference?
|
|
62
62
|
|
|
63
|
-
ours
|
|
64
|
-
|
|
65
|
-
|
|
63
|
+
compare_texts(ours: Pairwise.transcript(golden.user_turns, turns),
|
|
64
|
+
theirs: Pairwise.reference_transcript(golden.reference_messages),
|
|
65
|
+
vs: golden.human_assisted? ? "human-assisted" : "agent")
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# two transcripts, no golden — the shadow seam. The judge is
|
|
69
|
+
# the SAME object with the SAME prompt, so shadow pairs and golden cases are
|
|
70
|
+
# graded by one rule. An empty side returns nil (never a verdict against an
|
|
71
|
+
# empty string — that was a bug in the golden path until this seam landed).
|
|
72
|
+
def compare_texts(ours:, theirs:, vs: "agent")
|
|
73
|
+
return nil if ours.to_s.strip.empty? || theirs.to_s.strip.empty?
|
|
66
74
|
|
|
67
|
-
|
|
68
|
-
combine(panel, vs: golden.human_assisted? ? "human-assisted" : "agent")
|
|
75
|
+
combine(@asks.map { |ask| judge_once(ask, ours, theirs) }, vs: vs)
|
|
69
76
|
end
|
|
70
77
|
|
|
71
78
|
# The replayed conversation as the judge reads it: the user turns we sent,
|
|
@@ -81,8 +88,13 @@ module Insika
|
|
|
81
88
|
# is the conversation as the customer received it, and telling it "a person wrote
|
|
82
89
|
# this one" is an invitation to grade the author instead. The fact is carried to
|
|
83
90
|
# the READER as `vs: human-assisted` instead, which is where it changes a decision.
|
|
91
|
+
# A reference with no text at all returns "" — the caller's empty guard then
|
|
92
|
+
# refuses instead of judging against an empty string .
|
|
84
93
|
def self.reference_transcript(messages)
|
|
85
|
-
Array(messages)
|
|
94
|
+
msgs = Array(messages)
|
|
95
|
+
return "" if msgs.all? { |m| m["text"].to_s.strip.empty? }
|
|
96
|
+
|
|
97
|
+
msgs.map do |m|
|
|
86
98
|
speaker = m["role"].to_s == "user" ? "customer" : "assistant"
|
|
87
99
|
"#{speaker}: #{m['text'].to_s.strip}"
|
|
88
100
|
end.join("\n")
|
data/lib/insika/evals/report.rb
CHANGED
|
@@ -5,7 +5,7 @@ require "json"
|
|
|
5
5
|
module Insika
|
|
6
6
|
module Evals
|
|
7
7
|
# Renders a run's [CaseResult] as a machine-readable JSON blob (for the baseline
|
|
8
|
-
#
|
|
8
|
+
# gating in) and a human-readable markdown summary.
|
|
9
9
|
# Pure over the results — takes a clock value in, never reads it (so callers stay
|
|
10
10
|
# deterministic/testable).
|
|
11
11
|
module Report
|
|
@@ -90,7 +90,7 @@ module Insika
|
|
|
90
90
|
lines = ["# Eval report — #{at}", "",
|
|
91
91
|
"**#{h['passed']}/#{h['total'] - h['skipped']} passed** · #{h['failed']} failed" \
|
|
92
92
|
"#{" · #{h['skipped']} skipped" if h['skipped'].positive?}" \
|
|
93
|
-
"#{" · #{h['judge_pending']} awaiting judge
|
|
93
|
+
"#{" · #{h['judge_pending']} awaiting judge" if h['judge_pending'].positive?}", ""]
|
|
94
94
|
results.each do |r|
|
|
95
95
|
if r.skipped?
|
|
96
96
|
# WITH the reason, always: "12 skipped" alone is indistinguishable from a
|
data/lib/insika/evals/runner.rb
CHANGED
|
@@ -17,7 +17,7 @@ module Insika
|
|
|
17
17
|
# `tokens` is what the whole case cost, summed over its turns, or nil when no
|
|
18
18
|
# turn reported usage. `cached` is how much of that was served from the prompt
|
|
19
19
|
# cache, carried separately because it is the number that explains a total.
|
|
20
|
-
# Only the refinement gate reads them (
|
|
20
|
+
# Only the refinement gate reads them (records a run's cost); the
|
|
21
21
|
# report and the exit code are untouched.
|
|
22
22
|
RunCase = Struct.new(:result, :timings, :tokens, :cached, keyword_init: true)
|
|
23
23
|
|
|
@@ -26,13 +26,13 @@ module Insika
|
|
|
26
26
|
#
|
|
27
27
|
# capabilities: what the DEPLOYMENT has, per agent — anything answering
|
|
28
28
|
# `#for(agent_id)` with { "tools" =>, "capabilities" => } or nil. Used to skip a
|
|
29
|
-
# case the deployment cannot satisfy
|
|
29
|
+
# case the deployment cannot satisfy, BEFORE spending a turn on
|
|
30
30
|
# it. nil (or an unknown agent) = no resolution, and then a case with `requires`
|
|
31
31
|
# RUNS and says so in the report: "could not rule it out" is not a reason to
|
|
32
32
|
# stop testing something, and a suite that shrinks in silence is the failure
|
|
33
33
|
# this feature exists to avoid.
|
|
34
34
|
#
|
|
35
|
-
# pairwise: an Evals::Pairwise (optional
|
|
35
|
+
# pairwise: an Evals::Pairwise (optional). Only cases carrying a
|
|
36
36
|
# `reference:` are compared, and the verdict never touches pass/fail — it is the
|
|
37
37
|
# answer to "can we replace it", reported beside the suite's own verdict.
|
|
38
38
|
def initialize(transport:, judge: nil, conv_map: {}, capabilities: nil, pairwise: nil)
|
|
@@ -53,8 +53,8 @@ module Insika
|
|
|
53
53
|
skip = skip_reason(golden)
|
|
54
54
|
return RunCase.new(result: Assertions.skip(golden, skip), timings: []) if skip
|
|
55
55
|
|
|
56
|
-
# A backend that resolves state from a pre-existing conversation (e.g.
|
|
57
|
-
#
|
|
56
|
+
# A backend that resolves state from a pre-existing conversation (e.g. a
|
|
57
|
+
# consumer needing a real Chat UUID as X-Chat-Id) supplies it via conv_map; otherwise the
|
|
58
58
|
# synthetic "eval-<id>" keeps the adapter's own multi-turn continuation.
|
|
59
59
|
conv = @conv_map[golden.id] || "eval-#{golden.id}"
|
|
60
60
|
turns = []
|
|
@@ -76,7 +76,7 @@ module Insika
|
|
|
76
76
|
# Subjective layer: only when a judge is configured, the case has a rubric, and
|
|
77
77
|
# the turn ran cleanly (nothing to judge on an errored turn).
|
|
78
78
|
result.judge = @judge.score(golden: golden, result: last) if @judge && result.rubric && result.error.nil?
|
|
79
|
-
# Against the incumbent
|
|
79
|
+
# Against the incumbent. Same rule as the judge: nothing to
|
|
80
80
|
# compare on a turn that errored — half a conversation would lose the
|
|
81
81
|
# comparison for a reason that has nothing to do with the agent.
|
|
82
82
|
result.pairwise = @pairwise.compare(golden: golden, turns: turns) if @pairwise && result.error.nil?
|
|
@@ -16,7 +16,7 @@ module Insika
|
|
|
16
16
|
#
|
|
17
17
|
# `usage` is the turn's token counts as the deployment reported them
|
|
18
18
|
# (`response.completed`), or nil when the provider sent none. It is carried, never
|
|
19
|
-
# asserted on: the consumer is
|
|
19
|
+
# asserted on: the consumer is's refinement budget, which has to bound the
|
|
20
20
|
# cost of a gate replay and cannot invent the number. nil is preserved as nil
|
|
21
21
|
# rather than zeroed — "the provider did not say" and "it cost nothing" are
|
|
22
22
|
# different facts, and a budget that confuses them stops being a budget.
|
|
@@ -72,7 +72,7 @@ module Insika
|
|
|
72
72
|
end
|
|
73
73
|
end
|
|
74
74
|
|
|
75
|
-
# What the deployment HAS, per agent
|
|
75
|
+
# What the deployment HAS, per agent, read over the same gated
|
|
76
76
|
# `/v1` the replay uses. The eval stays a client: it asks the engine instead of
|
|
77
77
|
# keeping its own idea of which tools exist.
|
|
78
78
|
#
|
data/lib/insika/event_stream.rb
CHANGED
|
@@ -20,9 +20,11 @@ module Insika
|
|
|
20
20
|
# local :error event — the turn never waits on transport.
|
|
21
21
|
MAX_QUEUED = 1000
|
|
22
22
|
|
|
23
|
-
def initialize(task_id: nil, session_id: nil, on_close: nil)
|
|
23
|
+
def initialize(task_id: nil, session_id: nil, tenant: nil, types: nil, on_close: nil)
|
|
24
24
|
@task_id = task_id
|
|
25
25
|
@session_id = session_id
|
|
26
|
+
@tenant = tenant
|
|
27
|
+
@types = types
|
|
26
28
|
@on_close = on_close
|
|
27
29
|
@queue = Async::Queue.new
|
|
28
30
|
end
|
|
@@ -39,9 +41,21 @@ module Insika
|
|
|
39
41
|
|
|
40
42
|
# Meta filter: nil = matches any value. Events with no task_id in
|
|
41
43
|
# meta (e.g. :session_created) reach only subscribers with no task filter.
|
|
44
|
+
#
|
|
45
|
+
# A TENANT-scoped subscription is FAIL-CLOSED on the meta's tenant (WS1):
|
|
46
|
+
# an event that does not carry the tenant (control events, ignored turns)
|
|
47
|
+
# matches NO tenant subscription. The tenant only ever sees its own.
|
|
48
|
+
#
|
|
49
|
+
# `types:` (nil = any) keeps a subscriber's queue to the events it answers
|
|
50
|
+
# — an alert consumer must not sit behind a full-traffic stream's 1000-cap
|
|
51
|
+
# (WS6), and a filtered queue is the cheapest way to keep it there.
|
|
42
52
|
def matches?(event)
|
|
43
53
|
meta = event.meta || {}
|
|
44
|
-
|
|
54
|
+
owned = @tenant.nil? || meta[:tenant] == @tenant
|
|
55
|
+
|
|
56
|
+
owned &&
|
|
57
|
+
(@types.nil? || @types.include?(event.type)) &&
|
|
58
|
+
(@task_id.nil? || meta[:task_id] == @task_id) &&
|
|
45
59
|
(@session_id.nil? || meta[:session_id] == @session_id)
|
|
46
60
|
end
|
|
47
61
|
|
|
@@ -103,9 +117,13 @@ module Insika
|
|
|
103
117
|
end
|
|
104
118
|
|
|
105
119
|
# nil/nil = all events. Returns the Subscription (the caller iterates with
|
|
106
|
-
# `#each` on its own fiber).
|
|
107
|
-
|
|
108
|
-
|
|
120
|
+
# `#each` on its own fiber). `tenant:` scopes the stream to one tenant's
|
|
121
|
+
# events (WS1) — fail-closed, see Subscription#matches?. `types:` (nil =
|
|
122
|
+
# any) filters by event type so a subscriber's queue only ever holds what
|
|
123
|
+
# its consumer answers (WS6).
|
|
124
|
+
def subscribe(task_id: nil, session_id: nil, tenant: nil, types: nil)
|
|
125
|
+
sub = Subscription.new(task_id: task_id, session_id: session_id, tenant: tenant,
|
|
126
|
+
types: types,
|
|
109
127
|
on_close: ->(s) { @subscriptions.delete(s) })
|
|
110
128
|
@subscriptions << sub
|
|
111
129
|
sub
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Insika
|
|
6
|
+
# — the evidence contract (engine half).
|
|
7
|
+
#
|
|
8
|
+
# A tool DECLARES `evidence` in its manifest; the engine then does BOTH jobs
|
|
9
|
+
# from the same declaration: strips the result down to `{items: [{id, line}]}`
|
|
10
|
+
# for the model (the lean envelope), and appends every `id` to the session
|
|
11
|
+
# evidence ledger. No second flag, no "lean but not evidence" mode — a
|
|
12
|
+
# half-configuration cannot exist, which is what keeps "no claim without a
|
|
13
|
+
# tool ID" a tautology at the envelope instead of a convention.
|
|
14
|
+
#
|
|
15
|
+
# Everything here is pure Ruby, no IO: the ToolEnvelope calls it after the real
|
|
16
|
+
# tool returns. It does NOT write anything — the ledger write is the envelope's,
|
|
17
|
+
# via the state.
|
|
18
|
+
module Evidence
|
|
19
|
+
# The declaration (D1/C2.1). Parses three authorable shapes:
|
|
20
|
+
#
|
|
21
|
+
# { "evidence": "products" } // bare kind
|
|
22
|
+
# { "evidence": { "kind": "products" } } // full form
|
|
23
|
+
# { "evidence": { "kind": "products",
|
|
24
|
+
# "items": "results",
|
|
25
|
+
# "attachments": "cards" } } // non-default paths
|
|
26
|
+
#
|
|
27
|
+
# `kind` is the PILOT PACK's value, never a gem constant (the
|
|
28
|
+
# engine owns nothing about product shape).
|
|
29
|
+
Spec = Data.define(:kind, :items_path, :attachments_path) do
|
|
30
|
+
PATH_RE = /\A[a-zA-Z0-9_]+(?:\.[a-zA-Z0-9_]+)*\z/
|
|
31
|
+
|
|
32
|
+
# String | Hash | nil -> Spec | nil. Raises ValidationError on a blank kind
|
|
33
|
+
# or an empty/ill-formed path. All at ingestion, never at the turn.
|
|
34
|
+
def self.parse(raw)
|
|
35
|
+
return nil if raw.nil? || raw == false
|
|
36
|
+
|
|
37
|
+
h = raw.is_a?(String) ? { "kind" => raw } : Coercion.deep_stringify(raw)
|
|
38
|
+
h = h.is_a?(Hash) ? h : {}
|
|
39
|
+
kind = Coercion.presence(h["kind"])
|
|
40
|
+
raise Insika::ValidationError, "evidence.kind is required" if kind.nil?
|
|
41
|
+
|
|
42
|
+
items = presence_path(h["items"], "items")
|
|
43
|
+
attachments = presence_path(h["attachments"], "attachments")
|
|
44
|
+
new(kind: kind, items_path: items, attachments_path: attachments)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def to_h
|
|
48
|
+
{ "kind" => kind, "items" => items_path, "attachments" => attachments_path }.compact
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def self.presence_path(value, default)
|
|
52
|
+
s = Coercion.presence(value)
|
|
53
|
+
s = default if s.nil?
|
|
54
|
+
raise Insika::ValidationError, "evidence.#{default}: not a dotted path" unless PATH_RE.match?(s)
|
|
55
|
+
|
|
56
|
+
s
|
|
57
|
+
end
|
|
58
|
+
private_class_method :presence_path
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# The lean result the model sees — the ONLY thing that survives the envelope.
|
|
62
|
+
MAX_ITEMS = 16
|
|
63
|
+
# Line truncation keeps the transcript lean by force (E1).
|
|
64
|
+
LINE_MAX = 200
|
|
65
|
+
# Attachments are a channel nicety, never the answer; bounded on purpose.
|
|
66
|
+
MAX_ATTACHMENTS = 16
|
|
67
|
+
URL_MAX = 500
|
|
68
|
+
|
|
69
|
+
# The attachments contract, validated for the outbox (channel side, never the
|
|
70
|
+
# model): [{ "type" => "card"|"image", "url" => String, "caption" => String|nil }].
|
|
71
|
+
# Entries without a String url, or beyond MAX_ATTACHMENTS, are DROPPED — never
|
|
72
|
+
# a turn failure (the card is a channel nicety, not the answer).
|
|
73
|
+
def self.valid_attachments(list)
|
|
74
|
+
Array(list).filter_map do |entry|
|
|
75
|
+
next unless entry.is_a?(Hash)
|
|
76
|
+
|
|
77
|
+
url = (entry["url"] || entry[:url]).to_s
|
|
78
|
+
next if url.empty?
|
|
79
|
+
|
|
80
|
+
caption = Coercion.presence(entry["caption"] || entry[:caption])
|
|
81
|
+
{ "type" => (entry["type"] || entry[:type]).to_s,
|
|
82
|
+
"url" => url[0, URL_MAX],
|
|
83
|
+
"caption" => caption }
|
|
84
|
+
end.first(MAX_ATTACHMENTS)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Result shaping, stateless — callable from any tool-call fiber (the parallel
|
|
88
|
+
# tool_concurrency path). No shared state in this class.
|
|
89
|
+
class Processor
|
|
90
|
+
class << self
|
|
91
|
+
# -> the parsed Hash the evidence paths dig into. For `evidence_envelope`
|
|
92
|
+
# the raw body lives under `__insika_body` (D3); a code tool returns the
|
|
93
|
+
# object itself. Raises JSON::ParserError on a non-JSON envelope body —
|
|
94
|
+
# the envelope turns that into `{error:}`, nothing recorded (fail closed:
|
|
95
|
+
# no IDs, no claims to make).
|
|
96
|
+
def raw(spec, result)
|
|
97
|
+
return result unless result.is_a?(Hash) && result.key?("__insika_body")
|
|
98
|
+
|
|
99
|
+
JSON.parse(result["__insika_body"].to_s)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# -> [lean, attachments]. Assumes the shape already passed
|
|
103
|
+
# SchemaGuard.violation_output. Items are capped and lines truncated; the
|
|
104
|
+
# model must never see a null where the contract says items.
|
|
105
|
+
def build(spec, raw)
|
|
106
|
+
items = SchemaGuard.dig(raw, spec.items_path) || []
|
|
107
|
+
lean_items = items.first(MAX_ITEMS).map do |item|
|
|
108
|
+
{ "id" => (item["id"] || item[:id]).to_s,
|
|
109
|
+
"line" => Coercion.utf8((item["line"] || item[:line]).to_s)[0, LINE_MAX] }
|
|
110
|
+
end
|
|
111
|
+
lean = { "items" => lean_items }
|
|
112
|
+
attachments = Insika::Evidence.valid_attachments(SchemaGuard.dig(raw, spec.attachments_path))
|
|
113
|
+
[lean, attachments]
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# The session evidence ledger (C4). A session-scoped SET with an `ungrounded`
|
|
120
|
+
# counter: records every product id that entered the context via an
|
|
121
|
+
# evidence-declared tool this session , plus the ungrounded
|
|
122
|
+
# counter that feeds the daily metric. It is NOT a policy object — it records
|
|
123
|
+
# and answers `ids` / `ungrounded` / `lines`; it never decides.
|
|
124
|
+
class EvidenceLedger
|
|
125
|
+
# Oldest-evicted cap: a session that outlives it needs a real cap or the row
|
|
126
|
+
# grows forever.
|
|
127
|
+
MAX_IDS = 1_000
|
|
128
|
+
|
|
129
|
+
def initialize(store: nil, session_id: nil)
|
|
130
|
+
@store = store
|
|
131
|
+
@session_id = session_id
|
|
132
|
+
@ids = []
|
|
133
|
+
@ungrounded = 0
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# The in-memory accumulator (the envelope appends, the validator/enforcer
|
|
137
|
+
# read the union). The PERSISTED list is appended on flush (Executor, stage 8)
|
|
138
|
+
# — the envelope never blocks on the store.
|
|
139
|
+
def record(ids)
|
|
140
|
+
@ids.concat(Array(ids).map(&:to_s).reject(&:empty?))
|
|
141
|
+
self
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# -> the effective set for THIS turn: persisted session evidence (loaded at
|
|
145
|
+
# build) + the turn's new ids, deduped, capped.
|
|
146
|
+
def ids
|
|
147
|
+
(session_ids + @ids).uniq.last(MAX_IDS)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
attr_reader :ungrounded
|
|
151
|
+
|
|
152
|
+
def ungrounded_count(claim)
|
|
153
|
+
@ungrounded += 1
|
|
154
|
+
claim
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# -> self, flushed to the session record. Idempotent. A store OR not-found
|
|
158
|
+
# failure is swallowed (evidence is audit — it must never fail a committed
|
|
159
|
+
# turn; a session purged mid-turn by forget_customer/session_purge reads as
|
|
160
|
+
# "nothing to append", never an explosion).
|
|
161
|
+
def flush!
|
|
162
|
+
return self unless @store && @session_id
|
|
163
|
+
|
|
164
|
+
@store.append_evidence(@session_id, ids: @ids, ungrounded: @ungrounded)
|
|
165
|
+
@ids = []
|
|
166
|
+
@ungrounded = 0
|
|
167
|
+
self
|
|
168
|
+
rescue Insika::Error
|
|
169
|
+
self
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
private
|
|
173
|
+
|
|
174
|
+
def session_ids
|
|
175
|
+
return [] unless @store && @session_id
|
|
176
|
+
|
|
177
|
+
session = @store.find(@session_id)
|
|
178
|
+
Array(session&.evidence&.fetch("ids", [])).map(&:to_s)
|
|
179
|
+
rescue Insika::NotFoundError
|
|
180
|
+
[]
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|