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.
Files changed (204) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +296 -0
  3. data/README.md +48 -12
  4. data/bin/insika +725 -0
  5. data/bin/insika-router +87 -0
  6. data/docs/AGENTS.md +116 -406
  7. data/docs/API.md +5 -5
  8. data/docs/ARCHITECTURE.md +3 -2
  9. data/docs/ARTIFACTS.md +137 -0
  10. data/docs/BENCHMARK.md +2 -2
  11. data/docs/CHANNELS.md +14 -14
  12. data/docs/CONTEXT.md +63 -19
  13. data/docs/DEMO.md +80 -0
  14. data/docs/DEPLOY.md +87 -10
  15. data/docs/EMBEDDING.md +1 -1
  16. data/docs/EVALS.md +128 -3
  17. data/docs/FACTS.md +3 -3
  18. data/docs/HARVEST.md +5 -6
  19. data/docs/KNOWLEDGE.md +290 -0
  20. data/docs/LOADTEST.md +17 -29
  21. data/docs/MEDIA.md +128 -0
  22. data/docs/OBSERVABILITY.md +46 -12
  23. data/docs/OUTCOMES.md +137 -0
  24. data/docs/PLUGINS.md +51 -6
  25. data/docs/POLICY.md +222 -0
  26. data/docs/REFINEMENT.md +14 -9
  27. data/docs/RELEASING.md +4 -4
  28. data/docs/ROUTER.md +213 -0
  29. data/docs/RUNNING-LOCAL.md +5 -5
  30. data/docs/SCHEDULING.md +121 -0
  31. data/docs/SECURITY.md +23 -7
  32. data/docs/SKILLS.md +11 -2
  33. data/docs/SOAK.md +3 -3
  34. data/docs/TEMPLATES.md +134 -0
  35. data/docs/TOOLS.md +176 -27
  36. data/docs/WHY.md +1 -1
  37. data/docs/WORKFLOWS.md +2 -2
  38. data/docs/_includes/head_custom.html +5 -0
  39. data/docs/_includes/title.html +13 -0
  40. data/docs/_sass/color_schemes/insika.scss +32 -0
  41. data/docs/_sass/custom/custom.scss +199 -0
  42. data/docs/_sass/custom/setup.scss +26 -0
  43. data/docs/assets/img/favicon.svg +7 -0
  44. data/docs/assets/img/insika-mark.svg +7 -0
  45. data/docs/core-concepts.md +21 -0
  46. data/docs/domain.md +4 -4
  47. data/docs/improve.md +20 -0
  48. data/docs/index.md +8 -5
  49. data/docs/integrate.md +20 -0
  50. data/docs/operate.md +13 -6
  51. data/docs/prompts/ADD-TOOL.md +118 -0
  52. data/docs/prompts/DIAGNOSE-TURN.md +65 -0
  53. data/docs/prompts/GO-LIVE.md +138 -0
  54. data/docs/prompts/RUN-EXAMPLES.md +70 -0
  55. data/docs/reference.md +19 -0
  56. data/docs/ship.md +10 -2
  57. data/docs/start-here.md +18 -0
  58. data/lib/insika/agent_profile.rb +99 -17
  59. data/lib/insika/artifact_signing.rb +82 -0
  60. data/lib/insika/artifact_store.rb +160 -0
  61. data/lib/insika/channel_delivery.rb +1 -1
  62. data/lib/insika/chat_builder.rb +50 -19
  63. data/lib/insika/commands/agent_payload.rb +2 -2
  64. data/lib/insika/commands/backfill_knowledge.rb +145 -0
  65. data/lib/insika/commands/delete_artifact.rb +35 -0
  66. data/lib/insika/commands/delete_concept.rb +34 -0
  67. data/lib/insika/commands/delete_mcp.rb +6 -2
  68. data/lib/insika/commands/delete_tenant_data.rb +15 -3
  69. data/lib/insika/commands/gate_refinement.rb +1 -1
  70. data/lib/insika/commands/refresh_mcp_tools.rb +47 -0
  71. data/lib/insika/commands/restore_concept.rb +34 -0
  72. data/lib/insika/commands/seed_demo_data.rb +31 -0
  73. data/lib/insika/commands/upsert_mcp.rb +6 -3
  74. data/lib/insika/commands/write_concept.rb +57 -0
  75. data/lib/insika/compaction.rb +196 -0
  76. data/lib/insika/context/builder.rb +6 -2
  77. data/lib/insika/context/fragment.rb +4 -1
  78. data/lib/insika/context/priority.rb +8 -0
  79. data/lib/insika/context/providers/briefing.rb +53 -24
  80. data/lib/insika/context/providers/knowledge.rb +108 -0
  81. data/lib/insika/context/providers/prompt.rb +30 -24
  82. data/lib/insika/context/providers/session.rb +46 -10
  83. data/lib/insika/context_trace_store.rb +11 -1
  84. data/lib/insika/cron.rb +189 -0
  85. data/lib/insika/demo/agent_attrs.rb +43 -0
  86. data/lib/insika/demo/golden_cases.rb +81 -0
  87. data/lib/insika/demo/seeder.rb +336 -0
  88. data/lib/insika/doctor.rb +280 -17
  89. data/lib/insika/dsl/definition.rb +3 -2
  90. data/lib/insika/dsl/runtime.rb +64 -79
  91. data/lib/insika/dsl/server_boot.rb +23 -1
  92. data/lib/insika/dsl/system.rb +10 -2
  93. data/lib/insika/dsl.rb +103 -2
  94. data/lib/insika/env_schema.rb +21 -7
  95. data/lib/insika/evals/golden.rb +41 -4
  96. data/lib/insika/evals/judge.rb +47 -2
  97. data/lib/insika/evals/pairwise.rb +11 -0
  98. data/lib/insika/evals/persona.rb +98 -0
  99. data/lib/insika/evals/runner.rb +9 -0
  100. data/lib/insika/evals/simulator.rb +225 -0
  101. data/lib/insika/evals/transport.rb +84 -2
  102. data/lib/insika/event_stream.rb +10 -0
  103. data/lib/insika/executor.rb +295 -55
  104. data/lib/insika/followup_policy.rb +2 -25
  105. data/lib/insika/golden_store.rb +16 -1
  106. data/lib/insika/grounding/matcher.rb +1 -1
  107. data/lib/insika/knowledge.rb +680 -0
  108. data/lib/insika/knowledge_store.rb +140 -0
  109. data/lib/insika/loop_detector.rb +5 -34
  110. data/lib/insika/mcp_client.rb +94 -0
  111. data/lib/insika/mcp_json.rb +74 -0
  112. data/lib/insika/mcp_live_tool.rb +43 -0
  113. data/lib/insika/mcp_store.rb +98 -26
  114. data/lib/insika/mcp_tool_ingestor.rb +30 -8
  115. data/lib/insika/mcp_tool_registry.rb +100 -0
  116. data/lib/insika/media.rb +115 -31
  117. data/lib/insika/message_origin.rb +1 -1
  118. data/lib/insika/middleware.rb +9 -0
  119. data/lib/insika/onboarding.rb +17 -1
  120. data/lib/insika/outcome_store.rb +1 -1
  121. data/lib/insika/overlay_tool_registry.rb +37 -17
  122. data/lib/insika/packaging.rb +2 -2
  123. data/lib/insika/profile_source.rb +15 -1
  124. data/lib/insika/prompt_catalog.rb +10 -0
  125. data/lib/insika/retention.rb +36 -1
  126. data/lib/insika/router/app.rb +157 -0
  127. data/lib/insika/router/backend_pool.rb +98 -0
  128. data/lib/insika/router/hash_ring.rb +55 -0
  129. data/lib/insika/router/proxy_body.rb +34 -0
  130. data/lib/insika/router/session_key.rb +54 -0
  131. data/lib/insika/router.rb +18 -0
  132. data/lib/insika/schedule.rb +177 -0
  133. data/lib/insika/schedule_engine.rb +314 -0
  134. data/lib/insika/schedule_store.rb +208 -0
  135. data/lib/insika/server/app.rb +105 -15
  136. data/lib/insika/server/rack_app.rb +5 -1
  137. data/lib/insika/server/responses.rb +5 -5
  138. data/lib/insika/session_store.rb +34 -4
  139. data/lib/insika/settings_store.rb +8 -1
  140. data/lib/insika/skill_catalog.rb +12 -0
  141. data/lib/insika/soak/runner.rb +4 -4
  142. data/lib/insika/steer_injector.rb +21 -10
  143. data/lib/insika/studio/app.rb +591 -47
  144. data/lib/insika/studio/assets/dist/application.css +1 -1
  145. data/lib/insika/studio/assets/dist/application.js +21 -21
  146. data/lib/insika/studio/forms.rb +57 -5
  147. data/lib/insika/studio/nav_icons.rb +14 -1
  148. data/lib/insika/studio/views/_agent_tab_cache.erb +25 -0
  149. data/lib/insika/studio/views/_agent_tab_config.erb +514 -0
  150. data/lib/insika/studio/views/_agent_tab_history.erb +24 -0
  151. data/lib/insika/studio/views/_agent_tab_loops.erb +54 -0
  152. data/lib/insika/studio/views/_agent_tab_memory.erb +51 -0
  153. data/lib/insika/studio/views/_agent_tab_outcomes.erb +31 -0
  154. data/lib/insika/studio/views/_agent_tab_prompts.erb +108 -0
  155. data/lib/insika/studio/views/_agent_tab_skills.erb +38 -0
  156. data/lib/insika/studio/views/_agents_master.erb +44 -0
  157. data/lib/insika/studio/views/_message.erb +49 -32
  158. data/lib/insika/studio/views/agent_detail.erb +61 -820
  159. data/lib/insika/studio/views/agents.erb +70 -57
  160. data/lib/insika/studio/views/artifact.erb +23 -0
  161. data/lib/insika/studio/views/artifacts.erb +59 -0
  162. data/lib/insika/studio/views/evals.erb +2 -2
  163. data/lib/insika/studio/views/facts.erb +1 -1
  164. data/lib/insika/studio/views/funnel.erb +1 -1
  165. data/lib/insika/studio/views/home.erb +106 -67
  166. data/lib/insika/studio/views/knowledge.erb +123 -0
  167. data/lib/insika/studio/views/layout.erb +14 -11
  168. data/lib/insika/studio/views/mcp.erb +174 -80
  169. data/lib/insika/studio/views/session.erb +231 -177
  170. data/lib/insika/studio/views/settings.erb +50 -1
  171. data/lib/insika/studio/views/skills.erb +1 -1
  172. data/lib/insika/studio/views/tools.erb +24 -9
  173. data/lib/insika/telemetry/recorder.rb +49 -1
  174. data/lib/insika/templates/browser-agent/README.md +36 -0
  175. data/lib/insika/templates/browser-agent/agent.rb +49 -0
  176. data/lib/insika/templates/daily-digest/README.md +47 -0
  177. data/lib/insika/templates/daily-digest/agent.rb +77 -0
  178. data/lib/insika/templates/repo-explorer/README.md +36 -0
  179. data/lib/insika/templates/repo-explorer/agent.rb +45 -0
  180. data/lib/insika/templates/research-analyst/README.md +26 -0
  181. data/lib/insika/templates/research-analyst/agent.rb +68 -0
  182. data/lib/insika/templates/review-panel/README.md +20 -0
  183. data/lib/insika/templates/review-panel/agent.rb +50 -0
  184. data/lib/insika/templates/travel-planner/README.md +35 -0
  185. data/lib/insika/templates/travel-planner/agent.rb +87 -0
  186. data/lib/insika/templates.rb +112 -0
  187. data/lib/insika/tick.rb +24 -12
  188. data/lib/insika/timezone.rb +45 -0
  189. data/lib/insika/tool_batch.rb +67 -0
  190. data/lib/insika/tool_usage_report.rb +162 -0
  191. data/lib/insika/tools/generate_image.rb +52 -7
  192. data/lib/insika/tools/load_knowledge.rb +74 -0
  193. data/lib/insika/tools/run_persona_eval.rb +328 -0
  194. data/lib/insika/tools/save_artifact.rb +95 -0
  195. data/lib/insika/turn_budget.rb +91 -0
  196. data/lib/insika/turn_output.rb +1 -1
  197. data/lib/insika/turn_state.rb +15 -4
  198. data/lib/insika/version.rb +1 -1
  199. data/lib/insika/wiring/graph.rb +184 -12
  200. data/lib/insika/wiring/graph_chat.rb +102 -0
  201. data/lib/insika.rb +64 -0
  202. metadata +109 -5
  203. data/docs/build.md +0 -14
  204. 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 session_messages(request.session) if request.session
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 session_messages(session)
115
- @session_store.find(session.id)&.messages || []
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).
@@ -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