insika 0.3.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 +180 -0
- data/README.md +45 -10
- data/bin/insika +684 -0
- data/bin/insika-router +87 -0
- data/docs/AGENTS.md +94 -403
- data/docs/API.md +5 -5
- data/docs/ARCHITECTURE.md +3 -2
- data/docs/ARTIFACTS.md +95 -0
- data/docs/BENCHMARK.md +2 -2
- data/docs/CHANNELS.md +14 -14
- data/docs/CONTEXT.md +9 -7
- data/docs/DEMO.md +80 -0
- data/docs/DEPLOY.md +71 -3
- 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 +2 -2
- data/docs/MEDIA.md +128 -0
- data/docs/OBSERVABILITY.md +15 -10
- data/docs/OUTCOMES.md +137 -0
- data/docs/PLUGINS.md +51 -6
- data/docs/POLICY.md +216 -0
- data/docs/REFINEMENT.md +14 -9
- data/docs/RELEASING.md +4 -4
- data/docs/ROUTER.md +213 -0
- data/docs/RUNNING-LOCAL.md +3 -3
- data/docs/SCHEDULING.md +121 -0
- data/docs/SECURITY.md +22 -6
- data/docs/SKILLS.md +11 -2
- data/docs/SOAK.md +2 -2
- data/docs/TEMPLATES.md +134 -0
- data/docs/TOOLS.md +152 -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 +73 -16
- 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 +22 -2
- 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/context/priority.rb +2 -0
- data/lib/insika/context/providers/knowledge.rb +108 -0
- data/lib/insika/context/providers/prompt.rb +30 -24
- 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 +176 -8
- data/lib/insika/dsl/definition.rb +3 -2
- data/lib/insika/dsl/runtime.rb +60 -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 +16 -1
- 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 +83 -1
- data/lib/insika/event_stream.rb +10 -0
- data/lib/insika/executor.rb +231 -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/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 +8 -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 +1 -1
- data/lib/insika/skill_catalog.rb +12 -0
- data/lib/insika/steer_injector.rb +21 -10
- data/lib/insika/studio/app.rb +567 -45
- 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 +46 -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 +39 -1
- data/lib/insika/studio/views/skills.erb +1 -1
- data/lib/insika/studio/views/tools.erb +24 -9
- 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/tick.rb +24 -12
- data/lib/insika/timezone.rb +45 -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_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 +57 -0
- metadata +105 -5
- data/docs/build.md +0 -14
- data/docs/understand.md +0 -10
data/lib/insika/chat_builder.rb
CHANGED
|
@@ -15,7 +15,7 @@ module Insika
|
|
|
15
15
|
def initialize(tool_registry:, skill_catalog:, checkpoint_store:, event_stream:,
|
|
16
16
|
hooks:, tool_catalog: nil, memory_store: nil, subagent_runner: nil,
|
|
17
17
|
tool_trace_store: nil, media_runner: nil, session_store: nil,
|
|
18
|
-
contact_store: nil, followup_store: nil)
|
|
18
|
+
contact_store: nil, followup_store: nil, knowledge_store: nil)
|
|
19
19
|
@tool_registry = tool_registry
|
|
20
20
|
@skill_catalog = skill_catalog
|
|
21
21
|
@checkpoint_store = checkpoint_store
|
|
@@ -23,6 +23,10 @@ module Insika
|
|
|
23
23
|
@hooks = hooks
|
|
24
24
|
@tool_catalog = tool_catalog
|
|
25
25
|
@memory_store = memory_store
|
|
26
|
+
# load_knowledge is the knowledge-read system tool — wired only with
|
|
27
|
+
# @knowledge_store present AND profile.knowledge["retrieve"] (a double
|
|
28
|
+
# gate, like remember). nil = never wired (parity).
|
|
29
|
+
@knowledge_store = knowledge_store
|
|
26
30
|
# only to trace load_skill, which is not enveloped — nil = no trace (parity).
|
|
27
31
|
@tool_trace_store = tool_trace_store
|
|
28
32
|
# the object exposing #run_subagent (the Executor). nil = the
|
|
@@ -104,6 +108,15 @@ module Insika
|
|
|
104
108
|
event_stream: @event_stream, state: state)
|
|
105
109
|
end
|
|
106
110
|
|
|
111
|
+
# load_knowledge is a system default (outside the allowlist), like
|
|
112
|
+
# load_skill — wired only with @knowledge_store present AND the
|
|
113
|
+
# profile's opt-in (double gate, like remember). Reads the same
|
|
114
|
+
# (agent, tenant) scope the Knowledge context provider searches.
|
|
115
|
+
if @knowledge_store && Coercion.truthy?(state.profile.knowledge&.dig("retrieve"))
|
|
116
|
+
tools << Tools::LoadKnowledge.new(@knowledge_store, state.profile.id, tenant: state.tenant,
|
|
117
|
+
trace_recorder: @tool_trace_store, state: state)
|
|
118
|
+
end
|
|
119
|
+
|
|
107
120
|
# update_briefing / set_next_step are the briefing-write system tools
|
|
108
121
|
# — wired only with @session_store present AND
|
|
109
122
|
# profile.briefing_fields non-empty (double gate, like remember). Never
|
|
@@ -362,9 +375,16 @@ module Insika
|
|
|
362
375
|
# under concurrency `after_tool_result` labelled every result with whichever
|
|
363
376
|
# call started last.
|
|
364
377
|
state.current_tool_name = tool_call.name.to_s
|
|
365
|
-
|
|
378
|
+
case state.current_tool_name
|
|
379
|
+
when "load_skill"
|
|
366
380
|
args = tool_call.arguments || {}
|
|
367
381
|
emit.call(:skill_activated, { name: args["name"] || args[:name] })
|
|
382
|
+
when "load_knowledge"
|
|
383
|
+
# The adoption risk is the model never calling this tool, not what
|
|
384
|
+
# it injects — so the metric here is retrieval CALLS, tracked at
|
|
385
|
+
# the same point :skill_activated is.
|
|
386
|
+
args = tool_call.arguments || {}
|
|
387
|
+
emit.call(:knowledge_retrieved, { name: args["name"] || args[:name], agent: state.profile.id })
|
|
368
388
|
else
|
|
369
389
|
emit.call(:tool_call, { name: tool_call.name, arguments: tool_call.arguments })
|
|
370
390
|
end
|
|
@@ -13,9 +13,9 @@ module Insika
|
|
|
13
13
|
tools_allow_groups skills skills_eager context_providers workflows_allow policies
|
|
14
14
|
prompt_refs limits approvals_required capabilities subagents tools_deferred
|
|
15
15
|
memory prompt_caching tool_persistence tool_output_compression budget reliability alerts
|
|
16
|
-
routes stuck_signal outputs briefing_fields grounding funnel followup
|
|
16
|
+
routes stuck_signal outputs stt_prompt briefing_fields grounding funnel followup
|
|
17
17
|
params model_policy guardrails refinement capabilities_declared
|
|
18
|
-
edge_stream metadata distill harvest].freeze
|
|
18
|
+
edge_stream metadata distill harvest knowledge schedules].freeze
|
|
19
19
|
|
|
20
20
|
module_function
|
|
21
21
|
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "time"
|
|
4
|
+
|
|
5
|
+
module Insika
|
|
6
|
+
module Commands
|
|
7
|
+
# The recovery path (extraction is best-effort — a crash between a turn's
|
|
8
|
+
# terminal and the write loses that turn's concepts, not the conversation
|
|
9
|
+
# nor a previously learned one). Replays one agent's stored sessions
|
|
10
|
+
# through the SAME `Knowledge::Extractor` the per-turn hook uses.
|
|
11
|
+
#
|
|
12
|
+
# Synchronous — the CLI's own path (`insika knowledge:backfill`), same
|
|
13
|
+
# "no app boot" discipline as RunHarvest/RunDistillation: no task, no
|
|
14
|
+
# turn, nothing written but the concepts themselves.
|
|
15
|
+
class BackfillKnowledge
|
|
16
|
+
DEFAULT_MIN_MESSAGES = 2
|
|
17
|
+
|
|
18
|
+
def initialize(profiles:, knowledge_store:, session_store:, task_store:,
|
|
19
|
+
settings_store: nil, event_stream:, extractor_factory: nil, consolidator_factory: nil)
|
|
20
|
+
@profiles = ProfileSource.coerce(profiles)
|
|
21
|
+
@knowledge_store = knowledge_store
|
|
22
|
+
@session_store = session_store
|
|
23
|
+
@task_store = task_store
|
|
24
|
+
@settings_store = settings_store
|
|
25
|
+
@extractor_factory = extractor_factory ||
|
|
26
|
+
->(config) { Knowledge::ExtractorFactory.build(config, utility_model: utility_model) }
|
|
27
|
+
@consolidator_factory = consolidator_factory ||
|
|
28
|
+
->(config) { Knowledge::ConsolidatorFactory.build(config, utility_model: utility_model) }
|
|
29
|
+
@event_stream = event_stream
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# payload: { agent:, since?: ISO8601 }
|
|
33
|
+
# -> { backfilled: true, sessions: N, concepts: N, conflicts: N, dropped: {...} }
|
|
34
|
+
# | { backfilled: false, skipped: "disabled|no_model|no_sessions" }
|
|
35
|
+
def call(command)
|
|
36
|
+
p = AgentPayload.symbolize(command.payload)
|
|
37
|
+
agent = AgentPayload.presence(p[:agent])
|
|
38
|
+
raise Insika::ValidationError, "agent is required" if agent.nil?
|
|
39
|
+
|
|
40
|
+
profile = @profiles[agent] || (raise Insika::NotFoundError, "agent '#{agent}' not configured")
|
|
41
|
+
config = Coercion.deep_stringify(profile.knowledge)
|
|
42
|
+
return skip("disabled") if config.nil? || !Coercion.truthy?(config["extract"])
|
|
43
|
+
|
|
44
|
+
extractor = @extractor_factory.call(config)
|
|
45
|
+
return skip("no_model") if extractor.nil?
|
|
46
|
+
|
|
47
|
+
# Same resolved model as the extractor. nil (unresolvable) is still
|
|
48
|
+
# meaningful: Knowledge.write_concept's conservative default.
|
|
49
|
+
consolidator = @consolidator_factory.call(config)
|
|
50
|
+
|
|
51
|
+
sessions = resolve_sessions(agent, p)
|
|
52
|
+
return skip("no_sessions") if sessions.empty?
|
|
53
|
+
|
|
54
|
+
concepts = 0
|
|
55
|
+
conflicts = 0
|
|
56
|
+
dropped = Hash.new(0)
|
|
57
|
+
sessions.each do |s|
|
|
58
|
+
result = extractor.extract(prompt: build_prompt(config, s[:messages]))
|
|
59
|
+
result[:concepts].each do |c|
|
|
60
|
+
case write_concept(profile, s[:id], c, consolidator)[:verdict]
|
|
61
|
+
when :new, :related then concepts += 1
|
|
62
|
+
when :contradicting then conflicts += 1
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
result[:dropped].each { |k, v| dropped[k] += v }
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
emit(:knowledge_backfilled, agent: agent, sessions: sessions.size, concepts: concepts,
|
|
69
|
+
conflicts: conflicts, dropped: dropped.to_h)
|
|
70
|
+
{ backfilled: true, sessions: sessions.size, concepts: concepts, conflicts: conflicts, dropped: dropped.to_h }
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
def skip(reason) = { backfilled: false, skipped: reason }
|
|
76
|
+
|
|
77
|
+
# Every task of the agent, distinct session ids, newest first — the
|
|
78
|
+
# same reading `RunHarvest#window_session_ids` uses, without the
|
|
79
|
+
# last-N cap (a backfill is meant to be exhaustive over `--since`).
|
|
80
|
+
def resolve_sessions(agent, p)
|
|
81
|
+
since = AgentPayload.presence(p[:since])
|
|
82
|
+
tasks = @task_store.each_id.filter_map { |id| @task_store.find(id) }
|
|
83
|
+
.select { |t| task_agent(t) == agent }
|
|
84
|
+
.sort_by { |t| [t.created_at.to_s, t.id] }.reverse
|
|
85
|
+
tasks = tasks.select { |t| t.created_at.to_s >= since.to_s } if since
|
|
86
|
+
|
|
87
|
+
seen = {}
|
|
88
|
+
tasks.filter_map do |t|
|
|
89
|
+
sid = Coercion.presence(t.session_id)
|
|
90
|
+
next if sid.nil? || seen[sid]
|
|
91
|
+
|
|
92
|
+
seen[sid] = true
|
|
93
|
+
session = @session_store.find(sid)
|
|
94
|
+
next unless session && session.messages.size >= DEFAULT_MIN_MESSAGES
|
|
95
|
+
|
|
96
|
+
{ id: sid.to_s, messages: session.messages }
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def task_agent(task)
|
|
101
|
+
task.command.is_a?(Hash) ? task.command.dig("payload", "agent").to_s : ""
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def build_prompt(config, messages)
|
|
105
|
+
base = Coercion.presence(config["prompt"]) || Knowledge::DEFAULT_PROMPT
|
|
106
|
+
<<~PROMPT
|
|
107
|
+
#{base.rstrip}
|
|
108
|
+
|
|
109
|
+
## The conversation
|
|
110
|
+
|
|
111
|
+
#{render_transcript(messages)}
|
|
112
|
+
PROMPT
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def render_transcript(messages)
|
|
116
|
+
redacted, = Insika::Safety::Detectors.redact(
|
|
117
|
+
messages.each_with_index.map { |m, i| "[#{i}] #{m['role']}: #{m['content']}" }.join("\n")
|
|
118
|
+
)
|
|
119
|
+
redacted
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def write_concept(profile, session_id, concept, consolidator)
|
|
123
|
+
outcome = Knowledge.write_concept(store: @knowledge_store, agent_id: profile.id, concept: concept,
|
|
124
|
+
session_id: session_id, consolidator: consolidator)
|
|
125
|
+
case outcome[:verdict]
|
|
126
|
+
when :new, :related
|
|
127
|
+
emit(:knowledge_learned, name: outcome[:name], type: outcome[:type], agent: profile.id)
|
|
128
|
+
when :contradicting
|
|
129
|
+
emit(:knowledge_conflict, name: outcome[:name], agent: profile.id)
|
|
130
|
+
end
|
|
131
|
+
outcome
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def utility_model
|
|
135
|
+
return nil unless @settings_store
|
|
136
|
+
|
|
137
|
+
@settings_store.get["utility_model"]
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def emit(type, **data)
|
|
141
|
+
@event_stream.emit(Insika::Event.new(type: type, data: data, meta: { at: Time.now.utc.iso8601 }))
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "time"
|
|
4
|
+
|
|
5
|
+
module Insika
|
|
6
|
+
module Commands
|
|
7
|
+
# The Studio's delete on the Artifacts tab — a bus command, like every
|
|
8
|
+
# Studio mutation (the Studio never writes a store directly). Idempotent:
|
|
9
|
+
# an unknown id is a no-op, not an error.
|
|
10
|
+
class DeleteArtifact
|
|
11
|
+
def initialize(artifact_store:, event_stream:)
|
|
12
|
+
@artifact_store = artifact_store
|
|
13
|
+
@event_stream = event_stream
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# -> { deleted: id } | { deleted: nil } (unknown id).
|
|
17
|
+
def call(command)
|
|
18
|
+
id = Coercion.presence(command.payload[:id] || command.payload["id"])
|
|
19
|
+
raise ValidationError, "id is required" if id.nil?
|
|
20
|
+
|
|
21
|
+
deleted = @artifact_store.delete(id.to_s)
|
|
22
|
+
if deleted
|
|
23
|
+
@event_stream.emit(Insika::Event.new(
|
|
24
|
+
type: :artifact_deleted,
|
|
25
|
+
data: { id: id.to_s },
|
|
26
|
+
meta: { at: Time.now.utc.iso8601 }
|
|
27
|
+
))
|
|
28
|
+
{ deleted: id.to_s }
|
|
29
|
+
else
|
|
30
|
+
{ deleted: nil }
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "time"
|
|
4
|
+
|
|
5
|
+
module Insika
|
|
6
|
+
module Commands
|
|
7
|
+
# Control command: removes a learned concept from the KnowledgeStore.
|
|
8
|
+
# -> { name, agent, tenant, deleted: true }.
|
|
9
|
+
class DeleteConcept
|
|
10
|
+
def initialize(knowledge_store:, event_stream:)
|
|
11
|
+
@knowledge_store = knowledge_store
|
|
12
|
+
@event_stream = event_stream
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def call(command)
|
|
16
|
+
p = AgentPayload.symbolize(command.payload)
|
|
17
|
+
name = AgentPayload.presence(p[:name])
|
|
18
|
+
agent = AgentPayload.presence(p[:agent])
|
|
19
|
+
tenant = AgentPayload.presence(p[:tenant])
|
|
20
|
+
raise Insika::ValidationError, "name is required" if name.nil?
|
|
21
|
+
raise Insika::ValidationError, "agent is required" if agent.nil?
|
|
22
|
+
|
|
23
|
+
deleted = @knowledge_store.delete(agent, name, tenant: tenant)
|
|
24
|
+
raise Insika::NotFoundError, "concept '#{name}' not found for agent '#{agent}'" unless deleted
|
|
25
|
+
|
|
26
|
+
@event_stream.emit(Insika::Event.new(
|
|
27
|
+
type: :knowledge_deleted, data: { name: name, agent: agent },
|
|
28
|
+
meta: { at: Time.now.utc.iso8601 }
|
|
29
|
+
))
|
|
30
|
+
{ name: name, agent: agent, tenant: tenant, deleted: true }
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -5,11 +5,14 @@ require "time"
|
|
|
5
5
|
module Insika
|
|
6
6
|
module Commands
|
|
7
7
|
# Control command: removes an MCP instance from the
|
|
8
|
-
# McpStore. Idempotent (`existed: false` if there was none).
|
|
8
|
+
# McpStore. Idempotent (`existed: false` if there was none). Evicts any
|
|
9
|
+
# memoized client for this name — a removed instance stops answering
|
|
10
|
+
# tool calls right away, not after a restart. -> { existed: bool }.
|
|
9
11
|
class DeleteMcp
|
|
10
|
-
def initialize(mcp_store:, event_stream:)
|
|
12
|
+
def initialize(mcp_store:, event_stream:, mcp_registry: nil)
|
|
11
13
|
@mcp_store = mcp_store
|
|
12
14
|
@event_stream = event_stream
|
|
15
|
+
@mcp_registry = mcp_registry
|
|
13
16
|
end
|
|
14
17
|
|
|
15
18
|
def call(command)
|
|
@@ -18,6 +21,7 @@ module Insika
|
|
|
18
21
|
raise Insika::ValidationError, "name is required" if name.nil?
|
|
19
22
|
|
|
20
23
|
existed = @mcp_store.delete(name)
|
|
24
|
+
@mcp_registry&.evict(name)
|
|
21
25
|
@event_stream.emit(Insika::Event.new(
|
|
22
26
|
type: :mcp_deleted,
|
|
23
27
|
data: { name: name, existed: existed },
|
|
@@ -21,9 +21,9 @@ module Insika
|
|
|
21
21
|
context_trace_store: nil, model_visible_trace_store: nil,
|
|
22
22
|
outcome_store: nil, task_store: nil,
|
|
23
23
|
checkpoint_store: nil, outbox_store: nil, shadow_pairs: nil,
|
|
24
|
-
|
|
24
|
+
token_store: nil, funnel_store: nil, event_stream:,
|
|
25
25
|
followup_store: nil, contact_store: nil, proposal_store: nil,
|
|
26
|
-
harvest_store: nil)
|
|
26
|
+
harvest_store: nil, schedule_store: nil, artifact_store: nil)
|
|
27
27
|
@memory_store = memory_store
|
|
28
28
|
@token_store = token_store
|
|
29
29
|
@session_store = session_store
|
|
@@ -40,6 +40,8 @@ module Insika
|
|
|
40
40
|
@contact_store = contact_store # ; nil = nothing to sweep
|
|
41
41
|
@proposal_store = proposal_store # ; nil = nothing to sweep
|
|
42
42
|
@harvest_store = harvest_store # ; nil = nothing to sweep
|
|
43
|
+
@schedule_store = schedule_store # ; nil = nothing to sweep
|
|
44
|
+
@artifact_store = artifact_store # ; nil = nothing to sweep
|
|
43
45
|
@event_stream = event_stream
|
|
44
46
|
end
|
|
45
47
|
|
|
@@ -72,6 +74,12 @@ module Insika
|
|
|
72
74
|
# candidates reference sessions, and sessions carry the
|
|
73
75
|
# tenant prefix — the harvest rows die with the tenant (D11).
|
|
74
76
|
harvest = @harvest_store ? @harvest_store.purge(tenant: tenant) : 0
|
|
77
|
+
# recurring-schedule rows die with the tenant too (their
|
|
78
|
+
# message text is content, never kept behind an offboarded tenant).
|
|
79
|
+
schedules = @schedule_store ? @schedule_store.purge(tenant: tenant) : 0
|
|
80
|
+
# artifacts die with the tenant — a report is content, and the
|
|
81
|
+
# tenant binding is the isolation boundary (never a model-typed id).
|
|
82
|
+
artifacts = @artifact_store ? @artifact_store.purge(tenant: tenant) : 0
|
|
75
83
|
|
|
76
84
|
@event_stream.emit(Insika::Event.new(
|
|
77
85
|
type: :tenant_data_deleted,
|
|
@@ -83,12 +91,16 @@ module Insika
|
|
|
83
91
|
contacts: contacts,
|
|
84
92
|
proposals: proposals,
|
|
85
93
|
harvest: harvest,
|
|
94
|
+
schedules: schedules,
|
|
95
|
+
artifacts: artifacts,
|
|
86
96
|
tokens_revoked: tokens_revoked }.merge(purged),
|
|
87
97
|
meta: { at: Time.now.utc.iso8601 }
|
|
88
98
|
))
|
|
89
99
|
{ tenant: tenant, sessions: sessions, memory_records: memory_records,
|
|
90
100
|
outcomes: outcomes, funnel: funnel, followups: followups, contacts: contacts,
|
|
91
|
-
proposals: proposals, harvest: harvest,
|
|
101
|
+
proposals: proposals, harvest: harvest, schedules: schedules,
|
|
102
|
+
artifacts: artifacts,
|
|
103
|
+
tokens_revoked: tokens_revoked }.merge(purged)
|
|
92
104
|
end
|
|
93
105
|
end
|
|
94
106
|
end
|
|
@@ -12,7 +12,7 @@ module Insika
|
|
|
12
12
|
# Synchronous like `run_refinement`, and for the same reason: it is operator-paced
|
|
13
13
|
# work with a human waiting on the answer. It is NOT cheap — the replay is a real
|
|
14
14
|
# conversation per golden case — so it is fired deliberately, never on a timer
|
|
15
|
-
# (
|
|
15
|
+
# (a recurring timer may schedule OTHER turns; this gate stays human-paced).
|
|
16
16
|
#
|
|
17
17
|
# Payload:
|
|
18
18
|
# run_id (required) a run in :completed — the evidence the candidate answers
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "time"
|
|
4
|
+
|
|
5
|
+
module Insika
|
|
6
|
+
module Commands
|
|
7
|
+
# Control command: LIVE re-discovery of an MCP instance's tools.
|
|
8
|
+
# Delegates to McpToolRegistry#refresh (connect +
|
|
9
|
+
# tools/list + write McpStore#tools_cache) — TOOL EXECUTION never reads
|
|
10
|
+
# that cache; this command only refreshes what `entries`/Studio/doctor
|
|
11
|
+
# display. The CLI verb / API route / Studio button that call it are
|
|
12
|
+
# later work (PR3/PR4) — the command itself is the seam they'll share.
|
|
13
|
+
# -> { instance:, tools: [{name,description,inputSchema}, ...] }.
|
|
14
|
+
class RefreshMcpTools
|
|
15
|
+
def initialize(mcp_registry:, event_stream:)
|
|
16
|
+
@mcp_registry = mcp_registry
|
|
17
|
+
@event_stream = event_stream
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def call(command)
|
|
21
|
+
p = symbolize(command.payload)
|
|
22
|
+
name = Insika::Coercion.presence(p[:name]) ||
|
|
23
|
+
(raise Insika::ValidationError, "refresh_mcp_tools: 'name' (MCP instance) is required")
|
|
24
|
+
|
|
25
|
+
tools = @mcp_registry.refresh(name)
|
|
26
|
+
emit(name, tools)
|
|
27
|
+
{ instance: name, tools: tools }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
# Emits only the COUNT + the instance name (never a tool's schema, in
|
|
33
|
+
# case a schema description ever carries something sensitive): 0 leakage.
|
|
34
|
+
def emit(name, tools)
|
|
35
|
+
@event_stream.emit(Insika::Event.new(
|
|
36
|
+
type: :mcp_tools_refreshed,
|
|
37
|
+
data: { instance: name, tools: tools.size },
|
|
38
|
+
meta: { at: Time.now.utc.iso8601 }
|
|
39
|
+
))
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def symbolize(payload)
|
|
43
|
+
(payload || {}).each_with_object({}) { |(k, v), acc| acc[k.to_sym] = v }
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "time"
|
|
4
|
+
|
|
5
|
+
module Insika
|
|
6
|
+
module Commands
|
|
7
|
+
# Control command: restores an old version of a concept as the current
|
|
8
|
+
# content (a new write — linear history, like `RestoreSystemFile`).
|
|
9
|
+
# -> { name, agent, tenant, updated_at }.
|
|
10
|
+
class RestoreConcept
|
|
11
|
+
def initialize(knowledge_store:, event_stream:)
|
|
12
|
+
@knowledge_store = knowledge_store
|
|
13
|
+
@event_stream = event_stream
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def call(command)
|
|
17
|
+
p = AgentPayload.symbolize(command.payload)
|
|
18
|
+
name = AgentPayload.presence(p[:name])
|
|
19
|
+
agent = AgentPayload.presence(p[:agent])
|
|
20
|
+
tenant = AgentPayload.presence(p[:tenant])
|
|
21
|
+
raise Insika::ValidationError, "name is required" if name.nil?
|
|
22
|
+
raise Insika::ValidationError, "agent is required" if agent.nil?
|
|
23
|
+
raise Insika::ValidationError, "version is required" if p[:version].nil?
|
|
24
|
+
|
|
25
|
+
rec = @knowledge_store.restore(agent, name, p[:version], tenant: tenant)
|
|
26
|
+
@event_stream.emit(Insika::Event.new(
|
|
27
|
+
type: :knowledge_learned, data: { name: name, agent: agent },
|
|
28
|
+
meta: { at: Time.now.utc.iso8601 }
|
|
29
|
+
))
|
|
30
|
+
{ name: name, agent: agent, tenant: tenant, updated_at: rec["updated_at"] }
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "time"
|
|
4
|
+
|
|
5
|
+
module Insika
|
|
6
|
+
module Commands
|
|
7
|
+
# Studio/CLI-facing adapter over Insika::Demo::Seeder — the one path both
|
|
8
|
+
# `insika demo:seed` and the Studio's "Seed demo data" button use (same
|
|
9
|
+
# discipline as RunDistillation: one object, two front doors).
|
|
10
|
+
class SeedDemoData
|
|
11
|
+
def initialize(seeder:, event_stream:)
|
|
12
|
+
@seeder = seeder
|
|
13
|
+
@event_stream = event_stream
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# payload: { force: bool } -> the seeder's result Hash.
|
|
17
|
+
def call(command)
|
|
18
|
+
force = Coercion.truthy?(command.payload[:force] || command.payload["force"])
|
|
19
|
+
result = @seeder.seed!(force: force)
|
|
20
|
+
if result[:seeded]
|
|
21
|
+
@event_stream.emit(Insika::Event.new(
|
|
22
|
+
type: :demo_data_seeded,
|
|
23
|
+
data: { agent: result[:agent], counts: result[:counts] },
|
|
24
|
+
meta: { at: Time.now.utc.iso8601 }
|
|
25
|
+
))
|
|
26
|
+
end
|
|
27
|
+
result
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -8,17 +8,20 @@ module Insika
|
|
|
8
8
|
# (transport/command/url/enabled + `env` credentials) in the McpStore. The
|
|
9
9
|
# credentials are sentinel-aware per key (__OCULTO__ preserves; "" clears;
|
|
10
10
|
# a new string replaces). Returns the MASKED record (env never comes back in
|
|
11
|
-
# plaintext).
|
|
12
|
-
#
|
|
11
|
+
# plaintext). Evicts any memoized client for this name (mcp_registry is
|
|
12
|
+
# optional so a bare CLI/store-only caller need not wire one) so an edited
|
|
13
|
+
# command/url/env takes effect on the instance's next call — no restart.
|
|
13
14
|
class UpsertMcp
|
|
14
|
-
def initialize(mcp_store:, event_stream:)
|
|
15
|
+
def initialize(mcp_store:, event_stream:, mcp_registry: nil)
|
|
15
16
|
@mcp_store = mcp_store
|
|
16
17
|
@event_stream = event_stream
|
|
18
|
+
@mcp_registry = mcp_registry
|
|
17
19
|
end
|
|
18
20
|
|
|
19
21
|
def call(command)
|
|
20
22
|
p = AgentPayload.symbolize(command.payload)
|
|
21
23
|
masked = @mcp_store.upsert(p) # validates `name`; returns masked
|
|
24
|
+
@mcp_registry&.evict(masked["name"])
|
|
22
25
|
|
|
23
26
|
@event_stream.emit(Insika::Event.new(
|
|
24
27
|
type: :mcp_upserted,
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "time"
|
|
4
|
+
|
|
5
|
+
module Insika
|
|
6
|
+
module Commands
|
|
7
|
+
# Control command: writes a concept (complete concept markdown) into the
|
|
8
|
+
# KnowledgeStore. Validates the frontmatter (a `name` is required) before
|
|
9
|
+
# writing — same discipline as `WriteSkill`. The STORE POSITION (the
|
|
10
|
+
# `agent`/`name`/`tenant` triple) is the identity, exactly like a skill:
|
|
11
|
+
# an operator editing the raw markdown may leave the frontmatter `name:`
|
|
12
|
+
# unchanged even while promoting `provenance: observed` to `policy` —
|
|
13
|
+
# that promotion IS this command, no separate "promote" action needed.
|
|
14
|
+
# -> { name, agent, tenant, updated_at }.
|
|
15
|
+
class WriteConcept
|
|
16
|
+
def initialize(knowledge_store:, event_stream:)
|
|
17
|
+
@knowledge_store = knowledge_store
|
|
18
|
+
@event_stream = event_stream
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def call(command)
|
|
22
|
+
p = AgentPayload.symbolize(command.payload)
|
|
23
|
+
name = AgentPayload.presence(p[:name])
|
|
24
|
+
agent = AgentPayload.presence(p[:agent])
|
|
25
|
+
tenant = AgentPayload.presence(p[:tenant])
|
|
26
|
+
content = p[:content].to_s
|
|
27
|
+
raise Insika::ValidationError, "name is required" if name.nil?
|
|
28
|
+
raise Insika::ValidationError, "agent is required" if agent.nil?
|
|
29
|
+
|
|
30
|
+
parsed = validate_frontmatter!(content)
|
|
31
|
+
rec = @knowledge_store.write(agent, name, content, tenant: tenant)
|
|
32
|
+
emit(parsed[:type], name, agent)
|
|
33
|
+
{ name: name, agent: agent, tenant: tenant, updated_at: rec["updated_at"] }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def emit(type, name, agent)
|
|
39
|
+
@event_stream.emit(Insika::Event.new(
|
|
40
|
+
type: :knowledge_learned, data: { name: name, type: type, agent: agent },
|
|
41
|
+
meta: { at: Time.now.utc.iso8601 }
|
|
42
|
+
))
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Mirrors WriteSkill's frontmatter guard: without a parseable `name`,
|
|
46
|
+
# the concept would be unreachable by name once written (the Studio
|
|
47
|
+
# list keys on the store position, but the editor round-trips the
|
|
48
|
+
# content) — fail early, in the Command.
|
|
49
|
+
def validate_frontmatter!(content)
|
|
50
|
+
parsed = Insika::Knowledge::Concept.parse(content)
|
|
51
|
+
raise Insika::ValidationError, "concept missing YAML frontmatter (--- ... ---) or a `name`" if parsed.nil?
|
|
52
|
+
|
|
53
|
+
parsed
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -20,6 +20,8 @@ module Insika
|
|
|
20
20
|
PROMPT_REF = 90 # Prompt Catalog guardrails/refs (Prompt) — pinned
|
|
21
21
|
SKILL_BODY = 85 # <active_skill> trigger-matched body (SkillTrigger)
|
|
22
22
|
SKILL = 80 # <available_skills> level 1 (Skill)
|
|
23
|
+
KNOWLEDGE = 77 # <knowledge> top-K learned concepts (Knowledge) —
|
|
24
|
+
# below curated skills, above a single conversation's memory
|
|
23
25
|
MEMORY = 75 # <memory> read path (Memory)
|
|
24
26
|
TOOL_SEARCH = 70 # <available_tools> level 1 (ToolSearch)
|
|
25
27
|
BRIEFING = 65 # <briefing> session working state (Briefing) — D5:
|