brainiac 0.0.13 → 0.0.14
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/Gemfile.lock +2 -2
- data/bin/brainiac +18 -27
- data/lib/brainiac/config.rb +36 -3
- data/lib/brainiac/helpers.rb +20 -1
- data/lib/brainiac/intent.rb +179 -0
- data/lib/brainiac/routes/api.rb +61 -0
- data/lib/brainiac/version.rb +1 -1
- data/lib/brainiac.rb +1 -0
- data/receiver.rb +6 -0
- data/templates/brainiac.json.example +8 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: '04080c41c24921ed739b0d28ad15ce61b8eb5b5c12935d0cb0b1d72c6b0deeab'
|
|
4
|
+
data.tar.gz: bbb1dceb84ecaba924ada9189e8b63320828c035da4a6ff466ba8b47c4c30708
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: '0699a2277c78aa2c33e57b6ee4394ac7be8d27bc483a9f3866dd27e644e40fbdea4b85168b3452c6cbbd4fce42da6e282a9609e93541802b2c751f9845c55ea2'
|
|
7
|
+
data.tar.gz: adcd7eab6269a33b278499e3b873b072f488f7b06b18378507fba456f883b1c525b7dd8d8ce82b13e6464ab5184007e0887dfa9a82a69910f8f8d0164660ff9b
|
data/Gemfile.lock
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
brainiac (0.0.
|
|
4
|
+
brainiac (0.0.14)
|
|
5
5
|
puma (~> 7.2)
|
|
6
6
|
rackup (~> 2.3)
|
|
7
7
|
sinatra (~> 4.1)
|
|
@@ -84,7 +84,7 @@ DEPENDENCIES
|
|
|
84
84
|
CHECKSUMS
|
|
85
85
|
ast (2.4.3) sha256=954615157c1d6a382bc27d690d973195e79db7f55e9765ac7c481c60bdb4d383
|
|
86
86
|
base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b
|
|
87
|
-
brainiac (0.0.
|
|
87
|
+
brainiac (0.0.14)
|
|
88
88
|
json (2.19.9) sha256=9b9025b7cdddafa38d316eca0b2358488e42d417045c1b90d216a9fefe46b79a
|
|
89
89
|
language_server-protocol (3.17.0.5) sha256=fd1e39a51a28bf3eec959379985a72e296e9f9acfce46f6a79d31ca8760803cc
|
|
90
90
|
lint_roller (1.1.0) sha256=2c0c845b632a7d172cb849cc90c1bce937a28c5c8ccccb50dfd46a485003cc87
|
data/bin/brainiac
CHANGED
|
@@ -655,7 +655,8 @@ def list_projects
|
|
|
655
655
|
default_marker = config["default"] ? " (default)" : ""
|
|
656
656
|
puts " #{key}#{default_marker}"
|
|
657
657
|
puts " Path: #{config["repo_path"]}"
|
|
658
|
-
|
|
658
|
+
tags = config["tags"] || config["project_tags"]&.join(", ")
|
|
659
|
+
puts " Tags: #{tags}" if tags
|
|
659
660
|
puts " GitHub: #{config["github_repo"]}" if config["github_repo"]
|
|
660
661
|
puts " Agent: #{config["agent_cli"]} (model: #{config["agent_model"]})"
|
|
661
662
|
puts
|
|
@@ -2295,54 +2296,44 @@ when "update", "upgrade"
|
|
|
2295
2296
|
end
|
|
2296
2297
|
|
|
2297
2298
|
puts "Updating #{gem_name}..."
|
|
2298
|
-
|
|
2299
|
-
|
|
2299
|
+
output, status = Open3.capture2e("gem install #{gem_name}")
|
|
2300
|
+
puts output
|
|
2301
|
+
unless status.success?
|
|
2300
2302
|
puts ""
|
|
2301
2303
|
puts "Failed to update #{gem_name}."
|
|
2302
2304
|
exit 1
|
|
2303
2305
|
end
|
|
2304
2306
|
|
|
2305
|
-
#
|
|
2306
|
-
|
|
2307
|
-
new_version = begin
|
|
2308
|
-
spec = Gem::Specification.find_by_name(gem_name)
|
|
2309
|
-
spec.version.to_s
|
|
2310
|
-
rescue Gem::MissingSpecError
|
|
2311
|
-
"unknown"
|
|
2312
|
-
end
|
|
2307
|
+
# Parse installed version from gem install output
|
|
2308
|
+
new_version = output[/Successfully installed #{Regexp.escape(gem_name)}-(\S+)/, 1]
|
|
2313
2309
|
|
|
2314
|
-
if
|
|
2315
|
-
puts "✓ #{gem_name} is already at the latest version (#{new_version})"
|
|
2316
|
-
else
|
|
2310
|
+
if new_version
|
|
2317
2311
|
puts "✓ Updated #{gem_name}: #{current_version || "new"} → #{new_version}"
|
|
2318
2312
|
puts " Restart the server to activate: brainiac restart"
|
|
2313
|
+
else
|
|
2314
|
+
puts "✓ #{gem_name} is already at the latest version (#{current_version})"
|
|
2319
2315
|
end
|
|
2320
2316
|
else
|
|
2321
2317
|
# Update brainiac itself: brainiac update
|
|
2322
2318
|
current_version = BRAINIAC_VERSION
|
|
2323
2319
|
|
|
2324
2320
|
puts "Updating brainiac gem..."
|
|
2325
|
-
|
|
2326
|
-
|
|
2321
|
+
output, status = Open3.capture2e("gem install brainiac")
|
|
2322
|
+
puts output
|
|
2323
|
+
unless status.success?
|
|
2327
2324
|
puts ""
|
|
2328
2325
|
puts "Failed to update brainiac."
|
|
2329
2326
|
exit 1
|
|
2330
2327
|
end
|
|
2331
2328
|
|
|
2332
|
-
#
|
|
2333
|
-
|
|
2334
|
-
new_version = begin
|
|
2335
|
-
spec = Gem::Specification.find_by_name("brainiac")
|
|
2336
|
-
spec.version.to_s
|
|
2337
|
-
rescue Gem::MissingSpecError
|
|
2338
|
-
"unknown"
|
|
2339
|
-
end
|
|
2329
|
+
# Parse installed version from gem install output
|
|
2330
|
+
new_version = output[/Successfully installed brainiac-(\S+)/, 1]
|
|
2340
2331
|
|
|
2341
|
-
if
|
|
2342
|
-
puts "✓ brainiac is already at the latest version (#{new_version})"
|
|
2343
|
-
else
|
|
2332
|
+
if new_version
|
|
2344
2333
|
puts "✓ Updated brainiac: #{current_version} → #{new_version}"
|
|
2345
2334
|
puts " Restart the server to apply: brainiac restart"
|
|
2335
|
+
else
|
|
2336
|
+
puts "✓ brainiac is already at the latest version (#{current_version})"
|
|
2346
2337
|
end
|
|
2347
2338
|
end
|
|
2348
2339
|
|
data/lib/brainiac/config.rb
CHANGED
|
@@ -38,15 +38,48 @@ end
|
|
|
38
38
|
BRAINIAC_CONFIG = load_brainiac_config
|
|
39
39
|
|
|
40
40
|
# --- Default agent name ---
|
|
41
|
-
# Priority: AI_AGENT_NAME env var → brainiac.json "default_agent" → error.
|
|
42
|
-
|
|
41
|
+
# Priority: AI_AGENT_NAME env var → brainiac.json "default_agent" → first agent in agents.json → error.
|
|
42
|
+
def resolve_default_agent
|
|
43
|
+
# 1. Env var
|
|
44
|
+
name = ENV.fetch("AI_AGENT_NAME", nil)
|
|
45
|
+
return name if name
|
|
46
|
+
|
|
47
|
+
# 2. brainiac.json
|
|
48
|
+
name = BRAINIAC_CONFIG["default_agent"]
|
|
49
|
+
return name if name
|
|
50
|
+
|
|
51
|
+
# 3. First agent in agents.json
|
|
52
|
+
if File.exist?(AGENT_REGISTRY_FILE)
|
|
53
|
+
agents = begin
|
|
54
|
+
JSON.parse(File.read(AGENT_REGISTRY_FILE))
|
|
55
|
+
rescue StandardError
|
|
56
|
+
{}
|
|
57
|
+
end
|
|
58
|
+
first_agent = agents.values.first
|
|
59
|
+
if first_agent
|
|
60
|
+
agent_name = first_agent["fizzy_name"] || first_agent["display_name"] || agents.keys.first.capitalize
|
|
61
|
+
warn <<~MSG
|
|
62
|
+
[Brainiac] No default agent configured — using "#{agent_name}" (first agent in agents.json).
|
|
63
|
+
To set explicitly, either:
|
|
64
|
+
export AI_AGENT_NAME="#{agent_name}"
|
|
65
|
+
Or add to ~/.brainiac/brainiac.json:
|
|
66
|
+
{ "default_agent": "#{agent_name}" }
|
|
67
|
+
MSG
|
|
68
|
+
return agent_name
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# 4. Nothing found
|
|
43
73
|
raise <<~MSG
|
|
44
|
-
No default agent configured
|
|
74
|
+
No default agent configured and no agents found in #{AGENT_REGISTRY_FILE}.
|
|
75
|
+
Set one of:
|
|
45
76
|
1. Environment variable: export AI_AGENT_NAME="YourAgent"
|
|
46
77
|
2. In ~/.brainiac/brainiac.json: { "default_agent": "YourAgent" }
|
|
47
78
|
MSG
|
|
48
79
|
end
|
|
49
80
|
|
|
81
|
+
AI_AGENT_NAME = resolve_default_agent
|
|
82
|
+
|
|
50
83
|
LOG_LEVEL = ENV.fetch("LOG_LEVEL", "info").downcase
|
|
51
84
|
LOG = Logger.new($stdout)
|
|
52
85
|
LOG.level = case LOG_LEVEL
|
data/lib/brainiac/helpers.rb
CHANGED
|
@@ -399,8 +399,27 @@ def resolve_resume(resume, resolved, chdir)
|
|
|
399
399
|
false
|
|
400
400
|
end
|
|
401
401
|
|
|
402
|
+
# Check if intent detection says to skip dispatching the agent.
|
|
403
|
+
# Returns true if the message should be skipped, false otherwise.
|
|
404
|
+
# Only runs if a raw message is provided, an agent is named, and intent is enabled.
|
|
405
|
+
def intent_skip?(message, agent_name:, source: nil, channel: nil)
|
|
406
|
+
return false unless message && agent_name && intent_config["enabled"]
|
|
407
|
+
|
|
408
|
+
intent_channel = channel || source&.to_s || "conversation"
|
|
409
|
+
unless check_intent(message, agent_name: agent_name, channel: intent_channel)
|
|
410
|
+
LOG.info "[Intent] Skipping dispatch for #{agent_name} — message classified as not requiring response"
|
|
411
|
+
return true
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
false
|
|
415
|
+
end
|
|
416
|
+
|
|
402
417
|
def run_agent(prompt, project_config:, chdir: nil, log_name: "agent", model: nil, effort: nil, agent_name: nil, card_number: nil, comment_id: nil,
|
|
403
|
-
source: nil, source_context: {}, skip_column_move: false, cli_provider: nil, resume: false
|
|
418
|
+
source: nil, source_context: {}, skip_column_move: false, cli_provider: nil, resume: false,
|
|
419
|
+
message: nil, channel: nil)
|
|
420
|
+
# Intent gate: if a raw message is provided, check whether the agent should respond.
|
|
421
|
+
return nil if intent_skip?(message, agent_name: agent_name, source: source, channel: channel)
|
|
422
|
+
|
|
404
423
|
resolved = resolve_project_cli_config(project_config, cli_provider_override: cli_provider, agent_name: agent_name)
|
|
405
424
|
chdir ||= resolved["repo_path"]
|
|
406
425
|
model ||= resolved["agent_model"]
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Local LLM-based intent detection.
|
|
4
|
+
#
|
|
5
|
+
# Uses a lightweight local model (via Ollama) to classify whether an incoming
|
|
6
|
+
# message requires an agent to respond. This saves expensive API tokens by
|
|
7
|
+
# filtering out messages that are just humans chatting, reactions, or noise.
|
|
8
|
+
#
|
|
9
|
+
# Also detects when an agent's own response implies pending work — if the agent
|
|
10
|
+
# said "I'll do X" but the session ended without completing the work, this
|
|
11
|
+
# signals that the agent should be re-dispatched.
|
|
12
|
+
#
|
|
13
|
+
# Configuration (in ~/.brainiac/brainiac.json):
|
|
14
|
+
#
|
|
15
|
+
# "intent": {
|
|
16
|
+
# "enabled": true,
|
|
17
|
+
# "endpoint": "http://localhost:11434/api/generate",
|
|
18
|
+
# "model": "gemma3:4b",
|
|
19
|
+
# "timeout": 10,
|
|
20
|
+
# "temperature": 0.1
|
|
21
|
+
# }
|
|
22
|
+
#
|
|
23
|
+
# Plugins use this via:
|
|
24
|
+
# requires_response = check_intent(message, agent_name: "Galen", context: "Discord thread")
|
|
25
|
+
# return unless requires_response
|
|
26
|
+
#
|
|
27
|
+
# Returns true (agent should respond), false (skip), or true on any failure
|
|
28
|
+
# (fail-open so we never accidentally ignore a real request).
|
|
29
|
+
|
|
30
|
+
INTENT_CONFIG_DEFAULTS = {
|
|
31
|
+
"enabled" => false,
|
|
32
|
+
"endpoint" => "http://localhost:11434/api/generate",
|
|
33
|
+
"model" => "gemma3:4b",
|
|
34
|
+
"timeout" => 10,
|
|
35
|
+
"temperature" => 0.1
|
|
36
|
+
}.freeze
|
|
37
|
+
|
|
38
|
+
INTENT_PROMPT_TEMPLATE = <<~PROMPT
|
|
39
|
+
You are a message router for a {{CHANNEL}}. An AI agent named {{AGENT_NAME}} is participating in this conversation. Your job: determine if the latest message requires {{AGENT_NAME}} to take action or respond.
|
|
40
|
+
|
|
41
|
+
Rules:
|
|
42
|
+
- If the message is giving {{AGENT_NAME}} instructions, asking {{AGENT_NAME}} a question, or continuing a conversation WITH {{AGENT_NAME}} → yes
|
|
43
|
+
- If the message is humans talking to each other and {{AGENT_NAME}} is not being addressed → no
|
|
44
|
+
- If the message is a simple acknowledgment (like "thanks", "ok", "got it") directed at {{AGENT_NAME}}'s previous work → no
|
|
45
|
+
- If the message is asking a question to another person or agent → no
|
|
46
|
+
- If uncertain, lean toward yes (better to respond unnecessarily than miss a request)
|
|
47
|
+
|
|
48
|
+
Respond with ONLY "yes" or "no" — nothing else.
|
|
49
|
+
|
|
50
|
+
Latest message:
|
|
51
|
+
{{MESSAGE}}
|
|
52
|
+
PROMPT
|
|
53
|
+
|
|
54
|
+
PENDING_WORK_PROMPT_TEMPLATE = <<~PROMPT
|
|
55
|
+
You are analyzing a message posted by an AI agent named {{AGENT_NAME}}. Your job: determine if this message indicates the agent intends to do more work that hasn't been completed yet.
|
|
56
|
+
|
|
57
|
+
Rules:
|
|
58
|
+
- If the agent says it will do something, is about to start work, or promises a follow-up action → yes
|
|
59
|
+
- If the agent is reporting completed work, summarizing what was done, or delivering a final answer → no
|
|
60
|
+
- If the agent is asking a clarifying question and waiting for a human response → no
|
|
61
|
+
- If the agent says things like "give me a sec", "I'll implement this", "let me do that", "working on it" → yes
|
|
62
|
+
- If uncertain, lean toward no (avoid unnecessary re-dispatches)
|
|
63
|
+
|
|
64
|
+
Respond with ONLY "yes" or "no" — nothing else.
|
|
65
|
+
|
|
66
|
+
Agent's message:
|
|
67
|
+
{{MESSAGE}}
|
|
68
|
+
PROMPT
|
|
69
|
+
|
|
70
|
+
# Load intent config from brainiac.json, merging with defaults.
|
|
71
|
+
def intent_config
|
|
72
|
+
raw = BRAINIAC_CONFIG["intent"] || {}
|
|
73
|
+
INTENT_CONFIG_DEFAULTS.merge(raw)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Check whether a message requires an agent to respond.
|
|
77
|
+
#
|
|
78
|
+
# @param message [String] The message text to classify
|
|
79
|
+
# @param agent_name [String] The agent being addressed
|
|
80
|
+
# @param channel [String] Context description (e.g., "Discord thread", "Fizzy card comment")
|
|
81
|
+
# @return [Boolean] true if the agent should respond, false if it can be skipped
|
|
82
|
+
def check_intent(message, agent_name:, channel: "conversation")
|
|
83
|
+
config = intent_config
|
|
84
|
+
return true unless config["enabled"]
|
|
85
|
+
return true if message.nil? || message.strip.empty?
|
|
86
|
+
|
|
87
|
+
prompt = INTENT_PROMPT_TEMPLATE
|
|
88
|
+
.gsub("{{AGENT_NAME}}", agent_name)
|
|
89
|
+
.gsub("{{CHANNEL}}", channel)
|
|
90
|
+
.gsub("{{MESSAGE}}", message.strip)
|
|
91
|
+
|
|
92
|
+
LOG.info "[Intent] Checking intent for #{agent_name} (#{channel}): #{message.strip.slice(0, 80)}..."
|
|
93
|
+
response = query_local_llm(prompt, config)
|
|
94
|
+
result = positive_intent?(response)
|
|
95
|
+
LOG.info "[Intent] Result: #{result ? "RESPOND" : "SKIP"} (model: #{config["model"]})"
|
|
96
|
+
result
|
|
97
|
+
rescue StandardError => e
|
|
98
|
+
LOG.warn "[Intent] Classification failed (fail-open): #{e.message}"
|
|
99
|
+
true
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Query the local LLM via Ollama's HTTP API.
|
|
103
|
+
#
|
|
104
|
+
# @param prompt [String] The classification prompt
|
|
105
|
+
# @param config [Hash] Intent configuration
|
|
106
|
+
# @return [String] Raw response text from the model
|
|
107
|
+
def query_local_llm(prompt, config)
|
|
108
|
+
uri = URI(config["endpoint"])
|
|
109
|
+
payload = {
|
|
110
|
+
model: config["model"],
|
|
111
|
+
prompt: prompt,
|
|
112
|
+
stream: false,
|
|
113
|
+
options: { temperature: config["temperature"] }
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
117
|
+
http.open_timeout = config["timeout"]
|
|
118
|
+
http.read_timeout = config["timeout"]
|
|
119
|
+
|
|
120
|
+
request = Net::HTTP::Post.new(uri.path, "Content-Type" => "application/json")
|
|
121
|
+
request.body = JSON.generate(payload)
|
|
122
|
+
|
|
123
|
+
response = http.request(request)
|
|
124
|
+
raise "Ollama returned #{response.code}: #{response.body&.slice(0, 200)}" unless response.is_a?(Net::HTTPSuccess)
|
|
125
|
+
|
|
126
|
+
body = JSON.parse(response.body)
|
|
127
|
+
body["response"] || ""
|
|
128
|
+
rescue Errno::ECONNREFUSED
|
|
129
|
+
raise "Ollama not running at #{config["endpoint"]}"
|
|
130
|
+
rescue Net::OpenTimeout, Net::ReadTimeout
|
|
131
|
+
raise "Ollama timed out after #{config["timeout"]}s"
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Parse the LLM's yes/no response into a boolean.
|
|
135
|
+
# Fail-open: anything that isn't clearly "no" returns true.
|
|
136
|
+
#
|
|
137
|
+
# @param response [String] Raw response from the model
|
|
138
|
+
# @return [Boolean]
|
|
139
|
+
def positive_intent?(response)
|
|
140
|
+
cleaned = response.to_s.strip.downcase.gsub(/[^a-z]/, "")
|
|
141
|
+
cleaned != "no"
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Check whether an agent's own message implies pending work that wasn't completed.
|
|
145
|
+
# Used to detect when an agent posted "I'll do X" but the session ended without
|
|
146
|
+
# actually doing the work — signaling the agent should be re-dispatched.
|
|
147
|
+
#
|
|
148
|
+
# Fail-closed: returns false on any error (don't re-dispatch on classification failure).
|
|
149
|
+
#
|
|
150
|
+
# @param message [String] The agent's posted message
|
|
151
|
+
# @param agent_name [String] The agent who posted it
|
|
152
|
+
# @return [Boolean] true if the message implies uncommitted work
|
|
153
|
+
def check_pending_work(message, agent_name:)
|
|
154
|
+
config = intent_config
|
|
155
|
+
return false unless config["enabled"]
|
|
156
|
+
return false if message.nil? || message.strip.empty?
|
|
157
|
+
|
|
158
|
+
prompt = PENDING_WORK_PROMPT_TEMPLATE
|
|
159
|
+
.gsub("{{AGENT_NAME}}", agent_name)
|
|
160
|
+
.gsub("{{MESSAGE}}", message.strip)
|
|
161
|
+
|
|
162
|
+
response = query_local_llm(prompt, config)
|
|
163
|
+
result = pending_work_detected?(response)
|
|
164
|
+
LOG.info "[Intent] Pending work detected in #{agent_name}'s message" if result
|
|
165
|
+
result
|
|
166
|
+
rescue StandardError => e
|
|
167
|
+
LOG.warn "[Intent] Pending work check failed (fail-closed): #{e.message}"
|
|
168
|
+
false
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Parse the LLM's yes/no response for pending work detection.
|
|
172
|
+
# Fail-closed: anything that isn't clearly "yes" returns false.
|
|
173
|
+
#
|
|
174
|
+
# @param response [String] Raw response from the model
|
|
175
|
+
# @return [Boolean]
|
|
176
|
+
def pending_work_detected?(response)
|
|
177
|
+
cleaned = response.to_s.strip.downcase.gsub(/[^a-z]/, "")
|
|
178
|
+
cleaned == "yes"
|
|
179
|
+
end
|
data/lib/brainiac/routes/api.rb
CHANGED
|
@@ -354,6 +354,67 @@ post "/api/cron/reload" do
|
|
|
354
354
|
{ status: "reloaded", jobs: CRON_JOBS.size }.to_json
|
|
355
355
|
end
|
|
356
356
|
|
|
357
|
+
# --- Intent ---
|
|
358
|
+
|
|
359
|
+
get "/api/intent/config" do
|
|
360
|
+
content_type :json
|
|
361
|
+
config = intent_config
|
|
362
|
+
status = if config["enabled"]
|
|
363
|
+
begin
|
|
364
|
+
uri = URI(config["endpoint"].sub("/api/generate", "/api/tags"))
|
|
365
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
366
|
+
http.open_timeout = 3
|
|
367
|
+
http.read_timeout = 3
|
|
368
|
+
response = http.get(uri.path)
|
|
369
|
+
if response.is_a?(Net::HTTPSuccess)
|
|
370
|
+
models = JSON.parse(response.body).fetch("models", []).map { |m| m["name"] }
|
|
371
|
+
model_available = models.any? { |m| m.start_with?(config["model"].split(":").first) }
|
|
372
|
+
model_available ? "ok" : "model_not_found"
|
|
373
|
+
else
|
|
374
|
+
"ollama_error"
|
|
375
|
+
end
|
|
376
|
+
rescue Errno::ECONNREFUSED
|
|
377
|
+
"ollama_not_running"
|
|
378
|
+
rescue StandardError => e
|
|
379
|
+
"error: #{e.message}"
|
|
380
|
+
end
|
|
381
|
+
else
|
|
382
|
+
"disabled"
|
|
383
|
+
end
|
|
384
|
+
config.merge("status" => status).to_json
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
post "/api/intent/check" do
|
|
388
|
+
content_type :json
|
|
389
|
+
request.body.rewind
|
|
390
|
+
payload = JSON.parse(request.body.read)
|
|
391
|
+
|
|
392
|
+
message = payload["message"]
|
|
393
|
+
agent = payload["agent"] || AI_AGENT_NAME
|
|
394
|
+
channel = payload["channel"] || "conversation"
|
|
395
|
+
|
|
396
|
+
halt 400, { error: "Missing 'message' field" }.to_json unless message && !message.empty?
|
|
397
|
+
|
|
398
|
+
result = check_intent(message, agent_name: agent, channel: channel)
|
|
399
|
+
{ should_respond: result, agent: agent, channel: channel }.to_json
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
post "/api/intent/pending-work" do
|
|
403
|
+
content_type :json
|
|
404
|
+
request.body.rewind
|
|
405
|
+
payload = JSON.parse(request.body.read)
|
|
406
|
+
|
|
407
|
+
message = payload["message"]
|
|
408
|
+
agent = payload["agent"] || AI_AGENT_NAME
|
|
409
|
+
|
|
410
|
+
halt 400, { error: "Missing 'message' field" }.to_json unless message && !message.empty?
|
|
411
|
+
|
|
412
|
+
result = check_pending_work(message, agent_name: agent)
|
|
413
|
+
{ pending_work: result, agent: agent }.to_json
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
# --- Cron Logs ---
|
|
417
|
+
|
|
357
418
|
get "/api/cron/logs" do
|
|
358
419
|
content_type :json
|
|
359
420
|
job_id = params["id"]
|
data/lib/brainiac/version.rb
CHANGED
data/lib/brainiac.rb
CHANGED
data/receiver.rb
CHANGED
|
@@ -24,6 +24,7 @@ require_relative "lib/brainiac/helpers"
|
|
|
24
24
|
require_relative "lib/brainiac/notifications"
|
|
25
25
|
require_relative "lib/brainiac/cron"
|
|
26
26
|
require_relative "lib/brainiac/restart"
|
|
27
|
+
require_relative "lib/brainiac/intent"
|
|
27
28
|
require_relative "lib/brainiac/plugins"
|
|
28
29
|
require_relative "lib/brainiac/handlers/shared/git"
|
|
29
30
|
require_relative "lib/brainiac/handlers/shared/inline_tags"
|
|
@@ -98,6 +99,11 @@ configure do
|
|
|
98
99
|
end
|
|
99
100
|
|
|
100
101
|
LOG.info "[Brainiac] Starting v#{BRAINIAC_VERSION} on port #{settings.port} (#{settings.environment})"
|
|
102
|
+
if intent_config["enabled"]
|
|
103
|
+
LOG.info "[Intent] Enabled — model: #{intent_config["model"]}, endpoint: #{intent_config["endpoint"]}"
|
|
104
|
+
else
|
|
105
|
+
LOG.info "[Intent] Disabled (enable in brainiac.json → intent.enabled: true)"
|
|
106
|
+
end
|
|
101
107
|
|
|
102
108
|
# --- Dashboard authentication ---
|
|
103
109
|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: brainiac
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.0.
|
|
4
|
+
version: 0.0.14
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Andy Davis
|
|
@@ -133,6 +133,7 @@ files:
|
|
|
133
133
|
- lib/brainiac/handlers/shared/inline_tags.rb
|
|
134
134
|
- lib/brainiac/helpers.rb
|
|
135
135
|
- lib/brainiac/hooks.rb
|
|
136
|
+
- lib/brainiac/intent.rb
|
|
136
137
|
- lib/brainiac/notifications.rb
|
|
137
138
|
- lib/brainiac/plugins.rb
|
|
138
139
|
- lib/brainiac/prompts.rb
|