insika 0.0.1 → 0.2.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 +361 -0
- data/LICENSE +21 -0
- data/README.md +136 -2
- data/bin/insika +366 -0
- data/docs/AGENTS.md +618 -0
- data/docs/ARCHITECTURE.md +333 -0
- data/docs/BENCHMARK.md +114 -0
- data/docs/CHANNELS.md +453 -0
- data/docs/CONTEXT.md +117 -0
- data/docs/DEPLOY.md +354 -0
- data/docs/EMBEDDING.md +198 -0
- data/docs/EVALS.md +273 -0
- data/docs/LOADTEST.md +232 -0
- data/docs/OBSERVABILITY.md +374 -0
- data/docs/PLUGINS.md +211 -0
- data/docs/REFINEMENT.md +477 -0
- data/docs/RELEASING.md +70 -0
- data/docs/RUNNING-LOCAL.md +153 -0
- data/docs/SANDBOX.md +114 -0
- data/docs/SECURITY.md +375 -0
- data/docs/SKILLS.md +284 -0
- data/docs/TOOLS.md +302 -0
- data/docs/WHY.md +137 -0
- data/docs/WORKFLOWS.md +225 -0
- data/docs/build.md +14 -0
- data/docs/index.md +68 -0
- data/docs/onboarding/start.md +126 -0
- data/docs/operate.md +12 -0
- data/docs/ship.md +10 -0
- data/docs/understand.md +10 -0
- data/lib/insika/agent_file_store.rb +125 -0
- data/lib/insika/agent_profile.rb +255 -0
- data/lib/insika/alert_dispatcher.rb +139 -0
- data/lib/insika/allowlist.rb +28 -0
- data/lib/insika/baseline_store.rb +74 -0
- data/lib/insika/budget_ledger.rb +135 -0
- data/lib/insika/capability/resolved_tool.rb +34 -0
- data/lib/insika/capability_registry.rb +112 -0
- data/lib/insika/channel_delivery.rb +153 -0
- data/lib/insika/channel_registry.rb +30 -0
- data/lib/insika/channels/relay.rb +178 -0
- data/lib/insika/channels/web/widget.js +283 -0
- data/lib/insika/channels/web.rb +211 -0
- data/lib/insika/channels/webhook.rb +58 -0
- data/lib/insika/chat_builder.rb +303 -0
- data/lib/insika/checkpoint.rb +13 -0
- data/lib/insika/checkpoint_store.rb +153 -0
- data/lib/insika/circuit_state.rb +114 -0
- data/lib/insika/coercion.rb +58 -0
- data/lib/insika/command.rb +32 -0
- data/lib/insika/command_bus.rb +39 -0
- data/lib/insika/commands/agent_payload.rb +43 -0
- data/lib/insika/commands/approve_action.rb +46 -0
- data/lib/insika/commands/cancel_task.rb +33 -0
- data/lib/insika/commands/create_agent.rb +54 -0
- data/lib/insika/commands/create_session.rb +67 -0
- data/lib/insika/commands/delete_agent.rb +33 -0
- data/lib/insika/commands/delete_agent_file.rb +50 -0
- data/lib/insika/commands/delete_data_tool.rb +33 -0
- data/lib/insika/commands/delete_llm_provider.rb +36 -0
- data/lib/insika/commands/delete_mcp.rb +30 -0
- data/lib/insika/commands/delete_skill.rb +43 -0
- data/lib/insika/commands/delete_system_file.rb +29 -0
- data/lib/insika/commands/gate_refinement.rb +245 -0
- data/lib/insika/commands/import_mcp_tools.rb +48 -0
- data/lib/insika/commands/import_tools.rb +81 -0
- data/lib/insika/commands/issue_tenant_token.rb +41 -0
- data/lib/insika/commands/memory_add_note.rb +32 -0
- data/lib/insika/commands/memory_forget_fact.rb +32 -0
- data/lib/insika/commands/memory_put_fact.rb +35 -0
- data/lib/insika/commands/pause_task.rb +29 -0
- data/lib/insika/commands/resolve_refinement.rb +126 -0
- data/lib/insika/commands/restore_agent_file.rb +36 -0
- data/lib/insika/commands/restore_data_tool.rb +34 -0
- data/lib/insika/commands/restore_system_file.rb +31 -0
- data/lib/insika/commands/resume_task.rb +85 -0
- data/lib/insika/commands/revoke_token.rb +39 -0
- data/lib/insika/commands/rotate_tenant_token.rb +43 -0
- data/lib/insika/commands/run_refinement.rb +133 -0
- data/lib/insika/commands/send_message.rb +150 -0
- data/lib/insika/commands/set_agent_tools.rb +39 -0
- data/lib/insika/commands/set_skill_agents.rb +112 -0
- data/lib/insika/commands/trigger_workflow.rb +80 -0
- data/lib/insika/commands/update_agent.rb +49 -0
- data/lib/insika/commands/update_settings.rb +33 -0
- data/lib/insika/commands/upsert_llm_provider.rb +34 -0
- data/lib/insika/commands/upsert_mcp.rb +32 -0
- data/lib/insika/commands/write_agent_file.rb +57 -0
- data/lib/insika/commands/write_data_tool.rb +43 -0
- data/lib/insika/commands/write_golden.rb +58 -0
- data/lib/insika/commands/write_skill.rb +60 -0
- data/lib/insika/commands/write_system_file.rb +31 -0
- data/lib/insika/config_store.rb +89 -0
- data/lib/insika/context/builder.rb +166 -0
- data/lib/insika/context/catalog_provider.rb +23 -0
- data/lib/insika/context/fragment.rb +43 -0
- data/lib/insika/context/priority.rb +30 -0
- data/lib/insika/context/provider.rb +19 -0
- data/lib/insika/context/providers/memory.rb +60 -0
- data/lib/insika/context/providers/prompt.rb +105 -0
- data/lib/insika/context/providers/request.rb +32 -0
- data/lib/insika/context/providers/session.rb +123 -0
- data/lib/insika/context/providers/skill.rb +24 -0
- data/lib/insika/context/providers/skill_trigger.rb +128 -0
- data/lib/insika/context/providers/tool_search.rb +20 -0
- data/lib/insika/context_trace_store.rb +92 -0
- data/lib/insika/delegation_store.rb +153 -0
- data/lib/insika/doctor.rb +539 -0
- data/lib/insika/dsl/definition.rb +55 -0
- data/lib/insika/dsl/runtime.rb +382 -0
- data/lib/insika/dsl/server_boot.rb +98 -0
- data/lib/insika/dsl/system.rb +93 -0
- data/lib/insika/dsl/workflow_adapter.rb +59 -0
- data/lib/insika/dsl.rb +364 -0
- data/lib/insika/edge_limiter.rb +268 -0
- data/lib/insika/egress_guard.rb +75 -0
- data/lib/insika/env_schema.rb +249 -0
- data/lib/insika/errors.rb +201 -0
- data/lib/insika/evals/assertions.rb +247 -0
- data/lib/insika/evals/baseline.rb +69 -0
- data/lib/insika/evals/golden.rb +172 -0
- data/lib/insika/evals/judge.rb +225 -0
- data/lib/insika/evals/pairwise.rb +178 -0
- data/lib/insika/evals/report.rb +115 -0
- data/lib/insika/evals/runner.rb +141 -0
- data/lib/insika/evals/transport.rb +178 -0
- data/lib/insika/event.rb +18 -0
- data/lib/insika/event_stream.rb +132 -0
- data/lib/insika/executor.rb +1995 -0
- data/lib/insika/frontmatter.rb +42 -0
- data/lib/insika/golden_store.rb +145 -0
- data/lib/insika/hooks.rb +48 -0
- data/lib/insika/http_client.rb +63 -0
- data/lib/insika/inbound_log.rb +84 -0
- data/lib/insika/llm_configurator.rb +99 -0
- data/lib/insika/llm_provider_store.rb +83 -0
- data/lib/insika/loop_detector.rb +143 -0
- data/lib/insika/mcp_http_client.rb +67 -0
- data/lib/insika/mcp_store.rb +115 -0
- data/lib/insika/mcp_tool_ingestor.rb +143 -0
- data/lib/insika/memory_store.rb +93 -0
- data/lib/insika/message_origin.rb +76 -0
- data/lib/insika/middleware.rb +36 -0
- data/lib/insika/model_policy.rb +52 -0
- data/lib/insika/model_resolver.rb +176 -0
- data/lib/insika/model_selection.rb +115 -0
- data/lib/insika/onboarding.rb +208 -0
- data/lib/insika/outbox_store.rb +166 -0
- data/lib/insika/overlay_tool_registry.rb +102 -0
- data/lib/insika/pack.rb +102 -0
- data/lib/insika/pack_importer.rb +123 -0
- data/lib/insika/pending_action_store.rb +120 -0
- data/lib/insika/plugin/loader.rb +356 -0
- data/lib/insika/plugin.rb +35 -0
- data/lib/insika/policy/engine.rb +83 -0
- data/lib/insika/policy/policy.rb +120 -0
- data/lib/insika/policy_registry.rb +23 -0
- data/lib/insika/profile_source.rb +143 -0
- data/lib/insika/prompt_catalog.rb +61 -0
- data/lib/insika/provider_error_classifier.rb +160 -0
- data/lib/insika/queue_policy.rb +167 -0
- data/lib/insika/recovery.rb +168 -0
- data/lib/insika/refinement/candidate.rb +159 -0
- data/lib/insika/refinement/evidence_collector.rb +371 -0
- data/lib/insika/refinement/gate.rb +234 -0
- data/lib/insika/refinement/panel.rb +222 -0
- data/lib/insika/refinement/proposer.rb +262 -0
- data/lib/insika/refinement_store.rb +295 -0
- data/lib/insika/registry.rb +59 -0
- data/lib/insika/reliability.rb +185 -0
- data/lib/insika/safety/config.rb +109 -0
- data/lib/insika/safety/detectors.rb +176 -0
- data/lib/insika/safety/factory.rb +102 -0
- data/lib/insika/safety/input_guardrail.rb +102 -0
- data/lib/insika/safety/moderator.rb +94 -0
- data/lib/insika/safety/output_filter.rb +79 -0
- data/lib/insika/safety/output_validator.rb +101 -0
- data/lib/insika/safety/safe_responses.rb +47 -0
- data/lib/insika/sandbox/boundary.rb +93 -0
- data/lib/insika/sandbox/docker.rb +74 -0
- data/lib/insika/sandbox/local.rb +33 -0
- data/lib/insika/sandbox/runner.rb +80 -0
- data/lib/insika/sandbox.rb +85 -0
- data/lib/insika/schema_guard.rb +147 -0
- data/lib/insika/secret_masking.rb +34 -0
- data/lib/insika/server/a2a/agent_card.rb +27 -0
- data/lib/insika/server/a2a/app.rb +112 -0
- data/lib/insika/server/a2a/client.rb +101 -0
- data/lib/insika/server/a2a/errors.rb +32 -0
- data/lib/insika/server/a2a/http.rb +42 -0
- data/lib/insika/server/a2a/message.rb +27 -0
- data/lib/insika/server/a2a/protocol.rb +45 -0
- data/lib/insika/server/a2a/remotes.rb +25 -0
- data/lib/insika/server/a2a/task_projection.rb +40 -0
- data/lib/insika/server/app.rb +1022 -0
- data/lib/insika/server/boot.rb +119 -0
- data/lib/insika/server/rack_app.rb +118 -0
- data/lib/insika/server/responses.rb +165 -0
- data/lib/insika/server/sse_body.rb +96 -0
- data/lib/insika/server/tenant_auth.rb +61 -0
- data/lib/insika/session_actor.rb +162 -0
- data/lib/insika/session_store.rb +143 -0
- data/lib/insika/settings_store.rb +154 -0
- data/lib/insika/shutdown.rb +125 -0
- data/lib/insika/skill_catalog.rb +220 -0
- data/lib/insika/skill_store.rb +127 -0
- data/lib/insika/steer_injector.rb +110 -0
- data/lib/insika/store.rb +52 -0
- data/lib/insika/stores/memory.rb +123 -0
- data/lib/insika/stores/sqlite.rb +183 -0
- data/lib/insika/studio/app.rb +1693 -0
- data/lib/insika/studio/assets/dist/application.css +1 -0
- data/lib/insika/studio/assets/dist/application.js +70 -0
- data/lib/insika/studio/forms.rb +335 -0
- data/lib/insika/studio/nav_icons.rb +31 -0
- data/lib/insika/studio/views/_message.erb +44 -0
- data/lib/insika/studio/views/agent_detail.erb +285 -0
- data/lib/insika/studio/views/agents.erb +63 -0
- data/lib/insika/studio/views/approvals.erb +41 -0
- data/lib/insika/studio/views/chats.erb +34 -0
- data/lib/insika/studio/views/evals.erb +83 -0
- data/lib/insika/studio/views/home.erb +72 -0
- data/lib/insika/studio/views/layout.erb +94 -0
- data/lib/insika/studio/views/login.erb +17 -0
- data/lib/insika/studio/views/mcp.erb +91 -0
- data/lib/insika/studio/views/not_found.erb +5 -0
- data/lib/insika/studio/views/playground.erb +47 -0
- data/lib/insika/studio/views/refinement.erb +234 -0
- data/lib/insika/studio/views/session.erb +137 -0
- data/lib/insika/studio/views/settings.erb +168 -0
- data/lib/insika/studio/views/skills.erb +141 -0
- data/lib/insika/studio/views/system_files.erb +65 -0
- data/lib/insika/studio/views/task.erb +105 -0
- data/lib/insika/studio/views/tasks.erb +33 -0
- data/lib/insika/studio/views/tool_edit.erb +107 -0
- data/lib/insika/studio/views/tools.erb +89 -0
- data/lib/insika/subagent_graph.rb +96 -0
- data/lib/insika/system_file_store.rb +96 -0
- data/lib/insika/task_actor.rb +128 -0
- data/lib/insika/task_store.rb +250 -0
- data/lib/insika/telemetry/pricing.rb +104 -0
- data/lib/insika/telemetry/recorder.rb +228 -0
- data/lib/insika/telemetry.rb +127 -0
- data/lib/insika/testing/store_contract.rb +270 -0
- data/lib/insika/tick.rb +122 -0
- data/lib/insika/token_estimator.rb +16 -0
- data/lib/insika/token_store.rb +168 -0
- data/lib/insika/tool_assembly.rb +140 -0
- data/lib/insika/tool_catalog.rb +89 -0
- data/lib/insika/tool_definition.rb +518 -0
- data/lib/insika/tool_envelope.rb +140 -0
- data/lib/insika/tool_manifest.rb +218 -0
- data/lib/insika/tool_output_compressor.rb +100 -0
- data/lib/insika/tool_registry.rb +21 -0
- data/lib/insika/tool_store.rb +135 -0
- data/lib/insika/tool_trace_store.rb +92 -0
- data/lib/insika/tools/a2a_remote.rb +48 -0
- data/lib/insika/tools/agent_enum.rb +68 -0
- data/lib/insika/tools/concurrency.rb +54 -0
- data/lib/insika/tools/data_defined_tool.rb +219 -0
- data/lib/insika/tools/load_skill.rb +99 -0
- data/lib/insika/tools/remember.rb +53 -0
- data/lib/insika/tools/stuck_signal.rb +44 -0
- data/lib/insika/tools/subagent.rb +75 -0
- data/lib/insika/tools/subagents.rb +77 -0
- data/lib/insika/tools/tool_search.rb +94 -0
- data/lib/insika/turn_output.rb +139 -0
- data/lib/insika/turn_state.rb +162 -0
- data/lib/insika/turn_timing.rb +56 -0
- data/lib/insika/usage_ledger.rb +47 -0
- data/lib/insika/version.rb +3 -1
- data/lib/insika/wiring/graph.rb +249 -0
- data/lib/insika/workflow.rb +185 -0
- data/lib/insika/workflow_registry.rb +33 -0
- data/lib/insika.rb +220 -4
- metadata +412 -8
data/bin/insika
ADDED
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# `insika` CLI — strict config + doctor --fix. The operator's
|
|
5
|
+
# DX front door to the strict-config discipline:
|
|
6
|
+
#
|
|
7
|
+
# insika doctor [--fix] [--json] diagnose the deployment config; --fix applies
|
|
8
|
+
# the safe autofixes; exits non-zero on any error
|
|
9
|
+
# insika env [--json] list the known config keys + current (masked) values
|
|
10
|
+
# insika version print the engine version
|
|
11
|
+
# insika help this text
|
|
12
|
+
#
|
|
13
|
+
# It reads the SAME durable backend the server does (INSIKA_DB -> SQLite, else
|
|
14
|
+
# ephemeral memory), building only the config stores it needs — no full app boot, no
|
|
15
|
+
# DEEPSEEK, no seeding. So it is safe to run against a live deployment's volume.
|
|
16
|
+
|
|
17
|
+
require "optparse"
|
|
18
|
+
require_relative "../lib/insika"
|
|
19
|
+
|
|
20
|
+
module Insika
|
|
21
|
+
module CLI
|
|
22
|
+
module_function
|
|
23
|
+
|
|
24
|
+
def main(argv)
|
|
25
|
+
command = argv.shift || "help"
|
|
26
|
+
case command
|
|
27
|
+
when "doctor" then doctor(argv)
|
|
28
|
+
when "env" then env(argv)
|
|
29
|
+
when "refine" then refine(argv)
|
|
30
|
+
when "evals:import" then evals_import(argv)
|
|
31
|
+
when "evals:export" then evals_export(argv)
|
|
32
|
+
when "evals:baseline" then evals_baseline(argv)
|
|
33
|
+
when "version", "--version", "-v" then puts "insika #{Insika::VERSION}"
|
|
34
|
+
when "help", "--help", "-h" then puts usage
|
|
35
|
+
else
|
|
36
|
+
warn "unknown command: #{command}\n\n#{usage}"
|
|
37
|
+
exit 2
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def usage
|
|
42
|
+
<<~TXT
|
|
43
|
+
insika #{Insika::VERSION} — agent engine CLI
|
|
44
|
+
|
|
45
|
+
Usage: insika <command> [options]
|
|
46
|
+
|
|
47
|
+
Commands:
|
|
48
|
+
doctor [--fix] [--json] diagnose the deployment configuration
|
|
49
|
+
env [--json] list known config keys + current (masked) values
|
|
50
|
+
refine --agent ID [...] report what broke in an agent's real traffic
|
|
51
|
+
evals:import [--dir D] load the golden corpus into the store (editable in Studio)
|
|
52
|
+
evals:export [--dir D] write the stored cases back out as YAML
|
|
53
|
+
evals:baseline <sub> the accepted state per agent: show | import | export
|
|
54
|
+
version print the engine version
|
|
55
|
+
help show this help
|
|
56
|
+
|
|
57
|
+
Environment:
|
|
58
|
+
INSIKA_DB SQLite path (durable). Unset -> ephemeral memory.
|
|
59
|
+
INSIKA_CONFIG_STRICT when truthy, the server refuses boot on any finding.
|
|
60
|
+
TXT
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# insika doctor [--fix] [--json]
|
|
64
|
+
def doctor(argv)
|
|
65
|
+
opts = { fix: false, json: false }
|
|
66
|
+
OptionParser.new do |o|
|
|
67
|
+
o.banner = "Usage: insika doctor [--fix] [--json]"
|
|
68
|
+
o.on("--fix", "apply the safe autofixes, then re-diagnose") { opts[:fix] = true }
|
|
69
|
+
o.on("--json", "emit the report as JSON") { opts[:json] = true }
|
|
70
|
+
end.parse!(argv)
|
|
71
|
+
|
|
72
|
+
doc = build_doctor
|
|
73
|
+
report = opts[:fix] ? doc.fix!.last : doc.run
|
|
74
|
+
|
|
75
|
+
if opts[:json]
|
|
76
|
+
require "json"
|
|
77
|
+
puts JSON.pretty_generate(report.to_h)
|
|
78
|
+
else
|
|
79
|
+
puts report.to_s(color: $stdout.tty?)
|
|
80
|
+
end
|
|
81
|
+
exit(report.ok? ? 0 : 1)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# insika env [--json]
|
|
85
|
+
def env(argv)
|
|
86
|
+
json = false
|
|
87
|
+
OptionParser.new do |o|
|
|
88
|
+
o.banner = "Usage: insika env [--json]"
|
|
89
|
+
o.on("--json", "emit as JSON") { json = true }
|
|
90
|
+
end.parse!(argv)
|
|
91
|
+
|
|
92
|
+
rows = Insika::EnvSchema.known_specs.map do |s|
|
|
93
|
+
raw = ENV[s.name]
|
|
94
|
+
{ "key" => s.name, "type" => s.type.to_s, "secret" => s.secret?,
|
|
95
|
+
"set" => !raw.nil?, "value" => display_value(s, raw), "description" => s.description }
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
if json
|
|
99
|
+
require "json"
|
|
100
|
+
puts JSON.pretty_generate(rows)
|
|
101
|
+
else
|
|
102
|
+
rows.each do |r|
|
|
103
|
+
state = r["set"] ? r["value"] : "(unset)"
|
|
104
|
+
puts format("%-30s %-8s %s", r["key"], r["type"], state)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# insika refine --agent ID [--last-sessions N] [--since ISO8601] [--full] [--json]
|
|
110
|
+
#
|
|
111
|
+
# reads the agent's own traffic and records a ranked failure
|
|
112
|
+
# report. Read-only with respect to the agent — it writes only the run record.
|
|
113
|
+
# There is no scheduler in the engine: this command and the Studio button
|
|
114
|
+
# are how a run starts, and a cron that wants one calls this.
|
|
115
|
+
def refine(argv)
|
|
116
|
+
opts = { json: false, full: false }
|
|
117
|
+
OptionParser.new do |o|
|
|
118
|
+
o.banner = "Usage: insika refine --agent ID [options]"
|
|
119
|
+
o.on("--agent ID", "agent whose traffic to read (required)") { |v| opts[:agent] = v }
|
|
120
|
+
o.on("--last-sessions N", Integer, "read the N most recent conversations") { |v| opts[:last_sessions] = v }
|
|
121
|
+
o.on("--since ISO8601", "read only turns from this instant on") { |v| opts[:since] = v }
|
|
122
|
+
o.on("--full", "ignore the previous run and read the whole window") { opts[:full] = true }
|
|
123
|
+
o.on("--exclude PREFIXES", "comma-separated session-id prefixes to drop (e.g. loadtest-,debug-)") do |v|
|
|
124
|
+
opts[:exclude_sessions] = v.split(",").map(&:strip).reject(&:empty?)
|
|
125
|
+
end
|
|
126
|
+
o.on("--json", "emit the report as JSON") { opts[:json] = true }
|
|
127
|
+
end.parse!(argv)
|
|
128
|
+
|
|
129
|
+
abort "insika refine: --agent is required" if Insika::Coercion.blank?(opts[:agent])
|
|
130
|
+
|
|
131
|
+
# A misspelled agent or a bad window is operator error, not a bug: one line and
|
|
132
|
+
# exit 2 (like an unknown command), never a Ruby backtrace. Exit 1 stays what it
|
|
133
|
+
# means everywhere else in this CLI — "it ran and found trouble".
|
|
134
|
+
run = begin
|
|
135
|
+
run_refinement(opts)
|
|
136
|
+
rescue Insika::Error, ArgumentError => e
|
|
137
|
+
warn "insika refine: #{e.message}"
|
|
138
|
+
exit 2
|
|
139
|
+
end
|
|
140
|
+
if opts[:json]
|
|
141
|
+
require "json"
|
|
142
|
+
puts JSON.pretty_generate(run.to_h)
|
|
143
|
+
else
|
|
144
|
+
puts refine_report(run)
|
|
145
|
+
end
|
|
146
|
+
exit(run.status == :failed ? 1 : 0)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# insika evals:import [--dir evals/golden] [--keep-existing]
|
|
150
|
+
#
|
|
151
|
+
# The corpus on disk is the SEED and the export format; the store is what a
|
|
152
|
+
# deployment runs and what the Studio edits. Importing is how a
|
|
153
|
+
# fresh deploy gets the cases, and how a broken edit is undone.
|
|
154
|
+
def evals_import(argv)
|
|
155
|
+
opts = { dir: File.expand_path("../evals/golden", __dir__), overwrite: true }
|
|
156
|
+
OptionParser.new do |o|
|
|
157
|
+
o.banner = "Usage: insika evals:import [--dir DIR] [--keep-existing]"
|
|
158
|
+
o.on("--dir DIR", "corpus directory (default evals/golden)") { |v| opts[:dir] = v }
|
|
159
|
+
o.on("--keep-existing", "do not overwrite a case already in the store") { opts[:overwrite] = false }
|
|
160
|
+
end.parse!(argv)
|
|
161
|
+
|
|
162
|
+
ids = golden_store.import_dir(opts[:dir], overwrite: opts[:overwrite])
|
|
163
|
+
puts "imported #{ids.size} case(s) from #{opts[:dir]}"
|
|
164
|
+
puts ids.sort.map { |id| " #{id}" }.join("\n") unless ids.empty?
|
|
165
|
+
rescue Insika::Evals::GoldenLoader::InvalidGolden => e
|
|
166
|
+
warn "insika evals:import: #{e.message}"
|
|
167
|
+
exit 2
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# insika evals:export [--dir DIR] — the store back to YAML, one file per case under
|
|
171
|
+
# <dir>/<agent>/<id>.yml. Same format `evals:import` reads: no converter to keep
|
|
172
|
+
# honest, so a case authored in the Studio can be reviewed in a pull request.
|
|
173
|
+
def evals_export(argv)
|
|
174
|
+
opts = { dir: File.expand_path("../evals/golden", __dir__), force: false }
|
|
175
|
+
OptionParser.new do |o|
|
|
176
|
+
o.banner = "Usage: insika evals:export [--dir DIR] [--force]"
|
|
177
|
+
o.on("--dir DIR", "destination (default evals/golden)") { |v| opts[:dir] = v }
|
|
178
|
+
o.on("--force", "rewrite an existing corpus (drops the files' comments)") { opts[:force] = true }
|
|
179
|
+
end.parse!(argv)
|
|
180
|
+
|
|
181
|
+
paths = golden_store.export_dir(opts[:dir], force: opts[:force])
|
|
182
|
+
puts "exported #{paths.size} case(s) to #{opts[:dir]}"
|
|
183
|
+
rescue Insika::ValidationError => e
|
|
184
|
+
warn "insika evals:export: #{e.message}"
|
|
185
|
+
exit 2
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# insika evals:baseline show|import|export [--file evals/baseline.json]
|
|
189
|
+
#
|
|
190
|
+
# The accepted state of a golden set used to live ONLY in `evals/baseline.json`,
|
|
191
|
+
# which is fine for the CLI (it runs from a checkout) and useless for the refinement
|
|
192
|
+
# gate, which runs inside a deployment and has to know what "no
|
|
193
|
+
# regression" means without one. So it became a per-agent record.
|
|
194
|
+
#
|
|
195
|
+
# `import` splits the one file into those records, resolving each case's agent from
|
|
196
|
+
# the golden store — which is why the corpus has to be imported first. A case the
|
|
197
|
+
# store does not know is REPORTED, not guessed at: silently dropping it would shrink
|
|
198
|
+
# the accepted state, and a smaller baseline is a weaker gate.
|
|
199
|
+
def evals_baseline(argv)
|
|
200
|
+
sub = argv.shift || "show"
|
|
201
|
+
opts = { file: File.expand_path("../evals/baseline.json", __dir__) }
|
|
202
|
+
OptionParser.new do |o|
|
|
203
|
+
o.banner = "Usage: insika evals:baseline show|import|export [--file PATH]"
|
|
204
|
+
o.on("--file PATH", "the baseline JSON (default evals/baseline.json)") { |v| opts[:file] = v }
|
|
205
|
+
end.parse!(argv)
|
|
206
|
+
|
|
207
|
+
case sub
|
|
208
|
+
when "show" then baseline_show
|
|
209
|
+
when "import" then baseline_import(opts[:file])
|
|
210
|
+
when "export" then baseline_export(opts[:file])
|
|
211
|
+
else
|
|
212
|
+
warn "insika evals:baseline: unknown subcommand '#{sub}' (show|import|export)"
|
|
213
|
+
exit 2
|
|
214
|
+
end
|
|
215
|
+
rescue Insika::Error => e
|
|
216
|
+
warn "insika evals:baseline: #{e.message}"
|
|
217
|
+
exit 2
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def baseline_show
|
|
221
|
+
store = baseline_store
|
|
222
|
+
agents = store.agents
|
|
223
|
+
return puts("no baseline recorded — the refinement gate cannot run (insika evals:baseline import)") if agents.empty?
|
|
224
|
+
|
|
225
|
+
agents.sort.each do |agent|
|
|
226
|
+
record = store.get(agent)
|
|
227
|
+
puts format("%-24s %3d case(s) recorded %s", agent, (record["cases"] || {}).size, record["at"])
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def baseline_import(path)
|
|
232
|
+
raise Insika::ValidationError, "no such file: #{path}" unless File.exist?(path)
|
|
233
|
+
|
|
234
|
+
blob = JSON.parse(File.read(path))
|
|
235
|
+
by_agent = Hash.new { |h, k| h[k] = {} }
|
|
236
|
+
unknown = []
|
|
237
|
+
(blob["cases"] || {}).each do |case_id, entry|
|
|
238
|
+
golden = golden_store.find(case_id)
|
|
239
|
+
next unknown << case_id if golden.nil?
|
|
240
|
+
|
|
241
|
+
by_agent[golden.agent][case_id] = entry
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
by_agent.each { |agent, cases| baseline_store.put(agent, { "at" => blob["at"], "cases" => cases }) }
|
|
245
|
+
puts "imported #{by_agent.values.sum(&:size)} case(s) into #{by_agent.size} agent baseline(s) from #{path}"
|
|
246
|
+
by_agent.keys.sort.each { |a| puts " #{a} (#{by_agent[a].size})" }
|
|
247
|
+
return if unknown.empty?
|
|
248
|
+
|
|
249
|
+
warn " #{unknown.size} case(s) are not in the golden store and were NOT recorded: #{unknown.sort.join(', ')}"
|
|
250
|
+
warn " run `insika evals:import` first, then import the baseline again"
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def baseline_export(path)
|
|
254
|
+
store = baseline_store
|
|
255
|
+
merged = store.agents.each_with_object({}) { |a, acc| acc.merge!(store.get(a)["cases"] || {}) }
|
|
256
|
+
at = store.agents.filter_map { |a| store.get(a)["at"] }.max
|
|
257
|
+
File.write(path, JSON.pretty_generate({ "at" => at, "cases" => merged }))
|
|
258
|
+
puts "exported #{merged.size} case(s) from #{store.agents.size} agent baseline(s) to #{path}"
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# -- helpers -------------------------------------------------------
|
|
262
|
+
|
|
263
|
+
def baseline_store
|
|
264
|
+
backend = Insika::Wiring::Graph.backend_from_env
|
|
265
|
+
Insika::BaselineStore.new(config_store: Insika::ConfigStore.new(store: backend))
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def golden_store
|
|
269
|
+
backend = Insika::Wiring::Graph.backend_from_env
|
|
270
|
+
Insika::GoldenStore.new(config_store: Insika::ConfigStore.new(store: backend))
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
# Builds the handler over the real durable backend — same "no app boot" rule as
|
|
274
|
+
# `doctor`: no DEEPSEEK, no seeding, no reactor. Safe against a live volume,
|
|
275
|
+
# because the only write is the run record.
|
|
276
|
+
def run_refinement(opts)
|
|
277
|
+
backend = Insika::Wiring::Graph.backend_from_env
|
|
278
|
+
config_store = Insika::ConfigStore.new(store: backend)
|
|
279
|
+
profiles = Insika::StoredProfileSource.new(config_store: config_store)
|
|
280
|
+
handler = Insika::Commands::RunRefinement.new(
|
|
281
|
+
profiles: profiles,
|
|
282
|
+
refinement_store: Insika::RefinementStore.new(store: backend),
|
|
283
|
+
collector: Insika::Refinement::EvidenceCollector.new(
|
|
284
|
+
task_store: Insika::TaskStore.new(store: backend),
|
|
285
|
+
session_store: Insika::SessionStore.new(store: backend),
|
|
286
|
+
tool_trace_store: Insika::ToolTraceStore.new(store: backend),
|
|
287
|
+
profiles: profiles,
|
|
288
|
+
settings_store: Insika::SettingsStore.new(config_store: config_store)
|
|
289
|
+
),
|
|
290
|
+
event_stream: Insika::EventStream.new
|
|
291
|
+
)
|
|
292
|
+
handler.call(Insika::Command.build(:run_refinement, {
|
|
293
|
+
agent: opts[:agent], since: opts[:since],
|
|
294
|
+
last_sessions: opts[:last_sessions], full: opts[:full],
|
|
295
|
+
exclude_sessions: opts[:exclude_sessions]
|
|
296
|
+
}.compact, transport: :cli))
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
INDENT = " " * 23 # aligns the provenance line under the finding's title
|
|
300
|
+
|
|
301
|
+
def refine_report(run)
|
|
302
|
+
head = "#{run.agent_id} — #{run.status} (#{refine_window(run.window)})"
|
|
303
|
+
return "#{head}\n #{run.error}" if run.status == :failed
|
|
304
|
+
return "#{head}\n nothing found in this window" if run.findings.empty?
|
|
305
|
+
|
|
306
|
+
rows = run.findings.map do |f|
|
|
307
|
+
line = format(" %-14s ×%-4d %s", f["kind"], f["count"], f["title"])
|
|
308
|
+
line += "\n#{INDENT}#{f['detail']}" if f["detail"]
|
|
309
|
+
line += "\n#{INDENT}sessions: #{Array(f['sessions']).join(', ')}" if Array(f["sessions"]).any?
|
|
310
|
+
line
|
|
311
|
+
end
|
|
312
|
+
([head] + rows).join("\n")
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
# An empty window in the record means "the collector's own default" — resolve it
|
|
316
|
+
# for display instead of printing the word "default".
|
|
317
|
+
def refine_window(window)
|
|
318
|
+
return "since #{window['since']}" if window["since"]
|
|
319
|
+
|
|
320
|
+
n = window["last_sessions"] || Insika::Refinement::EvidenceCollector::DEFAULT_WINDOW
|
|
321
|
+
"last #{n} session(s)"
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
# Config stores over the real durable backend — no app boot / no DEEPSEEK.
|
|
325
|
+
def build_doctor
|
|
326
|
+
backend = Insika::Wiring::Graph.backend_from_env
|
|
327
|
+
config_store = Insika::ConfigStore.new(store: backend)
|
|
328
|
+
Insika::Doctor.new(
|
|
329
|
+
env: ENV,
|
|
330
|
+
settings_store: Insika::SettingsStore.new(config_store: config_store),
|
|
331
|
+
llm_provider_store: Insika::LLMProviderStore.new(config_store: config_store),
|
|
332
|
+
tool_store: Insika::ToolStore.new(config_store: config_store),
|
|
333
|
+
# Prompt-file corruption is invisible from the outside (the file is there, the
|
|
334
|
+
# agent answers) — the sweep is the only thing that names it.
|
|
335
|
+
agent_file_store: Insika::AgentFileStore.new(config_store: config_store),
|
|
336
|
+
# Skills + profiles: the eagerness/drift checks are about the RELATION between
|
|
337
|
+
# a skill and the agents holding it, so neither half alone is enough. The
|
|
338
|
+
# catalog brings the DISK seeds into the sweep (deploy/skills in a deployment,
|
|
339
|
+
# skills/ in the minimal wiring — whichever exists here); without it a stale
|
|
340
|
+
# `eager:` or a Natura-in-shared-body sitting in a seed pack is invisible.
|
|
341
|
+
skill_store: Insika::SkillStore.new(config_store: config_store),
|
|
342
|
+
skill_catalog: Insika::SkillCatalog.new(skill_roots),
|
|
343
|
+
profile_source: Insika::StoredProfileSource.new(config_store: config_store),
|
|
344
|
+
backend: backend
|
|
345
|
+
)
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
# The conventional seed roots, relative to where the operator runs the CLI —
|
|
349
|
+
# same "no app boot" rule as the rest of `doctor`, so the composition root that
|
|
350
|
+
# actually declares them is never loaded.
|
|
351
|
+
def skill_roots
|
|
352
|
+
%w[deploy/skills skills].map { |p| File.join(Dir.pwd, p) }.select { |p| Dir.exist?(p) }
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
def display_value(spec, raw)
|
|
356
|
+
return "(unset)" if raw.nil?
|
|
357
|
+
|
|
358
|
+
spec.secret? ? "•••• (set, #{raw.to_s.length} chars)" : raw
|
|
359
|
+
end
|
|
360
|
+
end
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
# No `if $PROGRAM_NAME == __FILE__` guard: the RubyGems wrapper `load`s this file
|
|
364
|
+
# (Gem.activate_bin_path), so under `gem install` the guard is false and the CLI
|
|
365
|
+
# would silently do nothing. This file is only ever an entrypoint.
|
|
366
|
+
Insika::CLI.main(ARGV)
|