insika 0.3.0 → 0.8.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 +296 -0
- data/README.md +48 -12
- data/bin/insika +725 -0
- data/bin/insika-router +87 -0
- data/docs/AGENTS.md +116 -406
- data/docs/API.md +5 -5
- data/docs/ARCHITECTURE.md +3 -2
- data/docs/ARTIFACTS.md +137 -0
- data/docs/BENCHMARK.md +2 -2
- data/docs/CHANNELS.md +14 -14
- data/docs/CONTEXT.md +63 -19
- data/docs/DEMO.md +80 -0
- data/docs/DEPLOY.md +87 -10
- data/docs/EMBEDDING.md +1 -1
- data/docs/EVALS.md +128 -3
- data/docs/FACTS.md +3 -3
- data/docs/HARVEST.md +5 -6
- data/docs/KNOWLEDGE.md +290 -0
- data/docs/LOADTEST.md +17 -29
- data/docs/MEDIA.md +128 -0
- data/docs/OBSERVABILITY.md +46 -12
- data/docs/OUTCOMES.md +137 -0
- data/docs/PLUGINS.md +51 -6
- data/docs/POLICY.md +222 -0
- data/docs/REFINEMENT.md +14 -9
- data/docs/RELEASING.md +4 -4
- data/docs/ROUTER.md +213 -0
- data/docs/RUNNING-LOCAL.md +5 -5
- data/docs/SCHEDULING.md +121 -0
- data/docs/SECURITY.md +23 -7
- data/docs/SKILLS.md +11 -2
- data/docs/SOAK.md +3 -3
- data/docs/TEMPLATES.md +134 -0
- data/docs/TOOLS.md +176 -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 +4 -4
- data/docs/improve.md +20 -0
- data/docs/index.md +8 -5
- 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 +99 -17
- data/lib/insika/artifact_signing.rb +82 -0
- data/lib/insika/artifact_store.rb +160 -0
- data/lib/insika/channel_delivery.rb +1 -1
- data/lib/insika/chat_builder.rb +50 -19
- data/lib/insika/commands/agent_payload.rb +2 -2
- data/lib/insika/commands/backfill_knowledge.rb +145 -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 +15 -3
- data/lib/insika/commands/gate_refinement.rb +1 -1
- data/lib/insika/commands/refresh_mcp_tools.rb +47 -0
- data/lib/insika/commands/restore_concept.rb +34 -0
- data/lib/insika/commands/seed_demo_data.rb +31 -0
- data/lib/insika/commands/upsert_mcp.rb +6 -3
- data/lib/insika/commands/write_concept.rb +57 -0
- data/lib/insika/compaction.rb +196 -0
- data/lib/insika/context/builder.rb +6 -2
- data/lib/insika/context/fragment.rb +4 -1
- data/lib/insika/context/priority.rb +8 -0
- data/lib/insika/context/providers/briefing.rb +53 -24
- data/lib/insika/context/providers/knowledge.rb +108 -0
- data/lib/insika/context/providers/prompt.rb +30 -24
- data/lib/insika/context/providers/session.rb +46 -10
- data/lib/insika/context_trace_store.rb +11 -1
- 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/doctor.rb +280 -17
- data/lib/insika/dsl/definition.rb +3 -2
- data/lib/insika/dsl/runtime.rb +64 -79
- data/lib/insika/dsl/server_boot.rb +23 -1
- data/lib/insika/dsl/system.rb +10 -2
- data/lib/insika/dsl.rb +103 -2
- data/lib/insika/env_schema.rb +21 -7
- data/lib/insika/evals/golden.rb +41 -4
- data/lib/insika/evals/judge.rb +47 -2
- data/lib/insika/evals/pairwise.rb +11 -0
- 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 +84 -2
- data/lib/insika/event_stream.rb +10 -0
- data/lib/insika/executor.rb +295 -55
- data/lib/insika/followup_policy.rb +2 -25
- data/lib/insika/golden_store.rb +16 -1
- data/lib/insika/grounding/matcher.rb +1 -1
- data/lib/insika/knowledge.rb +680 -0
- data/lib/insika/knowledge_store.rb +140 -0
- data/lib/insika/loop_detector.rb +5 -34
- 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 +115 -31
- data/lib/insika/message_origin.rb +1 -1
- data/lib/insika/middleware.rb +9 -0
- data/lib/insika/onboarding.rb +17 -1
- data/lib/insika/outcome_store.rb +1 -1
- data/lib/insika/overlay_tool_registry.rb +37 -17
- data/lib/insika/packaging.rb +2 -2
- data/lib/insika/profile_source.rb +15 -1
- data/lib/insika/prompt_catalog.rb +10 -0
- data/lib/insika/retention.rb +36 -1
- 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/schedule.rb +177 -0
- data/lib/insika/schedule_engine.rb +314 -0
- data/lib/insika/schedule_store.rb +208 -0
- data/lib/insika/server/app.rb +105 -15
- data/lib/insika/server/rack_app.rb +5 -1
- data/lib/insika/server/responses.rb +5 -5
- data/lib/insika/session_store.rb +34 -4
- data/lib/insika/settings_store.rb +8 -1
- data/lib/insika/skill_catalog.rb +12 -0
- data/lib/insika/soak/runner.rb +4 -4
- data/lib/insika/steer_injector.rb +21 -10
- data/lib/insika/studio/app.rb +591 -47
- data/lib/insika/studio/assets/dist/application.css +1 -1
- data/lib/insika/studio/assets/dist/application.js +21 -21
- data/lib/insika/studio/forms.rb +57 -5
- data/lib/insika/studio/nav_icons.rb +14 -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 +61 -820
- data/lib/insika/studio/views/agents.erb +70 -57
- data/lib/insika/studio/views/artifact.erb +23 -0
- data/lib/insika/studio/views/artifacts.erb +59 -0
- data/lib/insika/studio/views/evals.erb +2 -2
- data/lib/insika/studio/views/facts.erb +1 -1
- data/lib/insika/studio/views/funnel.erb +1 -1
- data/lib/insika/studio/views/home.erb +106 -67
- data/lib/insika/studio/views/knowledge.erb +123 -0
- data/lib/insika/studio/views/layout.erb +14 -11
- data/lib/insika/studio/views/mcp.erb +174 -80
- data/lib/insika/studio/views/session.erb +231 -177
- data/lib/insika/studio/views/settings.erb +50 -1
- data/lib/insika/studio/views/skills.erb +1 -1
- data/lib/insika/studio/views/tools.erb +24 -9
- data/lib/insika/telemetry/recorder.rb +49 -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 +47 -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 +68 -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/tick.rb +24 -12
- data/lib/insika/timezone.rb +45 -0
- data/lib/insika/tool_batch.rb +67 -0
- data/lib/insika/tool_usage_report.rb +162 -0
- data/lib/insika/tools/generate_image.rb +52 -7
- 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/turn_budget.rb +91 -0
- data/lib/insika/turn_output.rb +1 -1
- data/lib/insika/turn_state.rb +15 -4
- data/lib/insika/version.rb +1 -1
- data/lib/insika/wiring/graph.rb +184 -12
- data/lib/insika/wiring/graph_chat.rb +102 -0
- data/lib/insika.rb +64 -0
- metadata +109 -5
- data/docs/build.md +0 -14
- data/docs/understand.md +0 -10
|
@@ -14,8 +14,8 @@ module Insika
|
|
|
14
14
|
end
|
|
15
15
|
|
|
16
16
|
def call(request)
|
|
17
|
-
messages = transcript_for(request)
|
|
18
|
-
return [] if messages.nil? || messages.empty?
|
|
17
|
+
messages, compaction = transcript_for(request)
|
|
18
|
+
return [] if messages.nil? || (messages.empty? && compaction.nil?)
|
|
19
19
|
|
|
20
20
|
# A3/C3 opt-in: identical tool results in the transcript collapse to a
|
|
21
21
|
# back-reference (the cheap half of compaction). CHANGES WHAT THE MODEL
|
|
@@ -28,7 +28,7 @@ module Insika
|
|
|
28
28
|
# the fragment level means the budget cut (apply_budget) drops a whole
|
|
29
29
|
# tool cycle atomically — a tool_use is NEVER seeded without its result
|
|
30
30
|
# (which providers reject), without touching apply_budget itself.
|
|
31
|
-
eviction_units(messages).each_with_index.map do |unit, idx|
|
|
31
|
+
fragments = eviction_units(messages).each_with_index.map do |unit, idx|
|
|
32
32
|
ContextFragment.build(
|
|
33
33
|
# single message stays a Hash (compat with existing fragments); a
|
|
34
34
|
# multi-message cycle is an Array (seed_history flattens it back).
|
|
@@ -39,6 +39,7 @@ module Insika
|
|
|
39
39
|
source: id
|
|
40
40
|
)
|
|
41
41
|
end
|
|
42
|
+
compaction ? [compaction_fragment(compaction)] + fragments : fragments
|
|
42
43
|
end
|
|
43
44
|
|
|
44
45
|
private
|
|
@@ -90,15 +91,50 @@ module Insika
|
|
|
90
91
|
end
|
|
91
92
|
|
|
92
93
|
# Precedence: checkpoint -> explicit history -> store.
|
|
93
|
-
# The first present source wins; no merge.
|
|
94
|
+
# The first present source wins; no merge. -> [messages, compaction|nil].
|
|
95
|
+
# The compaction state (RFC-0044) applies to the STORE source only: a
|
|
96
|
+
# checkpoint resume replays the checkpoint's own tape (which already
|
|
97
|
+
# contains whatever summary the original turn saw) and an explicit
|
|
98
|
+
# vars[:history] is the caller's contract — neither is rewritten.
|
|
94
99
|
def transcript_for(request)
|
|
95
|
-
return request.checkpoint.messages if request.checkpoint
|
|
100
|
+
return [request.checkpoint.messages, nil] if request.checkpoint
|
|
96
101
|
|
|
97
102
|
explicit = explicit_history(request)
|
|
98
|
-
return explicit if explicit
|
|
99
|
-
return
|
|
103
|
+
return [explicit, nil] if explicit
|
|
104
|
+
return compacted_session_messages(request.session) if request.session
|
|
100
105
|
|
|
101
|
-
nil
|
|
106
|
+
[nil, nil]
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Splits the stored transcript at the persisted compaction boundary:
|
|
110
|
+
# messages[0...upto] are represented by the summary, messages[upto..]
|
|
111
|
+
# stay verbatim. `upto` is clamped to the transcript size (defensive —
|
|
112
|
+
# the store is append-only, so it should never outrun it).
|
|
113
|
+
def compacted_session_messages(session)
|
|
114
|
+
fresh = fresh_session(session)
|
|
115
|
+
return [[], nil] if fresh.nil?
|
|
116
|
+
|
|
117
|
+
messages = fresh.messages || []
|
|
118
|
+
state = fresh.respond_to?(:compaction) ? fresh.compaction : nil
|
|
119
|
+
upto = state ? state["upto"].to_i : 0
|
|
120
|
+
return [messages, nil] unless upto.positive? && Coercion.present?(state["summary"])
|
|
121
|
+
|
|
122
|
+
[messages.drop([upto, messages.size].min), state]
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# The compacted prefix as ONE history fragment, rendered FIRST (the
|
|
126
|
+
# Builder keeps history in production order). role "user" because it is
|
|
127
|
+
# provider-agnostic (a mid-history "system" message is not). Priority
|
|
128
|
+
# COMPACTION (59): the "oldest unit" — under budget it drops before any
|
|
129
|
+
# verbatim message. source "compaction" -> its own context-trace category.
|
|
130
|
+
def compaction_fragment(state)
|
|
131
|
+
ContextFragment.build(
|
|
132
|
+
content: { role: "user",
|
|
133
|
+
content: "<conversation_summary>\n#{state['summary']}\n</conversation_summary>" },
|
|
134
|
+
placement: :history,
|
|
135
|
+
priority: Context::Priority::COMPACTION,
|
|
136
|
+
source: "compaction"
|
|
137
|
+
)
|
|
102
138
|
end
|
|
103
139
|
|
|
104
140
|
# Source 2 (explicit history): the handler passes it in request.vars[:history].
|
|
@@ -111,8 +147,8 @@ module Insika
|
|
|
111
147
|
# CONDITIONAL requiredness: when a session is requested, a read
|
|
112
148
|
# failure becomes a ContextError (aborts the turn); the base required?
|
|
113
149
|
# does not receive the request, so the behavior lives here.
|
|
114
|
-
def
|
|
115
|
-
@session_store.find(session.id)
|
|
150
|
+
def fresh_session(session)
|
|
151
|
+
@session_store.find(session.id)
|
|
116
152
|
rescue StandardError => e # read failure (exception/StoreError)
|
|
117
153
|
raise ContextError.new("Session provider failed with a requested session: #{e.message}",
|
|
118
154
|
provider: id)
|
|
@@ -80,10 +80,20 @@ module Insika
|
|
|
80
80
|
"tools" => { "count" => int(tools[:count] || tools["count"]),
|
|
81
81
|
"tokens" => int(tools[:tokens] || tools["tokens"]) },
|
|
82
82
|
"fingerprints" => fingerprints_of(e[:fingerprints] || e["fingerprints"]),
|
|
83
|
-
"cache" => cache_of(e[:cache] || e["cache"])
|
|
83
|
+
"cache" => cache_of(e[:cache] || e["cache"]),
|
|
84
|
+
"compaction" => compaction_of(e[:compaction] || e["compaction"])
|
|
84
85
|
}.compact
|
|
85
86
|
end
|
|
86
87
|
|
|
88
|
+
# { upto, runs } — present only when the session was compacted (RFC-0044).
|
|
89
|
+
# Counts only, like everything else here; the summary text never lands.
|
|
90
|
+
def compaction_of(raw)
|
|
91
|
+
return nil unless raw.is_a?(Hash)
|
|
92
|
+
|
|
93
|
+
{ "upto" => int(raw[:upto] || raw["upto"]),
|
|
94
|
+
"runs" => int(raw[:runs] || raw["runs"]) }
|
|
95
|
+
end
|
|
96
|
+
|
|
87
97
|
# { name => sha256-hex }; names stringified, non-strings
|
|
88
98
|
# dropped. Absent when the caller passed nothing (a trace recorded before
|
|
89
99
|
# this feature has no key and the view guards on nil).
|
data/lib/insika/cron.rb
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Insika
|
|
4
|
+
# A cron expression, parsed. The engine's documented subset:
|
|
5
|
+
#
|
|
6
|
+
# minute hour day-of-month month day-of-week (5 fields, whitespace-separated)
|
|
7
|
+
#
|
|
8
|
+
# Per field: `*`, a single value, a range (N-M), a step (`*/N`, `N-M/N`,
|
|
9
|
+
# `N/N`), or a comma list of those. `?` and `*` both mean "any". `L`, `W`,
|
|
10
|
+
# `#` and month/day NAMES are NOT — the engine refuses them loudly at
|
|
11
|
+
# creation, never silently dropping a cron that only some dates understand.
|
|
12
|
+
#
|
|
13
|
+
# Day-of-week: 0-7, 7 == 0 (Sunday). When BOTH day fields are restricted, a
|
|
14
|
+
# date matches on EITHER (standard cron OR semantics).
|
|
15
|
+
#
|
|
16
|
+
# `next_after(time)` returns the first occurrence STRICTLY after `time`,
|
|
17
|
+
# materialized in the schedule's tz (returned as a UTC Time), or nil when
|
|
18
|
+
# the expression can never fire within 400 years (`0 0 31 2 *`). The bound
|
|
19
|
+
# keeps a syntactic-but-unreachable expression from hanging a pass.
|
|
20
|
+
class Cron
|
|
21
|
+
MONTHS_31 = [1, 3, 5, 7, 8, 10, 12].freeze
|
|
22
|
+
|
|
23
|
+
# One field: the sorted accepted values + whether it was a bare "*"
|
|
24
|
+
# (the day-of-week OR rule needs to know which fields are restricted).
|
|
25
|
+
class Field
|
|
26
|
+
def initialize(expression, range, field:)
|
|
27
|
+
@field = field
|
|
28
|
+
@range = range
|
|
29
|
+
@star = expression == "*"
|
|
30
|
+
@values = expression.split(",").flat_map { |term| parse_term(term) }.uniq.sort
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def star? = @star
|
|
34
|
+
|
|
35
|
+
def matches?(value) = @values.include?(value)
|
|
36
|
+
|
|
37
|
+
# -> Integer | nil: the smallest accepted value >= start (nil when none).
|
|
38
|
+
def at_or_after(start) = @values.find { |v| v >= start }
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def parse_term(term)
|
|
43
|
+
head, step = term.include?("/") ? term.split("/", 2) : [term, nil]
|
|
44
|
+
step = parse_step(step) if step
|
|
45
|
+
base = parse_base(head)
|
|
46
|
+
base = base.each_with_index.filter_map { |v, i| v if (i % step).zero? } if step
|
|
47
|
+
base.each { |v| validate(v) }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def parse_step(raw)
|
|
51
|
+
n = Integer(raw)
|
|
52
|
+
raise Insika::ValidationError, "cron #{@field} step must be a positive integer, got #{raw.inspect}" unless n.positive?
|
|
53
|
+
|
|
54
|
+
n
|
|
55
|
+
rescue ArgumentError
|
|
56
|
+
raise Insika::ValidationError,
|
|
57
|
+
"cron #{@field} step must be a positive integer, got #{raw.inspect}"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def parse_base(head)
|
|
61
|
+
if head.empty? || head == "?" || head == "*"
|
|
62
|
+
@range.to_a
|
|
63
|
+
elsif head.include?("-")
|
|
64
|
+
lo, hi = head.split("-", 2).map { |s| value(s) }
|
|
65
|
+
raise Insika::ValidationError, "cron #{@field} range reversed: #{head.inspect}" if lo > hi
|
|
66
|
+
|
|
67
|
+
(lo..hi).to_a
|
|
68
|
+
else
|
|
69
|
+
[value(head)]
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def value(raw)
|
|
74
|
+
v = Integer(raw)
|
|
75
|
+
v = 0 if @field == :dow && v == 7 # Sunday, the standard second spelling
|
|
76
|
+
v
|
|
77
|
+
rescue ArgumentError
|
|
78
|
+
raise Insika::ValidationError,
|
|
79
|
+
"cron #{@field} has an unparseable value: #{raw.inspect}"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def validate(v)
|
|
83
|
+
return if @range.cover?(v)
|
|
84
|
+
|
|
85
|
+
raise Insika::ValidationError,
|
|
86
|
+
"cron #{@field} value #{v} out of range (#{@range.inspect})"
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
attr_reader :expression
|
|
91
|
+
|
|
92
|
+
def initialize(expression)
|
|
93
|
+
@expression = expression.to_s.strip
|
|
94
|
+
fields = @expression.split(/\s+/)
|
|
95
|
+
if fields.size != 5
|
|
96
|
+
raise Insika::ValidationError,
|
|
97
|
+
"cron expression must have 5 fields (minute hour dom month dow), " \
|
|
98
|
+
"got #{fields.size}: #{@expression.inspect}"
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
minute, hour, dom, month, dow = fields
|
|
102
|
+
@minute = Field.new(minute, 0..59, field: :minute)
|
|
103
|
+
@hour = Field.new(hour, 0..23, field: :hour)
|
|
104
|
+
@dom = Field.new(dom, 1..31, field: :dom)
|
|
105
|
+
@month = Field.new(month, 1..12, field: :month)
|
|
106
|
+
@dow = Field.new(dow, 0..6, field: :dow)
|
|
107
|
+
freeze
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# -> Time (UTC) | nil: the first instant strictly after `time` whose
|
|
111
|
+
# wall-clock in `tz` matches. nil = the expression cannot fire within 400y.
|
|
112
|
+
def next_after(time, tz: "Etc/UTC")
|
|
113
|
+
Timezone.in_zone(tz, time) do
|
|
114
|
+
local = time.getlocal
|
|
115
|
+
cur = Cursor.new(local.year, local.month, local.day, local.hour, local.min)
|
|
116
|
+
advance_minute!(cur) # strictly after `time`
|
|
117
|
+
limit = cur.y + 400
|
|
118
|
+
while cur.y <= limit
|
|
119
|
+
if @month.matches?(cur.mo) && day_match?(cur)
|
|
120
|
+
if @hour.matches?(cur.h) && (mm = @minute.at_or_after(cur.mi))
|
|
121
|
+
return Time.local(cur.y, cur.mo, cur.d, cur.h, mm)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
nh = @hour.at_or_after(cur.h + 1)
|
|
125
|
+
if nh && (mm = @minute.at_or_after(0))
|
|
126
|
+
return Time.local(cur.y, cur.mo, cur.d, nh, mm)
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
advance_day!(cur)
|
|
130
|
+
cur.h = 0
|
|
131
|
+
cur.mi = 0
|
|
132
|
+
end
|
|
133
|
+
nil
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
private
|
|
138
|
+
|
|
139
|
+
# A date matches when a restricted day-of-month OR day-of-week accepts it
|
|
140
|
+
# (standard cron); a bare "*" on one side drops that side.
|
|
141
|
+
def day_match?(cur)
|
|
142
|
+
dom = @dom.matches?(cur.d)
|
|
143
|
+
dow = @dow.matches?(Time.utc(cur.y, cur.mo, cur.d).wday)
|
|
144
|
+
if @dom.star? && @dow.star?
|
|
145
|
+
true
|
|
146
|
+
elsif @dom.star?
|
|
147
|
+
dow
|
|
148
|
+
elsif @dow.star?
|
|
149
|
+
dom
|
|
150
|
+
else
|
|
151
|
+
dom || dow
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
Cursor = Struct.new(:y, :mo, :d, :h, :mi)
|
|
156
|
+
|
|
157
|
+
def advance_minute!(cur)
|
|
158
|
+
cur.mi += 1
|
|
159
|
+
if cur.mi > 59
|
|
160
|
+
cur.mi = 0
|
|
161
|
+
cur.h += 1
|
|
162
|
+
advance_day!(cur) if cur.h > 23
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def advance_day!(cur)
|
|
167
|
+
cur.d += 1
|
|
168
|
+
return if cur.d <= days_in_month(cur.y, cur.mo)
|
|
169
|
+
|
|
170
|
+
cur.d = 1
|
|
171
|
+
cur.mo += 1
|
|
172
|
+
if cur.mo > 12
|
|
173
|
+
cur.mo = 1
|
|
174
|
+
cur.y += 1
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def days_in_month(y, mo)
|
|
179
|
+
return 29 if mo == 2 && leap?(y)
|
|
180
|
+
return 28 if mo == 2
|
|
181
|
+
|
|
182
|
+
MONTHS_31.include?(mo) ? 31 : 30
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def leap?(y)
|
|
186
|
+
y % 4 == 0 && (y % 100 != 0 || y % 400 == 0)
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
end
|
|
@@ -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
|