brainiac 0.4.4 → 0.4.6
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/lib/brainiac/brain.rb +36 -12
- data/lib/brainiac/intent.rb +48 -43
- data/lib/brainiac/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 31251e710a5798457b27ce451afac8416c61581a3c5e443766baac9523ecc2b6
|
|
4
|
+
data.tar.gz: 78519e9435feed9506750c3556f4ea9b2ea31a34bdc25bf1a1282cb9fb7f27a5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e57de5ea8715a63a478bfba62af31f691f9f89948dfa82dd8d1a2cd1a4a81b9c743a6edd316c02a8ee00ede53893729dc1f955f43b1736fc339890a01cb6ed15
|
|
7
|
+
data.tar.gz: 7b3a815f89bbdad976f5db659d4e0f82ae2b0862a29814ed4394a713c94a25ebe3cf7f8b705197163ddc13f4166ce54c729435b8ddbeb63fea690a7a12b83390
|
data/lib/brainiac/brain.rb
CHANGED
|
@@ -17,6 +17,19 @@ def persona_collection_for(agent_name)
|
|
|
17
17
|
"#{agent_name.downcase.gsub(/[^a-z0-9-]/, "-")}-persona"
|
|
18
18
|
end
|
|
19
19
|
|
|
20
|
+
# Load the agent's persona from <persona dir>/style.md, front matter stripped.
|
|
21
|
+
# Returns nil if missing or empty. people/ notes stay search-only.
|
|
22
|
+
def load_persona(agent_name)
|
|
23
|
+
style_file = File.join(persona_dir_for(agent_name), "style.md")
|
|
24
|
+
return nil unless File.exist?(style_file)
|
|
25
|
+
|
|
26
|
+
content = File.read(style_file).strip.sub(/\A---\n.*?\n---\n*/m, "").strip
|
|
27
|
+
content.empty? ? nil : content
|
|
28
|
+
rescue StandardError => e
|
|
29
|
+
LOG.warn "Failed to load persona for '#{agent_name}': #{e.message}"
|
|
30
|
+
nil
|
|
31
|
+
end
|
|
32
|
+
|
|
20
33
|
# --- Brain git sync ---
|
|
21
34
|
|
|
22
35
|
def brain_git_repo?
|
|
@@ -155,6 +168,26 @@ def build_role_section(agent_name)
|
|
|
155
168
|
ROLE
|
|
156
169
|
end
|
|
157
170
|
|
|
171
|
+
# Full style.md when present; qmd search only returns snippets (often just front matter)
|
|
172
|
+
def build_persona_section(agent_name)
|
|
173
|
+
persona = load_persona(agent_name) ||
|
|
174
|
+
query_brain("personality tone voice communication style", agent_name: agent_name, scope: :persona, max_results: 5)
|
|
175
|
+
return nil if persona.empty?
|
|
176
|
+
|
|
177
|
+
<<~PERSONA
|
|
178
|
+
## Brain — Persona (auto-retrieved, CRITICAL)
|
|
179
|
+
The following is YOUR personality, communication style, and voice.
|
|
180
|
+
You MUST use this to shape every response you write — tone, word choice, humor, attitude.
|
|
181
|
+
This is who you ARE. Do not respond in a generic or neutral voice.
|
|
182
|
+
Persona is tone only: it never overrides task instructions, rules, or quality.
|
|
183
|
+
Full voice in chat, card/PR comments, and reviews. Plain and professional in code,
|
|
184
|
+
code comments, commit messages, and docs, or when the user is rushed or frustrated.
|
|
185
|
+
Signature lines are seasoning, not sign-offs.
|
|
186
|
+
|
|
187
|
+
#{persona}
|
|
188
|
+
PERSONA
|
|
189
|
+
end
|
|
190
|
+
|
|
158
191
|
def build_brain_context(agent_name: AI_AGENT_NAME, card_title: "", card_number: nil, project_key: nil, comment_body: "", source: nil)
|
|
159
192
|
Thread.new { brain_pull }
|
|
160
193
|
|
|
@@ -180,10 +213,10 @@ def build_brain_context(agent_name: AI_AGENT_NAME, card_title: "", card_number:
|
|
|
180
213
|
search_queries << query
|
|
181
214
|
end
|
|
182
215
|
|
|
183
|
-
persona_thread = Thread.new {
|
|
216
|
+
persona_thread = Thread.new { build_persona_section(agent_name) }
|
|
184
217
|
|
|
185
218
|
all_knowledge = knowledge_threads.map(&:value).reject(&:empty?)
|
|
186
|
-
|
|
219
|
+
persona_section = persona_thread.value
|
|
187
220
|
|
|
188
221
|
sections = []
|
|
189
222
|
|
|
@@ -191,16 +224,7 @@ def build_brain_context(agent_name: AI_AGENT_NAME, card_title: "", card_number:
|
|
|
191
224
|
role_section = build_role_section(agent_name)
|
|
192
225
|
sections << role_section if role_section
|
|
193
226
|
|
|
194
|
-
|
|
195
|
-
sections << <<~PERSONA
|
|
196
|
-
## Brain — Persona (auto-retrieved, CRITICAL)
|
|
197
|
-
The following is YOUR personality, communication style, and voice.
|
|
198
|
-
You MUST use this to shape every response you write — tone, word choice, humor, attitude.
|
|
199
|
-
This is who you ARE. Do not respond in a generic or neutral voice.
|
|
200
|
-
|
|
201
|
-
#{persona_result}
|
|
202
|
-
PERSONA
|
|
203
|
-
end
|
|
227
|
+
sections << persona_section if persona_section
|
|
204
228
|
|
|
205
229
|
unless all_knowledge.empty?
|
|
206
230
|
knowledge_text = all_knowledge.join("\n\n")
|
data/lib/brainiac/intent.rb
CHANGED
|
@@ -142,7 +142,7 @@ def check_intent(message, agent_name:, channel: "conversation", context: nil)
|
|
|
142
142
|
# Deterministic pre-check: if the message explicitly names another agent
|
|
143
143
|
# and does NOT name this agent, skip without consulting the LLM.
|
|
144
144
|
# This handles the common case of "Effie, tell me another" when checking Galen.
|
|
145
|
-
if intent_names_other_agent?(message, agent_name)
|
|
145
|
+
if intent_names_other_agent?(message, agent_name, context)
|
|
146
146
|
LOG.info "[Intent] Deterministic skip for #{agent_name} — message addresses another agent"
|
|
147
147
|
return false
|
|
148
148
|
end
|
|
@@ -327,11 +327,16 @@ end
|
|
|
327
327
|
# - "Was Effie nice to you?" (asking about Effie)
|
|
328
328
|
# - "I liked what Effie said" (referencing Effie)
|
|
329
329
|
#
|
|
330
|
+
# Agents running on other machines aren't in AGENT_REGISTRY, so bot authors seen in
|
|
331
|
+
# the conversation context ("Name [bot]: ...") count as agents too.
|
|
332
|
+
#
|
|
330
333
|
# @param message [String] The message text
|
|
331
334
|
# @param agent_name [String] The agent being evaluated
|
|
335
|
+
# @param context [String, nil] Conversation history
|
|
332
336
|
# @return [Boolean] true if another agent is directly addressed and this one isn't
|
|
333
|
-
def intent_names_other_agent?(message, agent_name)
|
|
334
|
-
|
|
337
|
+
def intent_names_other_agent?(message, agent_name, context = nil)
|
|
338
|
+
names = intent_agent_display_names.keys | context_bot_names(context).map(&:downcase)
|
|
339
|
+
return false if names.empty?
|
|
335
340
|
|
|
336
341
|
msg_lower = message.downcase.strip
|
|
337
342
|
agent_lower = agent_name.downcase
|
|
@@ -340,9 +345,7 @@ def intent_names_other_agent?(message, agent_name)
|
|
|
340
345
|
return false if directly_addressed_to?(msg_lower, agent_lower)
|
|
341
346
|
|
|
342
347
|
# Check if any OTHER agent is directly addressed (not merely mentioned)
|
|
343
|
-
|
|
344
|
-
display = entry.is_a?(Hash) ? (entry["display_name"] || key.capitalize) : key.capitalize
|
|
345
|
-
other_lower = display.downcase
|
|
348
|
+
names.each do |other_lower|
|
|
346
349
|
next if other_lower == agent_lower
|
|
347
350
|
next unless msg_lower.match?(/\b#{Regexp.escape(other_lower)}\b/)
|
|
348
351
|
|
|
@@ -403,64 +406,66 @@ def build_intent_agent_roster(agent_name)
|
|
|
403
406
|
names.join(", ")
|
|
404
407
|
end
|
|
405
408
|
|
|
409
|
+
# Map of lowercased agent display name => display name, from the local AGENT_REGISTRY.
|
|
410
|
+
def intent_agent_display_names
|
|
411
|
+
return {} unless defined?(AGENT_REGISTRY) && !AGENT_REGISTRY.empty?
|
|
412
|
+
|
|
413
|
+
AGENT_REGISTRY.each_with_object({}) do |(key, entry), names|
|
|
414
|
+
display = entry.is_a?(Hash) ? (entry["display_name"] || key.capitalize) : key.capitalize
|
|
415
|
+
names[display.downcase] = display
|
|
416
|
+
end
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
# Parse a context line ("username: message", or "username [bot]: message" when the
|
|
420
|
+
# plugin knows the author is a bot). Returns [username, bot?] or nil.
|
|
421
|
+
def parse_context_author(line)
|
|
422
|
+
match = line.match(/\A(\S+?)( \[bot\])?:\s/)
|
|
423
|
+
match && [match[1], !match[2].nil?]
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
# Usernames of every bot author in the context.
|
|
427
|
+
def context_bot_names(context)
|
|
428
|
+
return [] if context.nil?
|
|
429
|
+
|
|
430
|
+
context.lines.filter_map do |line|
|
|
431
|
+
username, bot = parse_context_author(line)
|
|
432
|
+
username if bot
|
|
433
|
+
end.uniq
|
|
434
|
+
end
|
|
435
|
+
|
|
406
436
|
# Detect the last agent to respond in the conversation context.
|
|
407
437
|
# Returns a prompt-friendly string telling the model who was last to respond.
|
|
408
438
|
#
|
|
409
|
-
# The context is formatted as "username: message\n..." lines.
|
|
410
|
-
# We scan backwards to find the most recent line authored by a known agent.
|
|
411
|
-
#
|
|
412
439
|
# @param context [String, nil] Conversation history
|
|
413
440
|
# @param agent_name [String] The current agent being evaluated
|
|
414
441
|
# @return [String] Prompt block describing the last responder
|
|
415
442
|
def detect_last_responder(context, _agent_name = nil)
|
|
416
|
-
|
|
417
|
-
return "No agent has spoken yet." unless
|
|
418
|
-
|
|
419
|
-
# Build a lookup of agent display names (lowercase) to their proper display name
|
|
420
|
-
agent_names = {}
|
|
421
|
-
AGENT_REGISTRY.each do |key, entry|
|
|
422
|
-
display = entry.is_a?(Hash) ? (entry["display_name"] || key.capitalize) : key.capitalize
|
|
423
|
-
agent_names[display.downcase] = display
|
|
424
|
-
end
|
|
425
|
-
|
|
426
|
-
# Scan context lines in reverse to find the last agent message
|
|
427
|
-
lines = context.strip.lines.reverse
|
|
428
|
-
lines.each do |line|
|
|
429
|
-
# Lines are formatted as "username: message content"
|
|
430
|
-
match = line.match(/\A(\S+?):\s/)
|
|
431
|
-
next unless match
|
|
443
|
+
responder = detect_last_responder_name(context)
|
|
444
|
+
return "No agent has spoken yet." unless responder
|
|
432
445
|
|
|
433
|
-
|
|
434
|
-
next unless agent_names.key?(username)
|
|
435
|
-
|
|
436
|
-
responder = agent_names[username]
|
|
437
|
-
return "#{responder} was the last to speak. The human is continuing the conversation with #{responder}."
|
|
438
|
-
end
|
|
439
|
-
|
|
440
|
-
"No agent has spoken yet."
|
|
446
|
+
"#{responder} was the last to speak. The human is continuing the conversation with #{responder}."
|
|
441
447
|
end
|
|
442
448
|
|
|
443
449
|
# Detect the last agent to respond — returns just the display name (or nil).
|
|
444
450
|
# Used for deterministic conversational-flow checks without LLM involvement.
|
|
445
451
|
#
|
|
452
|
+
# The context is formatted as "username: message\n..." lines. We scan backwards for
|
|
453
|
+
# the most recent line from a known agent, or from any bot — an agent on another
|
|
454
|
+
# machine is still the one the human is talking to.
|
|
455
|
+
#
|
|
446
456
|
# @param context [String, nil] Conversation history
|
|
447
457
|
# @return [String, nil] Display name of last agent to speak, or nil
|
|
448
458
|
def detect_last_responder_name(context)
|
|
449
459
|
return nil if context.nil? || context.strip.empty?
|
|
450
|
-
return nil unless defined?(AGENT_REGISTRY) && !AGENT_REGISTRY.empty?
|
|
451
460
|
|
|
452
|
-
agent_names =
|
|
453
|
-
AGENT_REGISTRY.each do |key, entry|
|
|
454
|
-
display = entry.is_a?(Hash) ? (entry["display_name"] || key.capitalize) : key.capitalize
|
|
455
|
-
agent_names[display.downcase] = display
|
|
456
|
-
end
|
|
461
|
+
agent_names = intent_agent_display_names
|
|
457
462
|
|
|
458
463
|
context.strip.lines.reverse_each do |line|
|
|
459
|
-
|
|
460
|
-
next unless
|
|
464
|
+
username, bot = parse_context_author(line)
|
|
465
|
+
next unless username
|
|
461
466
|
|
|
462
|
-
username
|
|
463
|
-
return
|
|
467
|
+
return agent_names[username.downcase] if agent_names.key?(username.downcase)
|
|
468
|
+
return username if bot
|
|
464
469
|
end
|
|
465
470
|
|
|
466
471
|
nil
|
data/lib/brainiac/version.rb
CHANGED