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
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ruby_llm"
|
|
4
|
+
|
|
5
|
+
module Insika
|
|
6
|
+
module Tools
|
|
7
|
+
# `save_artifact` — the agent delivers a page (a report, a digest) to a
|
|
8
|
+
# destination that survives the channel: title + content in, a URL out,
|
|
9
|
+
# which it can include in a channel message ("today's report: <url>").
|
|
10
|
+
#
|
|
11
|
+
# A REGISTRY tool (not a ChatBuilder system tool): the per-agent
|
|
12
|
+
# `tools_allow` is the switch — registered `optional: true`, so an agent
|
|
13
|
+
# that did not name it cannot call it, and nothing global enables it.
|
|
14
|
+
# The bindings (tenant, agent, task) are the tool INSTANCE's, deposited
|
|
15
|
+
# by the Executor into `turn_context` like a data-tool's — never
|
|
16
|
+
# parameters the model types.
|
|
17
|
+
class SaveArtifact < RubyLLM::Tool
|
|
18
|
+
description "Save an HTML (or markdown / SVG) page — a report, a digest, " \
|
|
19
|
+
"a summary — and get back a URL to share. Use when the answer " \
|
|
20
|
+
"is a document the channel message cannot carry."
|
|
21
|
+
param :title, desc: "Short human-readable title (<= 200 chars)"
|
|
22
|
+
param :content, desc: "The page content: HTML with inline SVG for charts, " \
|
|
23
|
+
"markdown, or SVG. No scripts, no external resources."
|
|
24
|
+
param :mime, desc: "text/html (default), text/markdown, or image/svg+xml",
|
|
25
|
+
required: false
|
|
26
|
+
|
|
27
|
+
def name = "save_artifact"
|
|
28
|
+
|
|
29
|
+
def initialize(artifact_store:, base_url: nil, signing_key: nil, signing_ttl: nil,
|
|
30
|
+
max_bytes: nil, event_stream: nil)
|
|
31
|
+
@artifact_store = artifact_store
|
|
32
|
+
@base_url = base_url.to_s.sub(%r{/\z}, "")
|
|
33
|
+
@signing_key = signing_key
|
|
34
|
+
@signing_ttl = signing_ttl
|
|
35
|
+
@max_bytes = max_bytes
|
|
36
|
+
@event_stream = event_stream
|
|
37
|
+
super()
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Per-turn bindings, deposited by the Executor (ToolAssembly's
|
|
41
|
+
# `turn_context=` seam — the same one data-tools use). From the TURN,
|
|
42
|
+
# never from the model (the tenant-lesson: an id the model types is
|
|
43
|
+
# inventable, and the turn looks right anyway).
|
|
44
|
+
attr_reader :turn_context
|
|
45
|
+
|
|
46
|
+
def turn_context=(ctx)
|
|
47
|
+
@turn_context = (ctx || {}).each_with_object({}) { |(k, v), acc| acc[k.to_sym] = v }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def execute(title:, content:, mime: "text/html")
|
|
51
|
+
# the deployment's size cap rides the tool instance (INSIKA_ARTIFACT_
|
|
52
|
+
# MAX_BYTES at wiring); absent -> the store's default.
|
|
53
|
+
attrs = { tenant: binding_tenant, agent: @turn_context&.dig(:agent_id),
|
|
54
|
+
task_id: @turn_context&.dig(:task_id), title: title, mime: mime,
|
|
55
|
+
content: content }
|
|
56
|
+
attrs[:max_bytes] = @max_bytes if @max_bytes
|
|
57
|
+
record = @artifact_store.create(**attrs)
|
|
58
|
+
# the authenticated Studio URL is the always-on answer; the signed
|
|
59
|
+
# link exists ONLY when a key was configured (it shares OUTSIDE the
|
|
60
|
+
# Studio and expires).
|
|
61
|
+
url = Insika::ArtifactSigning.url_for(id: record.id, base: @base_url)
|
|
62
|
+
result = { id: record.id, url: url }
|
|
63
|
+
signed = Insika::ArtifactSigning.url_for(
|
|
64
|
+
id: record.id, base: @base_url, key: @signing_key, ttl: @signing_ttl
|
|
65
|
+
)
|
|
66
|
+
result[:signed_url] = signed if signed != url
|
|
67
|
+
emit(record)
|
|
68
|
+
result
|
|
69
|
+
rescue Insika::ValidationError => e
|
|
70
|
+
{ error: e.message }
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
# The tenant is the AGENT's tenant: the declared command tenant, or the
|
|
76
|
+
# deployment's "platform" in single-tenant — never the chat id (a report
|
|
77
|
+
# belongs to the agent, not to whichever chat ran it), never a parameter
|
|
78
|
+
# the model types.
|
|
79
|
+
def binding_tenant
|
|
80
|
+
declared = @turn_context&.dig(:command_tenant).to_s
|
|
81
|
+
declared.empty? ? "platform" : declared
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# :artifact_saved carries ids + title — NEVER the content (a report is
|
|
85
|
+
# customer data that rides the store, not the events).
|
|
86
|
+
def emit(record)
|
|
87
|
+
@event_stream&.emit(Insika::Event.new(
|
|
88
|
+
type: :artifact_saved,
|
|
89
|
+
data: { id: record.id, title: record.title },
|
|
90
|
+
meta: { task_id: @turn_context&.dig(:task_id) }
|
|
91
|
+
))
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
data/lib/insika/turn_output.rb
CHANGED
|
@@ -33,7 +33,7 @@ module Insika
|
|
|
33
33
|
# answer; the fragment is still on the stream as `:intermediate` for whoever is
|
|
34
34
|
# debugging it.
|
|
35
35
|
#
|
|
36
|
-
# `halt_when`
|
|
36
|
+
# `halt_when` is the one case where narration IS the turn: the tool
|
|
37
37
|
# already answered the customer, and the model's lead-in ("vou te inscrever
|
|
38
38
|
# agora") is all the turn is worth. That text is retained and published by the
|
|
39
39
|
# Executor's halt branch — see #halt_text.
|
data/lib/insika/turn_state.rb
CHANGED
|
@@ -31,10 +31,15 @@ module Insika
|
|
|
31
31
|
:message_source, # WS9: how the turn's message entered (:voice when it came
|
|
32
32
|
# from a transcribed audio part; nil = typed). Rides the
|
|
33
33
|
# terminal event additively.
|
|
34
|
-
:media_attachments, # WS9: the ask's attachments (image parts);
|
|
35
|
-
# The provider bills them; usage flows like
|
|
36
|
-
# The first
|
|
37
|
-
#
|
|
34
|
+
:media_attachments, # WS9: the ask's attachments (image + document parts);
|
|
35
|
+
# nil = none. The provider bills them; usage flows like
|
|
36
|
+
# any ask. The first URL of each kind also lands on
|
|
37
|
+
# turn_context (`{{ctx.image_url}}` / `{{ctx.document_url}}`).
|
|
38
|
+
:image_attachments, # WS9: the IMAGE subset of media_attachments (nil = no
|
|
39
|
+
# inbound image this turn). The default edit SOURCE for
|
|
40
|
+
# generate_image — kept separate from
|
|
41
|
+
# media_attachments because that one may also hold a
|
|
42
|
+
# document, which is never a paint(with:) source.
|
|
38
43
|
:output_parts, # WS9 (saída): media the turn GENERATED, as additive
|
|
39
44
|
# parts ({ type:, mime_type:, base64:, model: }). Rides
|
|
40
45
|
# the terminal event (output_parts) and the /v1/responses
|
|
@@ -114,6 +119,12 @@ module Insika
|
|
|
114
119
|
# rules the run started under.
|
|
115
120
|
attr_accessor :queue_policy
|
|
116
121
|
|
|
122
|
+
# Internal: the run's SteerInjector, or nil when steering is off (or the chat
|
|
123
|
+
# cannot host the boundary). Rebuilt per ATTEMPT — a WS3 fallback builds a new
|
|
124
|
+
# chat, and the injector belongs to the chat it appends to. The Executor reaches
|
|
125
|
+
# for it at the end of stage 6 to absorb whatever no tool boundary took.
|
|
126
|
+
attr_accessor :steer_injector
|
|
127
|
+
|
|
117
128
|
# Internal: true when this turn re-enters the pipeline via
|
|
118
129
|
# resume_task/recovery. The EdgeLimiter reads it to NEVER re-count or block a
|
|
119
130
|
# turn that was already admitted — a crash/pause under a saturated window must
|
data/lib/insika/version.rb
CHANGED
data/lib/insika/wiring/graph.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "graph_chat"
|
|
4
|
+
|
|
3
5
|
module Insika
|
|
4
6
|
module Wiring
|
|
5
7
|
# SHARED composition core for both roots: the minimal wiring
|
|
@@ -78,11 +80,21 @@ module Insika
|
|
|
78
80
|
# followup) so the engine, the tools and the Studio always have stores.
|
|
79
81
|
contact_store = Insika::ContactStore.new(store: backend)
|
|
80
82
|
followup_store = Insika::FollowupStore.new(store: backend)
|
|
83
|
+
# the recurring-schedule rows: one row per
|
|
84
|
+
# declared schedule, declaration + runtime state. Built unconditionally
|
|
85
|
+
# (empty and free when no pack declares schedules) so the engine, the
|
|
86
|
+
# doctor and the Studio always have a store.
|
|
87
|
+
schedule_store = Insika::ScheduleStore.new(store: backend)
|
|
81
88
|
# the distilled proposals + the latched dedup ledger +
|
|
82
89
|
# the per-session markers. Built unconditionally (empty and free when
|
|
83
90
|
# no pack declares distill) so the wiki, the engine and the LGPD
|
|
84
91
|
# purges always have a store.
|
|
85
92
|
proposal_store = Insika::ProposalStore.new(store: backend)
|
|
93
|
+
# concepts extracted from finished turns. Built
|
|
94
|
+
# unconditionally (empty and free when no pack declares knowledge)
|
|
95
|
+
# so the extractor, the Studio and the LGPD purges always have a
|
|
96
|
+
# store.
|
|
97
|
+
knowledge_store = Insika::KnowledgeStore.new(store: backend)
|
|
86
98
|
# refinement RUNS (reports over real traffic). Runtime data,
|
|
87
99
|
# same backend as sessions/tasks — the collector and the command that write it
|
|
88
100
|
# are the root's business (deployment-only, like the memory commands).
|
|
@@ -93,6 +105,11 @@ module Insika
|
|
|
93
105
|
# when no pack declares harvest) so the engine, the Studio and the
|
|
94
106
|
# LGPD purges always have a store.
|
|
95
107
|
harvest_store = Insika::HarvestStore.new(store: backend)
|
|
108
|
+
# the report destination — one record per run, no
|
|
109
|
+
# versioning. Built unconditionally (empty and free when nothing saves)
|
|
110
|
+
# so the tool, the routes, the retention sweep and the tenant purge
|
|
111
|
+
# always have a store.
|
|
112
|
+
artifact_store = Insika::ArtifactStore.new(store: backend)
|
|
96
113
|
|
|
97
114
|
code_tool_registry = Insika::ToolRegistry.new
|
|
98
115
|
workflow_registry = Insika::WorkflowRegistry.new
|
|
@@ -118,7 +135,10 @@ module Insika
|
|
|
118
135
|
funnel_store: funnel_store,
|
|
119
136
|
contact_store: contact_store,
|
|
120
137
|
followup_store: followup_store,
|
|
138
|
+
schedule_store: schedule_store,
|
|
121
139
|
proposal_store: proposal_store,
|
|
140
|
+
knowledge_store: knowledge_store,
|
|
141
|
+
artifact_store: artifact_store,
|
|
122
142
|
code_tool_registry: code_tool_registry,
|
|
123
143
|
workflow_registry: workflow_registry, policy_registry: policy_registry,
|
|
124
144
|
capability_registry: Insika::CapabilityRegistry.new, hooks: Insika::Hooks.new,
|
|
@@ -141,6 +161,13 @@ module Insika
|
|
|
141
161
|
def build(spine:, profiles:, tool_registry:, tool_catalog:, skill_catalog:,
|
|
142
162
|
prompt_catalog:, guardrails:, context_providers:, edge_limiter: nil,
|
|
143
163
|
executor_extra: {})
|
|
164
|
+
# the save_artifact tool: a REGISTRY tool registered OPTIONAL (the
|
|
165
|
+
# per-agent allowlist is the switch — an agent that did not name it
|
|
166
|
+
# cannot call it). Registered in the shared code registry, so both
|
|
167
|
+
# roots (base registry + the deployment's overlay, which composes it)
|
|
168
|
+
# expose it. The gem's require lives IN the factory block (loaded on
|
|
169
|
+
# the 1st instance, turn time -> wiring-load stays gem-free).
|
|
170
|
+
register_artifact_tool(spine)
|
|
144
171
|
spine.hooks.register(:task, after: guardrails.output_validator)
|
|
145
172
|
middleware = Insika::MiddlewareStack.new([edge_limiter, guardrails.input_guardrail].compact)
|
|
146
173
|
|
|
@@ -188,6 +215,9 @@ module Insika
|
|
|
188
215
|
# parity). The base graph wires it over the same backend the context
|
|
189
216
|
# trace uses.
|
|
190
217
|
model_visible_trace_store: Insika::ModelVisibleTraceStore.new(store: spine.backend),
|
|
218
|
+
# the per-turn extraction hook (nil = the loop is off,
|
|
219
|
+
# parity). Gated per-agent by `profile.knowledge`.
|
|
220
|
+
knowledge_store: spine.knowledge_store,
|
|
191
221
|
**executor_extra
|
|
192
222
|
)
|
|
193
223
|
|
|
@@ -233,7 +263,8 @@ module Insika
|
|
|
233
263
|
followup_store: spine.followup_store, # records age out too
|
|
234
264
|
contact_store: spine.contact_store, # cells age out too
|
|
235
265
|
proposal_store: spine.proposal_store, # proposals age out too
|
|
236
|
-
harvest_store: spine.harvest_store # candidates/log/snapshots too
|
|
266
|
+
harvest_store: spine.harvest_store, # candidates/log/snapshots too
|
|
267
|
+
artifact_store: spine.artifact_store # reports expire on their own TTL
|
|
237
268
|
),
|
|
238
269
|
interval: tick_env("INSIKA_TICK_INTERVAL") || Insika::Tick::DEFAULT_INTERVAL,
|
|
239
270
|
stale_after: tick_env("INSIKA_TICK_STALE_AFTER") || Insika::Tick::DEFAULT_STALE_AFTER
|
|
@@ -256,6 +287,16 @@ module Insika
|
|
|
256
287
|
contact_store: spine.contact_store, task_store: spine.task_store,
|
|
257
288
|
profiles: profiles, executor: executor, event_stream: spine.event_stream
|
|
258
289
|
)
|
|
290
|
+
# the recurring-schedule firer — the tick's
|
|
291
|
+
# fourth duty, wired here after the Tick, gated by its own claim
|
|
292
|
+
# window. Always built: a pack that declares `schedules` later finds
|
|
293
|
+
# its firer already on the tick (inert when no profile declares one).
|
|
294
|
+
executor.tick.schedule = Insika::ScheduleEngine.new(
|
|
295
|
+
store: spine.backend, schedule_store: spine.schedule_store,
|
|
296
|
+
task_store: spine.task_store, session_store: spine.session_store,
|
|
297
|
+
profiles: profiles, executor: executor, budget_ledger: spine.budget_ledger,
|
|
298
|
+
event_stream: spine.event_stream
|
|
299
|
+
)
|
|
259
300
|
# the distillation engine — the tick-duty that finds idle
|
|
260
301
|
# customer sessions and distills them on its own worker fiber (a
|
|
261
302
|
# supervisor child, started in serving mode next to the tick).
|
|
@@ -307,13 +348,16 @@ module Insika
|
|
|
307
348
|
memory_store: spine.memory_store, memory_audit_store: spine.memory_audit_store,
|
|
308
349
|
refinement_store: spine.refinement_store,
|
|
309
350
|
harvest_store: spine.harvest_store,
|
|
351
|
+
artifact_store: spine.artifact_store,
|
|
310
352
|
outbox_store: spine.outbox_store, shadow_pair_store: spine.shadow_pair_store,
|
|
311
353
|
inbound_log: spine.inbound_log,
|
|
312
354
|
outcome_store: spine.outcome_store,
|
|
313
355
|
funnel_store: spine.funnel_store,
|
|
314
356
|
contact_store: spine.contact_store,
|
|
315
357
|
followup_store: spine.followup_store,
|
|
358
|
+
schedule_store: spine.schedule_store,
|
|
316
359
|
proposal_store: spine.proposal_store,
|
|
360
|
+
knowledge_store: spine.knowledge_store,
|
|
317
361
|
token_store: spine.token_store, budget_ledger: spine.budget_ledger,
|
|
318
362
|
circuit_state: spine.circuit_state,
|
|
319
363
|
channel_registry: spine.channel_registry, channel_delivery: channel_delivery,
|
|
@@ -327,6 +371,49 @@ module Insika
|
|
|
327
371
|
)
|
|
328
372
|
end
|
|
329
373
|
|
|
374
|
+
# The boot step behind every root's `load_plugins` (Server::Boot's first
|
|
375
|
+
# named step). Discovers plugin manifests, validates them, requires the
|
|
376
|
+
# entries and registers their contributions into the ALREADY-BUILT graph —
|
|
377
|
+
# which is safe by construction: every seam the Loader touches resolves at
|
|
378
|
+
# turn time (registries and hooks by name, the middleware stack and the
|
|
379
|
+
# provider list per call, the catalogs on reload), and the step runs
|
|
380
|
+
# single-fiber before the server accepts connections.
|
|
381
|
+
#
|
|
382
|
+
# Roots, highest precedence first (duplicate id -> first root wins):
|
|
383
|
+
# 1. workspace — INSIKA_PLUGIN_DIR (the operator's override spot);
|
|
384
|
+
# 2. gems — whatever called Insika::Plugin.announce (default-enabled);
|
|
385
|
+
# 3. bundled — `bundled_root` (the repo's plugins/; requires enabling).
|
|
386
|
+
# INSIKA_PLUGINS enables workspace/bundled ids; INSIKA_PLUGINS_DISABLED is
|
|
387
|
+
# the absolute veto (deny wins, like every allowlist in the engine).
|
|
388
|
+
def load_plugins(graph, env: ENV, bundled_root: nil)
|
|
389
|
+
announced = Insika::Plugin.announced_roots
|
|
390
|
+
workspace = Insika::EnvSchema.read("INSIKA_PLUGIN_DIR", env)
|
|
391
|
+
roots = ([workspace] + announced + [bundled_root]).compact.map { |d| File.expand_path(d.to_s) }
|
|
392
|
+
|
|
393
|
+
result = Insika::Plugin::Loader.new(
|
|
394
|
+
roots: roots,
|
|
395
|
+
registries: {
|
|
396
|
+
tools: graph.code_tool_registry, workflows: graph.workflow_registry,
|
|
397
|
+
policies: graph.policy_registry, capabilities: graph.capability_registry,
|
|
398
|
+
channels: graph.channel_registry, hooks: graph.hooks,
|
|
399
|
+
middleware: graph.middleware, context_providers: graph.context_providers
|
|
400
|
+
},
|
|
401
|
+
enabled: plugin_csv(Insika::EnvSchema.read("INSIKA_PLUGINS", env)),
|
|
402
|
+
disabled: plugin_csv(Insika::EnvSchema.read("INSIKA_PLUGINS_DISABLED", env)),
|
|
403
|
+
announced_roots: announced, event_stream: graph.event_stream
|
|
404
|
+
).load_all
|
|
405
|
+
|
|
406
|
+
# Plugin knowledge joins at the LOWEST precedence: an operator's
|
|
407
|
+
# workspace/authored skill always beats a same-named plugin one.
|
|
408
|
+
graph.skill_catalog.add_roots(result[:skill_dirs])
|
|
409
|
+
graph.prompt_catalog.add_roots(result[:prompt_dirs])
|
|
410
|
+
result
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
def plugin_csv(value)
|
|
414
|
+
value.to_s.split(",").map(&:strip).reject(&:empty?)
|
|
415
|
+
end
|
|
416
|
+
|
|
330
417
|
# The CORE command surface every root needs — turn essentials + the operator
|
|
331
418
|
# controls (pause/approve) the Studio dispatches. Registering pause_task/
|
|
332
419
|
# approve_action HERE is the crux of: it retires the config.ru:28-34 patch.
|
|
@@ -411,16 +498,18 @@ module Insika
|
|
|
411
498
|
model_visible_trace_store: Insika::ModelVisibleTraceStore.new(store: spine.backend),
|
|
412
499
|
outcome_store: spine.outcome_store,
|
|
413
500
|
funnel_store: spine.funnel_store, # the fold dies with the tenant
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
501
|
+
followup_store: spine.followup_store,
|
|
502
|
+
contact_store: spine.contact_store,
|
|
503
|
+
proposal_store: spine.proposal_store,
|
|
504
|
+
harvest_store: spine.harvest_store,
|
|
505
|
+
schedule_store: spine.schedule_store,
|
|
506
|
+
artifact_store: spine.artifact_store,
|
|
507
|
+
task_store: spine.task_store, checkpoint_store: spine.checkpoint_store,
|
|
508
|
+
outbox_store: spine.outbox_store,
|
|
509
|
+
shadow_pairs: spine.shadow_pair_store,
|
|
510
|
+
token_store: spine.token_store, # revoked BEFORE the sweep
|
|
511
|
+
event_stream: spine.event_stream
|
|
512
|
+
))
|
|
424
513
|
# the operator's baseline freeze — the number
|
|
425
514
|
# read. Synchronous control command, dispatched from the Studio.
|
|
426
515
|
bus.register(:freeze_funnel_baseline,
|
|
@@ -434,6 +523,12 @@ module Insika
|
|
|
434
523
|
bus.register(:cancel_followup,
|
|
435
524
|
Insika::Commands::CancelFollowup.new(followup_store: spine.followup_store,
|
|
436
525
|
event_stream: spine.event_stream))
|
|
526
|
+
# the report destination's Studio mutation — delete one
|
|
527
|
+
# artifact (the only write on the Artifacts tab; a bus command, like
|
|
528
|
+
# every Studio mutation).
|
|
529
|
+
bus.register(:delete_artifact,
|
|
530
|
+
Insika::Commands::DeleteArtifact.new(artifact_store: spine.artifact_store,
|
|
531
|
+
event_stream: spine.event_stream))
|
|
437
532
|
bus.register(:revoke_contact,
|
|
438
533
|
Insika::Commands::RevokeContact.new(contact_store: spine.contact_store,
|
|
439
534
|
followup_store: spine.followup_store,
|
|
@@ -493,6 +588,23 @@ module Insika
|
|
|
493
588
|
# code path; the engine keeps the automatic pass when the root does
|
|
494
589
|
# not pass a runner here (nil = the command is not on the bus).
|
|
495
590
|
bus.register(:run_distillation, run_distillation) if run_distillation
|
|
591
|
+
# the demo seed's ONE path — the CLI's `insika demo:seed` builds
|
|
592
|
+
# the same Insika::Demo::Seeder itself (no executor, no bus, from
|
|
593
|
+
# Graph.spine alone), and the Studio's "Seed demo data" button
|
|
594
|
+
# dispatches this bus command; nothing else writes the demo agent.
|
|
595
|
+
config_store = Insika::ConfigStore.new(store: spine.backend)
|
|
596
|
+
demo_seeder = Insika::Demo::Seeder.new(
|
|
597
|
+
profiles: profiles, store: spine.backend, session_store: spine.session_store,
|
|
598
|
+
task_store: spine.task_store, outcome_store: spine.outcome_store,
|
|
599
|
+
funnel_store: spine.funnel_store, followup_store: spine.followup_store,
|
|
600
|
+
refinement_store: spine.refinement_store, pending_action_store: spine.pending_action_store,
|
|
601
|
+
proposal_store: spine.proposal_store, memory_store: spine.memory_store,
|
|
602
|
+
golden_store: Insika::GoldenStore.new(config_store: config_store),
|
|
603
|
+
baseline_store: Insika::BaselineStore.new(config_store: config_store),
|
|
604
|
+
event_stream: spine.event_stream
|
|
605
|
+
)
|
|
606
|
+
bus.register(:seed_demo_data,
|
|
607
|
+
Insika::Commands::SeedDemoData.new(seeder: demo_seeder, event_stream: spine.event_stream))
|
|
496
608
|
bus
|
|
497
609
|
end
|
|
498
610
|
|
|
@@ -505,7 +617,9 @@ module Insika
|
|
|
505
617
|
:harvest_store,
|
|
506
618
|
:token_store, :budget_ledger, :circuit_state, :outbox_store, :shadow_pair_store,
|
|
507
619
|
:inbound_log, :outcome_store, :funnel_store,
|
|
508
|
-
:contact_store, :followup_store, :proposal_store,
|
|
620
|
+
:contact_store, :followup_store, :schedule_store, :proposal_store,
|
|
621
|
+
:knowledge_store,
|
|
622
|
+
:artifact_store,
|
|
509
623
|
:code_tool_registry, :workflow_registry, :policy_registry, :capability_registry, :hooks,
|
|
510
624
|
:channel_registry, keyword_init: true
|
|
511
625
|
)
|
|
@@ -520,7 +634,10 @@ module Insika
|
|
|
520
634
|
:inbound_log, :token_store,
|
|
521
635
|
:budget_ledger, :circuit_state, :outcome_store, :funnel_store,
|
|
522
636
|
:contact_store, :followup_store, :proposal_store,
|
|
637
|
+
:schedule_store,
|
|
523
638
|
:harvest_store,
|
|
639
|
+
:knowledge_store,
|
|
640
|
+
:artifact_store,
|
|
524
641
|
:channel_registry, :channel_delivery,
|
|
525
642
|
:code_tool_registry, :tool_registry, :workflow_registry, :policy_registry, :capability_registry,
|
|
526
643
|
:tool_catalog, :skill_catalog, :prompt_catalog,
|
|
@@ -531,6 +648,61 @@ module Insika
|
|
|
531
648
|
) do
|
|
532
649
|
def durable? = backend.is_a?(Insika::Stores::SQLite)
|
|
533
650
|
end
|
|
651
|
+
|
|
652
|
+
# The save_artifact tool, registered OPTIONAL on the shared code registry
|
|
653
|
+
# (the per-agent allowlist is the switch). The signing/base config is
|
|
654
|
+
# bound at registration time from the environment — the tool instance is
|
|
655
|
+
# per-deployment, and the tenant/agent/task bindings arrive per-turn via
|
|
656
|
+
# the deposited turn context.
|
|
657
|
+
# `run_persona_eval` (C3.1): a QA agent's own probe against a SIBLING
|
|
658
|
+
# agent of THIS SAME graph. Registered OPTIONAL on the shared code
|
|
659
|
+
# registry, same switch as `save_artifact` — but unlike that one, this
|
|
660
|
+
# is NOT called unconditionally from `build`: it needs a `golden_store`
|
|
661
|
+
# and a `settings_store` (the persona model + judge panel), and neither
|
|
662
|
+
# lives on the spine (the base/minimal wiring builds without either, the
|
|
663
|
+
# same way it builds without a Studio) — so each root calls this itself,
|
|
664
|
+
# once it has both. `config/deployment.rb` (round1, and the actual
|
|
665
|
+
# production `config.ru` root) and `DSL::Runtime` both do.
|
|
666
|
+
#
|
|
667
|
+
# `graph` is the ALREADY-RETURNED `Graph::Result` — bus/executor fully
|
|
668
|
+
# assembled by the time a caller has it to pass here, so `GraphChat.new`
|
|
669
|
+
# needs no laziness of its own (the tool's own factory block is what is
|
|
670
|
+
# lazy — this method just registers it).
|
|
671
|
+
# `llm:` — the caller's own RubyLLM::Context, when it has one
|
|
672
|
+
# (DSL::Runtime does; config/deployment.rb does not, and reads the
|
|
673
|
+
# process-wide RubyLLM constant like the rest of that root already
|
|
674
|
+
# does). Forwarded as-is so the tool's OWN persona/judge calls spend
|
|
675
|
+
# the right credentials instead of silently defaulting to nil.
|
|
676
|
+
def register_persona_eval_tool(graph, golden_store:, settings_store:, llm: nil)
|
|
677
|
+
graph.code_tool_registry.register("run_persona_eval", optional: true) do
|
|
678
|
+
require "ruby_llm"
|
|
679
|
+
require_relative "../tools/run_persona_eval"
|
|
680
|
+
Insika::Tools::RunPersonaEval.new(
|
|
681
|
+
golden_store: golden_store, profiles: graph.profiles, tool_registry: graph.tool_registry,
|
|
682
|
+
runtime: GraphChat.new(graph: graph), graph: graph, settings_store: settings_store,
|
|
683
|
+
budget_ledger: graph.budget_ledger, event_stream: graph.event_stream, llm: llm
|
|
684
|
+
)
|
|
685
|
+
end
|
|
686
|
+
end
|
|
687
|
+
|
|
688
|
+
def register_artifact_tool(spine)
|
|
689
|
+
signing_key = Insika::EnvSchema.read("INSIKA_ARTIFACT_SIGNING_KEY")
|
|
690
|
+
signing_ttl = Insika::EnvSchema.read("INSIKA_ARTIFACT_SIGNING_TTL")
|
|
691
|
+
max_bytes = Insika::EnvSchema.read("INSIKA_ARTIFACT_MAX_BYTES")
|
|
692
|
+
base_url = Insika::Coercion.presence(Insika::EnvSchema.read("INSIKA_PUBLIC_URL"))
|
|
693
|
+
spine.code_tool_registry.register("save_artifact", optional: true) do
|
|
694
|
+
require "ruby_llm"
|
|
695
|
+
require_relative "../tools/save_artifact"
|
|
696
|
+
Insika::Tools::SaveArtifact.new(
|
|
697
|
+
artifact_store: spine.artifact_store,
|
|
698
|
+
base_url: base_url,
|
|
699
|
+
signing_key: signing_key,
|
|
700
|
+
signing_ttl: signing_ttl&.to_i,
|
|
701
|
+
max_bytes: max_bytes&.to_i,
|
|
702
|
+
event_stream: spine.event_stream
|
|
703
|
+
)
|
|
704
|
+
end
|
|
705
|
+
end
|
|
534
706
|
end
|
|
535
707
|
end
|
|
536
708
|
end
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Insika
|
|
4
|
+
module Wiring
|
|
5
|
+
# A synchronous, in-process "chat" seam over an assembled `Graph::Result`:
|
|
6
|
+
# dispatch a Command, drain the event stream to its terminal event, hand
|
|
7
|
+
# back the outcome. Extracted from `DSL::Runtime#chat`/`#run_workflow`
|
|
8
|
+
# (C3.1) so a graph built OUTSIDE the DSL — `config/deployment.rb`, the
|
|
9
|
+
# actual production/round1 composition root `config.ru` boots — can hand a
|
|
10
|
+
# `#chat`-capable object to anything that needs one (today:
|
|
11
|
+
# `run_persona_eval`'s `Evals::GraphTransport` seam), without a second copy
|
|
12
|
+
# of this dispatch+drain plumbing. `DSL::Runtime` now delegates here too —
|
|
13
|
+
# one implementation, exercised by both roots.
|
|
14
|
+
class GraphChat
|
|
15
|
+
TERMINAL = %i[task_completed task_failed task_cancelled].freeze
|
|
16
|
+
|
|
17
|
+
def initialize(graph:)
|
|
18
|
+
@graph = graph
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# One turn, in-process -> the assistant's text. `agent:` is REQUIRED —
|
|
22
|
+
# this seam has no notion of "the default agent" (that is a DSL::Runtime
|
|
23
|
+
# concept, filled in by its own caller before delegating here). Raises
|
|
24
|
+
# Insika::Error on a failed/cancelled turn, or the command's own
|
|
25
|
+
# rejection (unknown agent, bad schema) verbatim.
|
|
26
|
+
def chat(message, agent:, session_id: nil, timeout: nil)
|
|
27
|
+
command = Insika::Command.build(
|
|
28
|
+
:send_message, { agent: agent.to_s, message: message, session_id: session_id },
|
|
29
|
+
transport: :cli
|
|
30
|
+
)
|
|
31
|
+
run_command(command, session_id: session_id, timeout: timeout)[:text]
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Dispatch + drain, generic over any Command — the seam a workflow run
|
|
35
|
+
# also needs (a :trigger_workflow Command carries :output, not :text).
|
|
36
|
+
# Subscribes BEFORE dispatching (the fiber may emit eagerly) and binds to
|
|
37
|
+
# the dispatched task. A synchronous handler error (unknown agent/
|
|
38
|
+
# workflow, bad input against the schema) propagates untouched — the
|
|
39
|
+
# caller sees the real ValidationError/NotFoundError, not a turn failure.
|
|
40
|
+
def run_command(command, session_id:, timeout:)
|
|
41
|
+
require "async"
|
|
42
|
+
outcome = {}
|
|
43
|
+
rejected = nil
|
|
44
|
+
Async do |task|
|
|
45
|
+
serving = !session_id.nil?
|
|
46
|
+
@graph.executor.supervised = true if serving
|
|
47
|
+
ensure_session(session_id) if session_id
|
|
48
|
+
sub = @graph.event_stream.subscribe
|
|
49
|
+
res = begin
|
|
50
|
+
@graph.bus.dispatch(command)
|
|
51
|
+
rescue StandardError => e
|
|
52
|
+
# CAPTURED, not re-raised inside the reactor: letting it escape the
|
|
53
|
+
# Async block logs "Task may have ended with unhandled exception" —
|
|
54
|
+
# alarming noise for a DOCUMENTED rejection (a schema violation, an
|
|
55
|
+
# unknown agent). Re-raised verbatim below, outside the reactor.
|
|
56
|
+
sub.close
|
|
57
|
+
rejected = e
|
|
58
|
+
next
|
|
59
|
+
end
|
|
60
|
+
sub.bind(task_id: res[:task_id])
|
|
61
|
+
drain(sub, outcome, task, timeout)
|
|
62
|
+
teardown_serving if serving
|
|
63
|
+
end.wait
|
|
64
|
+
raise rejected if rejected
|
|
65
|
+
raise Insika::Error, "turn #{outcome[:error]}" if outcome[:error]
|
|
66
|
+
|
|
67
|
+
outcome
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
# Reads the stream until this turn reaches a terminal event, then stops.
|
|
73
|
+
# A workflow run also carries its typed OUTPUT, which is not the turn text.
|
|
74
|
+
def drain(sub, outcome, task, timeout)
|
|
75
|
+
reader = task.async do
|
|
76
|
+
sub.each do |ev|
|
|
77
|
+
case ev.type
|
|
78
|
+
when :task_completed then outcome[:text] = ev.data[:content].to_s
|
|
79
|
+
when :workflow_completed then outcome[:output] = ev.data[:output]
|
|
80
|
+
when :task_failed then outcome[:error] = "failed: #{ev.data[:message] || ev.data[:error]}"
|
|
81
|
+
when :task_cancelled then outcome[:error] = "cancelled"
|
|
82
|
+
end
|
|
83
|
+
sub.close if TERMINAL.include?(ev.type)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
timeout ? task.with_timeout(timeout) { reader.wait } : reader.wait
|
|
87
|
+
rescue Async::TimeoutError
|
|
88
|
+
outcome[:error] = "timed out after #{timeout}s"
|
|
89
|
+
sub.close
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def ensure_session(session_id)
|
|
93
|
+
@graph.session_store.find(session_id) || @graph.session_store.create(id: session_id, vars: {})
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def teardown_serving
|
|
97
|
+
@graph.executor.stop_session_actors
|
|
98
|
+
@graph.executor.instance_variable_get(:@supervisor)&.stop
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|