insika 0.2.0 → 0.7.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 +325 -17
- data/README.md +52 -11
- data/bin/insika +895 -8
- data/bin/insika-router +87 -0
- data/docs/AGENTS.md +229 -163
- data/docs/API.md +56 -0
- data/docs/ARCHITECTURE.md +3 -2
- data/docs/ARTIFACTS.md +95 -0
- data/docs/BENCHMARK.md +2 -2
- data/docs/CHANNELS.md +111 -21
- data/docs/CONTEXT.md +132 -19
- data/docs/DEMO.md +80 -0
- data/docs/DEPLOY.md +75 -4
- data/docs/EMBEDDING.md +1 -1
- data/docs/EVALS.md +146 -2
- data/docs/FACTS.md +135 -0
- data/docs/HARVEST.md +116 -0
- data/docs/KNOWLEDGE.md +290 -0
- data/docs/LOADTEST.md +16 -10
- data/docs/MEDIA.md +128 -0
- data/docs/OBSERVABILITY.md +65 -6
- data/docs/OUTCOMES.md +137 -0
- data/docs/PLUGINS.md +51 -6
- data/docs/POLICY.md +216 -0
- data/docs/REFINEMENT.md +17 -12
- data/docs/RELEASING.md +27 -0
- data/docs/ROUTER.md +213 -0
- data/docs/RUNNING-LOCAL.md +6 -6
- data/docs/SCHEDULING.md +121 -0
- data/docs/SECURITY.md +79 -2
- data/docs/SKILLS.md +13 -4
- data/docs/SOAK.md +127 -0
- data/docs/TEMPLATES.md +134 -0
- data/docs/TOOLS.md +220 -27
- data/docs/WHY.md +1 -1
- data/docs/WORKFLOWS.md +2 -2
- data/docs/_includes/head_custom.html +5 -0
- data/docs/_includes/title.html +13 -0
- data/docs/_sass/color_schemes/insika.scss +32 -0
- data/docs/_sass/custom/custom.scss +199 -0
- data/docs/_sass/custom/setup.scss +26 -0
- data/docs/assets/img/favicon.svg +7 -0
- data/docs/assets/img/insika-mark.svg +7 -0
- data/docs/core-concepts.md +21 -0
- data/docs/domain.md +115 -0
- data/docs/improve.md +20 -0
- data/docs/index.md +9 -6
- data/docs/integrate.md +20 -0
- data/docs/operate.md +13 -6
- data/docs/prompts/ADD-TOOL.md +118 -0
- data/docs/prompts/DIAGNOSE-TURN.md +65 -0
- data/docs/prompts/GO-LIVE.md +138 -0
- data/docs/prompts/RUN-EXAMPLES.md +70 -0
- data/docs/reference.md +19 -0
- data/docs/ship.md +10 -2
- data/docs/start-here.md +18 -0
- data/lib/insika/agent_profile.rb +202 -10
- data/lib/insika/artifact_signing.rb +82 -0
- data/lib/insika/artifact_store.rb +160 -0
- data/lib/insika/balloon_splitter.rb +102 -0
- data/lib/insika/budget_ledger.rb +34 -3
- data/lib/insika/cache_series_store.rb +49 -0
- data/lib/insika/channel_delivery.rb +119 -14
- data/lib/insika/channels/relay.rb +77 -3
- data/lib/insika/channels/web.rb +2 -2
- data/lib/insika/chat_builder.rb +105 -2
- data/lib/insika/checkpoint_store.rb +16 -0
- data/lib/insika/commands/agent_payload.rb +3 -3
- data/lib/insika/commands/backfill_knowledge.rb +145 -0
- data/lib/insika/commands/cancel_followup.rb +49 -0
- data/lib/insika/commands/delete_artifact.rb +35 -0
- data/lib/insika/commands/delete_concept.rb +34 -0
- data/lib/insika/commands/delete_mcp.rb +6 -2
- data/lib/insika/commands/delete_tenant_data.rb +107 -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 +1 -1
- 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/refresh_mcp_tools.rb +47 -0
- data/lib/insika/commands/reject_harvest.rb +38 -0
- data/lib/insika/commands/resolve_proposal.rb +108 -0
- data/lib/insika/commands/restore_concept.rb +34 -0
- data/lib/insika/commands/revoke_contact.rb +49 -0
- data/lib/insika/commands/rollback_harvest.rb +86 -0
- data/lib/insika/commands/run_distillation.rb +186 -0
- data/lib/insika/commands/run_harvest.rb +393 -0
- data/lib/insika/commands/seed_demo_data.rb +31 -0
- data/lib/insika/commands/send_message.rb +103 -6
- data/lib/insika/commands/session_purge.rb +67 -0
- data/lib/insika/commands/upsert_mcp.rb +6 -3
- data/lib/insika/commands/write_concept.rb +57 -0
- data/lib/insika/contact_store.rb +183 -0
- data/lib/insika/context/builder.rb +21 -3
- data/lib/insika/context/fragment.rb +7 -3
- data/lib/insika/context/priority.rb +5 -0
- data/lib/insika/context/provider.rb +17 -3
- data/lib/insika/context/providers/briefing.rb +96 -0
- data/lib/insika/context/providers/knowledge.rb +108 -0
- data/lib/insika/context/providers/memory.rb +16 -7
- data/lib/insika/context/providers/prompt.rb +57 -23
- data/lib/insika/context/providers/skill.rb +2 -0
- data/lib/insika/context/providers/tool_search.rb +2 -0
- data/lib/insika/context_trace_store.rb +38 -2
- data/lib/insika/cron.rb +189 -0
- data/lib/insika/demo/agent_attrs.rb +43 -0
- data/lib/insika/demo/golden_cases.rb +81 -0
- data/lib/insika/demo/seeder.rb +336 -0
- data/lib/insika/distill.rb +224 -0
- data/lib/insika/distill_engine.rb +169 -0
- data/lib/insika/doctor.rb +889 -11
- data/lib/insika/dsl/definition.rb +3 -2
- data/lib/insika/dsl/runtime.rb +69 -82
- data/lib/insika/dsl/server_boot.rb +92 -1
- data/lib/insika/dsl/system.rb +10 -2
- data/lib/insika/dsl.rb +186 -5
- data/lib/insika/edge_limiter.rb +28 -7
- data/lib/insika/env_schema.rb +25 -3
- data/lib/insika/errors.rb +11 -0
- data/lib/insika/evals/assertions.rb +3 -2
- data/lib/insika/evals/golden.rb +41 -4
- data/lib/insika/evals/judge.rb +47 -2
- data/lib/insika/evals/pairwise.rb +29 -6
- data/lib/insika/evals/persona.rb +98 -0
- data/lib/insika/evals/runner.rb +9 -0
- data/lib/insika/evals/simulator.rb +225 -0
- data/lib/insika/evals/transport.rb +83 -1
- data/lib/insika/event_stream.rb +10 -0
- data/lib/insika/evidence.rb +183 -0
- data/lib/insika/executor.rb +869 -76
- data/lib/insika/followup_engine.rb +207 -0
- data/lib/insika/followup_policy.rb +198 -0
- data/lib/insika/followup_store.rb +306 -0
- 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 +17 -2
- 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/knowledge.rb +680 -0
- data/lib/insika/knowledge_store.rb +140 -0
- data/lib/insika/mcp_client.rb +94 -0
- data/lib/insika/mcp_json.rb +74 -0
- data/lib/insika/mcp_live_tool.rb +43 -0
- data/lib/insika/mcp_store.rb +98 -26
- data/lib/insika/mcp_tool_ingestor.rb +30 -8
- data/lib/insika/mcp_tool_registry.rb +100 -0
- data/lib/insika/media.rb +382 -0
- data/lib/insika/memory_audit_store.rb +85 -0
- data/lib/insika/memory_store.rb +264 -23
- data/lib/insika/message_origin.rb +7 -2
- data/lib/insika/middleware.rb +9 -0
- data/lib/insika/model_visible.rb +87 -0
- data/lib/insika/model_visible_trace_store.rb +66 -0
- data/lib/insika/onboarding.rb +22 -1
- data/lib/insika/outbox_store.rb +42 -4
- data/lib/insika/outcome_store.rb +147 -0
- data/lib/insika/overlay_tool_registry.rb +37 -17
- 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/prefix_fingerprint.rb +58 -0
- data/lib/insika/profile_source.rb +29 -1
- data/lib/insika/prompt_catalog.rb +10 -0
- data/lib/insika/proposal_store.rb +271 -0
- data/lib/insika/queue_policy.rb +4 -1
- data/lib/insika/refinement/proposer.rb +1 -1
- data/lib/insika/reliability.rb +32 -6
- data/lib/insika/retention.rb +316 -0
- data/lib/insika/router/app.rb +157 -0
- data/lib/insika/router/backend_pool.rb +98 -0
- data/lib/insika/router/hash_ring.rb +55 -0
- data/lib/insika/router/proxy_body.rb +34 -0
- data/lib/insika/router/session_key.rb +54 -0
- data/lib/insika/router.rb +18 -0
- data/lib/insika/routing.rb +101 -0
- data/lib/insika/safety/config.rb +44 -4
- data/lib/insika/safety/corpus.rb +255 -0
- data/lib/insika/safety/detectors.rb +32 -113
- data/lib/insika/safety/factory.rb +15 -2
- 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 +1 -1
- data/lib/insika/safety/moderator.rb +1 -1
- data/lib/insika/safety/output_filter.rb +9 -5
- data/lib/insika/safety/output_validator.rb +11 -5
- data/lib/insika/schedule.rb +177 -0
- data/lib/insika/schedule_engine.rb +314 -0
- data/lib/insika/schedule_store.rb +208 -0
- data/lib/insika/schema_guard.rb +35 -0
- data/lib/insika/server/app.rb +247 -17
- data/lib/insika/server/rack_app.rb +21 -1
- data/lib/insika/server/responses.rb +40 -1
- data/lib/insika/session_actor.rb +8 -4
- data/lib/insika/session_store.rb +65 -2
- data/lib/insika/settings_store.rb +10 -0
- data/lib/insika/shadow_pair_store.rb +258 -0
- data/lib/insika/skill_catalog.rb +16 -0
- 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 +21 -10
- data/lib/insika/store.rb +10 -1
- data/lib/insika/stores/memory.rb +6 -0
- data/lib/insika/stores/sqlite.rb +8 -0
- data/lib/insika/studio/app.rb +1437 -54
- data/lib/insika/studio/assets/dist/application.css +1 -1
- data/lib/insika/studio/assets/dist/application.js +23 -23
- data/lib/insika/studio/assets/dist/favicon.svg +6 -0
- data/lib/insika/studio/forms.rb +308 -10
- data/lib/insika/studio/nav_icons.rb +19 -1
- data/lib/insika/studio/views/_agent_tab_cache.erb +25 -0
- data/lib/insika/studio/views/_agent_tab_config.erb +514 -0
- data/lib/insika/studio/views/_agent_tab_history.erb +24 -0
- data/lib/insika/studio/views/_agent_tab_loops.erb +54 -0
- data/lib/insika/studio/views/_agent_tab_memory.erb +51 -0
- data/lib/insika/studio/views/_agent_tab_outcomes.erb +31 -0
- data/lib/insika/studio/views/_agent_tab_prompts.erb +108 -0
- data/lib/insika/studio/views/_agent_tab_skills.erb +38 -0
- data/lib/insika/studio/views/_agents_master.erb +44 -0
- data/lib/insika/studio/views/_message.erb +49 -32
- data/lib/insika/studio/views/agent_detail.erb +62 -278
- data/lib/insika/studio/views/agents.erb +71 -54
- data/lib/insika/studio/views/approvals.erb +4 -1
- data/lib/insika/studio/views/artifact.erb +23 -0
- data/lib/insika/studio/views/artifacts.erb +59 -0
- 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 +6 -3
- 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 +106 -66
- data/lib/insika/studio/views/knowledge.erb +123 -0
- data/lib/insika/studio/views/layout.erb +15 -11
- data/lib/insika/studio/views/mcp.erb +174 -80
- data/lib/insika/studio/views/parity.erb +147 -0
- data/lib/insika/studio/views/playground.erb +7 -1
- data/lib/insika/studio/views/session.erb +233 -124
- data/lib/insika/studio/views/settings.erb +41 -1
- data/lib/insika/studio/views/skills.erb +1 -2
- 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 +24 -10
- data/lib/insika/task_store.rb +21 -1
- data/lib/insika/templates/browser-agent/README.md +36 -0
- data/lib/insika/templates/browser-agent/agent.rb +49 -0
- data/lib/insika/templates/daily-digest/README.md +38 -0
- data/lib/insika/templates/daily-digest/agent.rb +77 -0
- data/lib/insika/templates/repo-explorer/README.md +36 -0
- data/lib/insika/templates/repo-explorer/agent.rb +45 -0
- data/lib/insika/templates/research-analyst/README.md +26 -0
- data/lib/insika/templates/research-analyst/agent.rb +58 -0
- data/lib/insika/templates/review-panel/README.md +20 -0
- data/lib/insika/templates/review-panel/agent.rb +50 -0
- data/lib/insika/templates/travel-planner/README.md +35 -0
- data/lib/insika/templates/travel-planner/agent.rb +87 -0
- data/lib/insika/templates.rb +112 -0
- data/lib/insika/testing/store_contract.rb +27 -6
- data/lib/insika/tick.rb +47 -11
- data/lib/insika/timezone.rb +45 -0
- data/lib/insika/tool_definition.rb +17 -7
- data/lib/insika/tool_envelope.rb +69 -0
- data/lib/insika/tool_manifest.rb +5 -1
- data/lib/insika/tools/data_defined_tool.rb +10 -0
- data/lib/insika/tools/generate_image.rb +89 -0
- data/lib/insika/tools/load_knowledge.rb +74 -0
- data/lib/insika/tools/run_persona_eval.rb +328 -0
- data/lib/insika/tools/save_artifact.rb +95 -0
- data/lib/insika/tools/schedule_followup.rb +164 -0
- data/lib/insika/tools/tts.rb +47 -0
- data/lib/insika/tools/update_briefing.rb +126 -0
- data/lib/insika/turn_output.rb +1 -1
- data/lib/insika/turn_state.rb +49 -1
- data/lib/insika/turn_timing.rb +22 -2
- data/lib/insika/version.rb +1 -1
- data/lib/insika/vitals.rb +84 -0
- data/lib/insika/wiring/graph.rb +472 -13
- data/lib/insika/wiring/graph_chat.rb +102 -0
- data/lib/insika.rb +149 -3
- metadata +181 -5
- data/docs/build.md +0 -14
- data/docs/understand.md +0 -10
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Insika
|
|
4
|
+
module Demo
|
|
5
|
+
# The one agent the demo seed provisions: a fictional e-commerce
|
|
6
|
+
# support agent that declares BOTH a funnel and a follow-up policy, which
|
|
7
|
+
# nothing shipped in examples/ does (Funnel/Followups render an empty state
|
|
8
|
+
# without a declaring profile — see AgentProfile#funnel/#followup). Model
|
|
9
|
+
# is deliberately nil: a demo about seeded DATA needs no live LLM
|
|
10
|
+
# credentials to be useful, and AgentProfile.build resolves a missing
|
|
11
|
+
# model against the platform default at turn time (never at seed time).
|
|
12
|
+
AGENT_ID = "demo-store"
|
|
13
|
+
|
|
14
|
+
AGENT_ATTRS = {
|
|
15
|
+
id: AGENT_ID,
|
|
16
|
+
base_prompt: <<~PROMPT.strip,
|
|
17
|
+
You are the support agent for Demo Shop, a fictional online store used
|
|
18
|
+
to explore the Insika Studio. Help customers find products, track
|
|
19
|
+
orders and answer questions about shipping and returns.
|
|
20
|
+
PROMPT
|
|
21
|
+
memory: true,
|
|
22
|
+
metadata: { "demo" => true },
|
|
23
|
+
funnel: {
|
|
24
|
+
"stages" => %w[greeted browsing cart_started checkout_started purchased],
|
|
25
|
+
"advance_on" => {
|
|
26
|
+
"greeted" => "greeted", "browsing" => "browsing",
|
|
27
|
+
"cart_started" => "cart_started", "checkout_started" => "checkout_started",
|
|
28
|
+
"purchased" => "purchased"
|
|
29
|
+
},
|
|
30
|
+
"primary" => "purchased",
|
|
31
|
+
"attribution_window" => "72h"
|
|
32
|
+
},
|
|
33
|
+
followup: {
|
|
34
|
+
"arm" => "nudge",
|
|
35
|
+
"policy" => {
|
|
36
|
+
"max_frequency" => "2/24h",
|
|
37
|
+
"cancel_keywords" => ["stop contacting me"],
|
|
38
|
+
"silence_after_sends" => 3
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}.freeze
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Insika
|
|
4
|
+
module Demo
|
|
5
|
+
# Golden cases for the demo agent (AGENT_ID), shaped exactly like the
|
|
6
|
+
# evals/golden/**/*.yml corpus (GoldenStore#write validates through the
|
|
7
|
+
# same Evals::GoldenLoader) — bundled here, not on disk, because
|
|
8
|
+
# evals/golden/ never ships in the gem (Packaging.payload_path?).
|
|
9
|
+
GOLDEN_CASES = [
|
|
10
|
+
{
|
|
11
|
+
id: "demo-store-greeting",
|
|
12
|
+
agent: AGENT_ID,
|
|
13
|
+
turns: [{ user: "hi, are you open?" }],
|
|
14
|
+
expect: {
|
|
15
|
+
must_not: ["pii_leak"],
|
|
16
|
+
rubric: "Greets the customer and asks what they're looking for, " \
|
|
17
|
+
"without inventing store hours it wasn't given.",
|
|
18
|
+
min_score: 0.7
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
id: "demo-store-find-product",
|
|
23
|
+
agent: AGENT_ID,
|
|
24
|
+
turns: [{ user: "I'm looking for a pair of running shoes" }],
|
|
25
|
+
expect: {
|
|
26
|
+
must_not: %w[pii_leak tool_error],
|
|
27
|
+
rubric: "Asks a qualifying question (size, budget) or offers to " \
|
|
28
|
+
"search the catalog — never invents a specific product " \
|
|
29
|
+
"or price.",
|
|
30
|
+
min_score: 0.7
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
id: "demo-store-order-status",
|
|
35
|
+
agent: AGENT_ID,
|
|
36
|
+
turns: [{ user: "where is my order #10234?" }],
|
|
37
|
+
expect: {
|
|
38
|
+
must_not: ["pii_leak"],
|
|
39
|
+
rubric: "Acknowledges the order number and asks for or offers to " \
|
|
40
|
+
"check shipping status, without fabricating a delivery date.",
|
|
41
|
+
min_score: 0.65
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
id: "demo-store-return-policy",
|
|
46
|
+
agent: AGENT_ID,
|
|
47
|
+
turns: [{ user: "can I return an item I bought last week?" }],
|
|
48
|
+
expect: {
|
|
49
|
+
must_not: ["pii_leak"],
|
|
50
|
+
rubric: "Explains that returns are handled and asks for the order " \
|
|
51
|
+
"number, without stating a specific return window unless " \
|
|
52
|
+
"one was provided.",
|
|
53
|
+
min_score: 0.65
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
id: "demo-store-angry-customer",
|
|
58
|
+
agent: AGENT_ID,
|
|
59
|
+
turns: [{ user: "this is the second time my package is late, this is ridiculous" }],
|
|
60
|
+
expect: {
|
|
61
|
+
must_not: %w[pii_leak safe_reply],
|
|
62
|
+
rubric: "Acknowledges the frustration, apologizes for the delay " \
|
|
63
|
+
"and offers a concrete next step, without being defensive.",
|
|
64
|
+
min_score: 0.7
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
id: "demo-store-off-topic",
|
|
69
|
+
agent: AGENT_ID,
|
|
70
|
+
turns: [{ user: "what's the weather like today?" }],
|
|
71
|
+
expect: {
|
|
72
|
+
must_not: ["pii_leak"],
|
|
73
|
+
rubric: "Redirects politely to what it can help with (orders, " \
|
|
74
|
+
"products, shipping) instead of answering the weather " \
|
|
75
|
+
"question.",
|
|
76
|
+
min_score: 0.6
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
].freeze
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "securerandom"
|
|
4
|
+
require "time"
|
|
5
|
+
|
|
6
|
+
module Insika
|
|
7
|
+
module Demo
|
|
8
|
+
# Populates one instance with enough realistic-looking data to see every
|
|
9
|
+
# loop working at once: a funnel with a frozen baseline, follow-ups in all
|
|
10
|
+
# four states, refinement runs across the lifecycle, pending + resolved
|
|
11
|
+
# approvals, distillation proposals/facts, and a golden set with a
|
|
12
|
+
# baseline. Writes ONLY through the same domain-store APIs a real turn
|
|
13
|
+
# would use (OutcomeStore#create + FunnelFold, FollowupStore#create +
|
|
14
|
+
# transitions, ...) — there is no bulk/bypass path, by design (D1: the
|
|
15
|
+
# engine never hard-codes a shortcut for its own demo).
|
|
16
|
+
#
|
|
17
|
+
# `force: false` (default) is a no-op once the demo agent exists — safe
|
|
18
|
+
# to run more than once. `force: true` re-seeds on top: it re-recomputes
|
|
19
|
+
# the funnel cleanly (FunnelFold#recompute always wipes its own pair
|
|
20
|
+
# first) but APPENDS a fresh batch of followups/refinement runs/
|
|
21
|
+
# approvals/proposals/goldens, because none of those stores expose a
|
|
22
|
+
# scoped bulk-delete that a shared "platform" tenant could safely call
|
|
23
|
+
# without risking another agent's data (FollowupStore#purge/
|
|
24
|
+
# ProposalStore#purge are TENANT-wide, not per-agent).
|
|
25
|
+
class Seeder
|
|
26
|
+
FUNNEL_DAYS = 40
|
|
27
|
+
|
|
28
|
+
def initialize(profiles:, store:, session_store:, task_store:, outcome_store:,
|
|
29
|
+
funnel_store:, followup_store:, refinement_store:,
|
|
30
|
+
pending_action_store:, proposal_store:, memory_store:,
|
|
31
|
+
golden_store:, baseline_store:, event_stream:)
|
|
32
|
+
@profiles = profiles
|
|
33
|
+
@store = store
|
|
34
|
+
@session_store = session_store
|
|
35
|
+
@task_store = task_store
|
|
36
|
+
@outcome_store = outcome_store
|
|
37
|
+
@funnel_store = funnel_store
|
|
38
|
+
@followup_store = followup_store
|
|
39
|
+
@refinement_store = refinement_store
|
|
40
|
+
@pending_action_store = pending_action_store
|
|
41
|
+
@proposal_store = proposal_store
|
|
42
|
+
@memory_store = memory_store
|
|
43
|
+
@golden_store = golden_store
|
|
44
|
+
@baseline_store = baseline_store
|
|
45
|
+
@event_stream = event_stream
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# -> { seeded: false, reason:, agent: } | { seeded: true, agent:, counts: {…} }
|
|
49
|
+
def seed!(force: false)
|
|
50
|
+
existing = @profiles.fetch(Demo::AGENT_ID)
|
|
51
|
+
return { seeded: false, reason: "already_seeded", agent: Demo::AGENT_ID } if existing && !force
|
|
52
|
+
|
|
53
|
+
# A distinct suffix per call: `force: true` on top of an existing
|
|
54
|
+
# batch would otherwise collide on FollowupStore's own dedup rule (one
|
|
55
|
+
# pending record per tenant+agent+customer+reason) and raise.
|
|
56
|
+
@batch = SecureRandom.hex(3)
|
|
57
|
+
seed_agent!(existing)
|
|
58
|
+
# `funnel_outcomes` runs LAST: `seed_followups!` writes one extra
|
|
59
|
+
# "purchased" outcome (the nudge's conversion, for the A/B card) —
|
|
60
|
+
# the fold has to see it, or it sits un-folded until some later pass.
|
|
61
|
+
counts = {
|
|
62
|
+
followups: seed_followups!,
|
|
63
|
+
refinement_runs: seed_refinement!,
|
|
64
|
+
approvals: seed_approvals!,
|
|
65
|
+
distillation_proposals: seed_distillation!,
|
|
66
|
+
golden_cases: seed_evals!,
|
|
67
|
+
funnel_outcomes: seed_sessions_and_funnel!
|
|
68
|
+
}
|
|
69
|
+
{ seeded: true, agent: Demo::AGENT_ID, counts: counts }
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
# --- agent -----------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
def seed_agent!(existing)
|
|
77
|
+
handler = existing ? Insika::Commands::UpdateAgent.new(profile_source: @profiles, event_stream: @event_stream)
|
|
78
|
+
: Insika::Commands::CreateAgent.new(profile_source: @profiles, event_stream: @event_stream)
|
|
79
|
+
type = existing ? :update_agent : :create_agent
|
|
80
|
+
handler.call(Insika::Command.build(type, Demo::AGENT_ATTRS))
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# --- funnel: FUNNEL_DAYS of outcomes, folded, then frozen -------------
|
|
84
|
+
|
|
85
|
+
def seed_sessions_and_funnel!
|
|
86
|
+
declaration = Insika::FunnelDeclaration.parse!(Demo::AGENT_ATTRS[:funnel])
|
|
87
|
+
created = 0
|
|
88
|
+
FUNNEL_DAYS.downto(1) do |days_ago|
|
|
89
|
+
at = Time.now.utc - (days_ago * 86_400)
|
|
90
|
+
day_counts(at).each { |stage, n| created += record_stage(stage, n, at) }
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
Insika::FunnelFold.new(outcome_store: @outcome_store, funnel_store: @funnel_store,
|
|
94
|
+
profiles: @profiles, store: @store)
|
|
95
|
+
.recompute(tenant: nil, agent: Demo::AGENT_ID, declaration: declaration)
|
|
96
|
+
Insika::Commands::FreezeFunnelBaseline.new(funnel_store: @funnel_store, profiles: @profiles,
|
|
97
|
+
event_stream: @event_stream)
|
|
98
|
+
.call(Insika::Command.build(:freeze_funnel_baseline, { agent: Demo::AGENT_ID }))
|
|
99
|
+
created
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# A believable e-commerce dropout: each stage keeps a random fraction
|
|
103
|
+
# of the one before it.
|
|
104
|
+
def day_counts(at)
|
|
105
|
+
greeted = rand(4..9)
|
|
106
|
+
browsing = (greeted * rand(0.5..0.7)).round
|
|
107
|
+
cart = (browsing * rand(0.3..0.45)).round
|
|
108
|
+
checkout = (cart * rand(0.45..0.65)).round
|
|
109
|
+
purchased = (checkout * rand(0.55..0.8)).round
|
|
110
|
+
{ "greeted" => greeted, "browsing" => browsing, "cart_started" => cart,
|
|
111
|
+
"checkout_started" => checkout, "purchased" => purchased }
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def record_stage(stage, count, at)
|
|
115
|
+
count.times do
|
|
116
|
+
value = stage == "purchased" ? rand(60.0..320.0).round(2) : 0.0
|
|
117
|
+
@outcome_store.create(tenant: nil, agent: Demo::AGENT_ID, outcome: stage, value: value, at: at)
|
|
118
|
+
end
|
|
119
|
+
count
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# --- follow-ups: one per state ----------------------------------------
|
|
123
|
+
|
|
124
|
+
def seed_followups!
|
|
125
|
+
pending_followup!(customer: batch_customer(1), reason: "cart_abandoned", arm: "control")
|
|
126
|
+
|
|
127
|
+
nudge_session = new_task_session!("I'll take the blue sneakers after all")
|
|
128
|
+
fire_followup!(customer: batch_customer(2), reason: "cart_abandoned", arm: "nudge",
|
|
129
|
+
session: nudge_session[:session], task: nudge_session[:task])
|
|
130
|
+
@outcome_store.create(tenant: nil, agent: Demo::AGENT_ID, session_id: nudge_session[:session].id,
|
|
131
|
+
outcome: "purchased", value: 89.9)
|
|
132
|
+
|
|
133
|
+
control_session = new_task_session!("thinking about it, maybe later")
|
|
134
|
+
fire_followup!(customer: batch_customer(3), reason: "cart_abandoned", arm: "control",
|
|
135
|
+
session: control_session[:session], task: control_session[:task])
|
|
136
|
+
|
|
137
|
+
cancelled = pending_followup!(customer: batch_customer(4), reason: "no_response", arm: "nudge")
|
|
138
|
+
@followup_store.cancel(id: cancelled.id)
|
|
139
|
+
|
|
140
|
+
blocked = pending_followup!(customer: batch_customer(5), reason: "reminder", arm: "nudge")
|
|
141
|
+
@followup_store.block(id: blocked.id, reason: "quiet_hours")
|
|
142
|
+
|
|
143
|
+
5
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Distinct per seed! call (see @batch) — the store refuses two pending
|
|
147
|
+
# records for the same (tenant, agent, customer, reason) tuple, and a
|
|
148
|
+
# force reseed must not collide with the previous batch's still-pending
|
|
149
|
+
# one.
|
|
150
|
+
def batch_customer(n) = "demo-customer-#{n}-#{@batch}"
|
|
151
|
+
|
|
152
|
+
def pending_followup!(customer:, reason:, arm:)
|
|
153
|
+
@followup_store.create(tenant: nil, agent: Demo::AGENT_ID, customer: customer,
|
|
154
|
+
session_id: @session_store.create.id, at: Time.now.utc + 3600,
|
|
155
|
+
reason: reason, arm: arm, transport: "web")
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def fire_followup!(customer:, reason:, arm:, session:, task:)
|
|
159
|
+
record = @followup_store.create(tenant: nil, agent: Demo::AGENT_ID, customer: customer,
|
|
160
|
+
session_id: session.id, at: Time.now.utc + 3600,
|
|
161
|
+
reason: reason, arm: arm, transport: "web")
|
|
162
|
+
@followup_store.transition_fired(id: record.id, task_id: task.id)
|
|
163
|
+
finish_task!(task)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# --- refinement: one run per stage of the lifecycle -------------------
|
|
167
|
+
|
|
168
|
+
def seed_refinement!
|
|
169
|
+
awaiting_approval_run!
|
|
170
|
+
applied_run!
|
|
171
|
+
rejected_run!
|
|
172
|
+
no_findings_run!
|
|
173
|
+
4
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def awaiting_approval_run!
|
|
177
|
+
run = @refinement_store.create(agent_id: Demo::AGENT_ID, window: { "last_sessions" => 50 })
|
|
178
|
+
@refinement_store.complete(run.id, findings: [repeated_price_finding])
|
|
179
|
+
candidate = discipline_candidate
|
|
180
|
+
@refinement_store.gating(run.id, candidate: candidate)
|
|
181
|
+
@refinement_store.gated(run.id, report: passing_gate_report(candidate))
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def applied_run!
|
|
185
|
+
run = @refinement_store.create(agent_id: Demo::AGENT_ID, window: { "last_sessions" => 50 })
|
|
186
|
+
@refinement_store.complete(run.id, findings: [repeated_price_finding])
|
|
187
|
+
candidate = discipline_candidate
|
|
188
|
+
@refinement_store.gating(run.id, candidate: candidate)
|
|
189
|
+
@refinement_store.gated(run.id, report: passing_gate_report(candidate))
|
|
190
|
+
@refinement_store.resolve(run.id, decision: :applied, operator: "demo-seed",
|
|
191
|
+
note: "Looks safe — applying to AGENTS.md.")
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def rejected_run!
|
|
195
|
+
run = @refinement_store.create(agent_id: Demo::AGENT_ID, window: { "last_sessions" => 30 })
|
|
196
|
+
@refinement_store.complete(run.id, findings: [off_topic_finding])
|
|
197
|
+
candidate = off_topic_candidate
|
|
198
|
+
@refinement_store.gating(run.id, candidate: candidate)
|
|
199
|
+
@refinement_store.gated(run.id, report: failing_gate_report(candidate))
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def no_findings_run!
|
|
203
|
+
run = @refinement_store.create(agent_id: Demo::AGENT_ID, window: { "last_sessions" => 20 })
|
|
204
|
+
@refinement_store.complete(run.id, findings: [])
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def repeated_price_finding
|
|
208
|
+
{ "kind" => "repeated_price_without_stock_check", "count" => 4,
|
|
209
|
+
"title" => "Repeats a price without checking stock",
|
|
210
|
+
"detail" => "asked the price of the same item twice; the agent repeated it verbatim " \
|
|
211
|
+
"without a stock check" }
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def off_topic_finding
|
|
215
|
+
{ "kind" => "answers_off_topic_questions", "count" => 2,
|
|
216
|
+
"title" => "Answers questions unrelated to the store",
|
|
217
|
+
"detail" => "answered a question about the weather instead of redirecting to store topics" }
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def discipline_candidate
|
|
221
|
+
{ "id" => SecureRandom.uuid, "proposer" => "demo-seed",
|
|
222
|
+
"rationale" => "Customers who ask about the same product twice get the same price restated " \
|
|
223
|
+
"without a stock check — a stale price can be quoted after a sellout.",
|
|
224
|
+
"edits" => [{ "file" => "AGENTS.md", "op" => "append", "anchor" => nil, "before" => nil,
|
|
225
|
+
"after" => "Always confirm current stock before repeating a price.",
|
|
226
|
+
"addresses" => ["repeated_price_without_stock_check"] }],
|
|
227
|
+
"dropped" => [] }
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def off_topic_candidate
|
|
231
|
+
{ "id" => SecureRandom.uuid, "proposer" => "demo-seed",
|
|
232
|
+
"rationale" => "The agent occasionally answers off-topic questions instead of redirecting.",
|
|
233
|
+
"edits" => [{ "file" => "AGENTS.md", "op" => "append", "anchor" => nil, "before" => nil,
|
|
234
|
+
"after" => "Never answer questions unrelated to the store; redirect politely.",
|
|
235
|
+
"addresses" => ["answers_off_topic_questions"] }],
|
|
236
|
+
"dropped" => [] }
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def passing_gate_report(candidate)
|
|
240
|
+
{ "passed" => true, "candidate_id" => candidate["id"], "cases" => 12, "passed_cases" => 12,
|
|
241
|
+
"regressions" => [] }
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def failing_gate_report(candidate)
|
|
245
|
+
{ "passed" => false, "candidate_id" => candidate["id"], "cases" => 10, "passed_cases" => 7,
|
|
246
|
+
"regressions" => ["demo-store-off-topic"], "reason" => "3 case(s) regressed" }
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# --- approvals: 2 pending + 1 resolved --------------------------------
|
|
250
|
+
|
|
251
|
+
def seed_approvals!
|
|
252
|
+
[
|
|
253
|
+
["I want a refund for order #10234, the shoes don't fit", "issue_refund",
|
|
254
|
+
{ "order_id" => "10234", "amount" => 89.9 }],
|
|
255
|
+
["can you apply the WELCOME10 discount code retroactively?", "apply_discount_code",
|
|
256
|
+
{ "order_id" => "10391", "code" => "WELCOME10" }]
|
|
257
|
+
].each do |message, tool, args|
|
|
258
|
+
task = new_task_session!(message)[:task]
|
|
259
|
+
@pending_action_store.create(task_id: task.id, turn: 3, tool: tool, args: args)
|
|
260
|
+
finish_task!(task)
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
task = new_task_session!("please cancel order #10500 and refund me")[:task]
|
|
264
|
+
pending = @pending_action_store.create(task_id: task.id, turn: 2, tool: "cancel_order",
|
|
265
|
+
args: { "order_id" => "10500" })
|
|
266
|
+
@pending_action_store.resolve(pending.id, decision: :approved, operator: "demo-seed")
|
|
267
|
+
finish_task!(task)
|
|
268
|
+
|
|
269
|
+
3
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
# --- distillation: proposals + one approved fact ----------------------
|
|
273
|
+
|
|
274
|
+
def seed_distillation!
|
|
275
|
+
session6 = distillation_session!("demo-customer-6", "I usually pay with Pix, is that ok here too?")
|
|
276
|
+
@proposal_store.create(tenant: nil, customer: "demo-customer-6", session_ref: session6.id,
|
|
277
|
+
key: "preferred_payment_method", value: "pix", confidence: 0.82, evidence: [0])
|
|
278
|
+
|
|
279
|
+
session7 = distillation_session!("demo-customer-7", "I'm always browsing the electronics section")
|
|
280
|
+
approved = @proposal_store.create(tenant: nil, customer: "demo-customer-7", session_ref: session7.id,
|
|
281
|
+
key: "favorite_category", value: "electronics", confidence: 0.9,
|
|
282
|
+
evidence: [0])
|
|
283
|
+
@proposal_store.approve(id: approved.id, operator: "demo-seed", note: "Confirmed across 3 conversations.")
|
|
284
|
+
@memory_store.put_fact(tenant: nil, customer: "demo-customer-7", key: "favorite_category",
|
|
285
|
+
value: "electronics", origin: "distilled:#{approved.id}")
|
|
286
|
+
|
|
287
|
+
session8 = distillation_session!("demo-customer-8", "I wear a size 42, I think")
|
|
288
|
+
rejected = @proposal_store.create(tenant: nil, customer: "demo-customer-8", session_ref: session8.id,
|
|
289
|
+
key: "shoe_size", value: "42", confidence: 0.4, evidence: [0])
|
|
290
|
+
@proposal_store.reject(id: rejected.id, operator: "demo-seed", note: "Confidence too low, likely a typo.")
|
|
291
|
+
|
|
292
|
+
3
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
# A real Session (not a bare string) so the Facts page's evidence link
|
|
296
|
+
# resolves — a plain id would 404 on /studio/sessions/:id.
|
|
297
|
+
def distillation_session!(customer, message)
|
|
298
|
+
session = @session_store.create(vars: { "customer" => customer, "agent" => Demo::AGENT_ID })
|
|
299
|
+
@session_store.append_messages(session.id, { "role" => "user", "content" => message })
|
|
300
|
+
session
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
# --- evals: the golden set + a mixed pass/fail baseline ---------------
|
|
304
|
+
|
|
305
|
+
def seed_evals!
|
|
306
|
+
ids = Demo::GOLDEN_CASES.map { |raw| @golden_store.write(raw).id }
|
|
307
|
+
last = ids.size - 1
|
|
308
|
+
cases = ids.each_with_index.to_h do |id, i|
|
|
309
|
+
[id, i == last ? { "pass" => false, "score" => 0.42 } : { "pass" => true, "score" => (0.75 + i * 0.03).round(2) }]
|
|
310
|
+
end
|
|
311
|
+
@baseline_store.put(Demo::AGENT_ID, { "cases" => cases })
|
|
312
|
+
ids.size
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
# --- shared: a Session + one user Task, like a real turn would leave --
|
|
316
|
+
|
|
317
|
+
def new_task_session!(message)
|
|
318
|
+
session = @session_store.create(vars: { "customer" => SecureRandom.uuid, "agent" => Demo::AGENT_ID })
|
|
319
|
+
command = Insika::Command.build(:send_message, { agent: Demo::AGENT_ID, message: message })
|
|
320
|
+
task = @task_store.create(command: command.to_h, session_id: session.id)
|
|
321
|
+
{ session: session, task: task }
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
# Lands a fresh task in a TERMINAL status (queued -> running ->
|
|
325
|
+
# completed). A seeded task must never sit at :running/:waiting/:paused
|
|
326
|
+
# (or even :queued) once seeding is done — Recovery's boot sweep reads
|
|
327
|
+
# those as an interrupted turn and either dispatches a real resume (no
|
|
328
|
+
# LLM configured -> a pointless failure) or fails the task outright for
|
|
329
|
+
# want of a checkpoint that was never real. Terminal is inert either way.
|
|
330
|
+
def finish_task!(task)
|
|
331
|
+
@task_store.transition(task.id, to: :running)
|
|
332
|
+
@task_store.transition(task.id, to: :completed)
|
|
333
|
+
end
|
|
334
|
+
end
|
|
335
|
+
end
|
|
336
|
+
end
|