legion-gaia 0.9.38 → 0.9.44
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 +44 -0
- data/CLAUDE.md +1 -1
- data/README.md +4 -4
- data/legion-gaia.gemspec +1 -1
- data/lib/legion/gaia/advisory.rb +10 -2
- data/lib/legion/gaia/audit_observer.rb +8 -1
- data/lib/legion/gaia/bond_registry.rb +8 -1
- data/lib/legion/gaia/channel_registry.rb +18 -4
- data/lib/legion/gaia/channels/slack_adapter.rb +22 -2
- data/lib/legion/gaia/channels/teams/bot_framework_auth.rb +4 -3
- data/lib/legion/gaia/channels/teams/webhook_handler.rb +7 -1
- data/lib/legion/gaia/channels/teams_adapter.rb +24 -5
- data/lib/legion/gaia/logging.rb +3 -13
- data/lib/legion/gaia/notification_gate/schedule_evaluator.rb +7 -1
- data/lib/legion/gaia/offline_handler.rb +16 -5
- data/lib/legion/gaia/phase_wiring.rb +155 -34
- data/lib/legion/gaia/proactive.rb +116 -81
- data/lib/legion/gaia/proactive_delivery.rb +104 -0
- data/lib/legion/gaia/proactive_dispatcher.rb +85 -5
- data/lib/legion/gaia/registry.rb +7 -13
- data/lib/legion/gaia/router/agent_bridge.rb +19 -7
- data/lib/legion/gaia/router/router_bridge.rb +57 -11
- data/lib/legion/gaia/routes.rb +12 -3
- data/lib/legion/gaia/runner_host.rb +35 -1
- data/lib/legion/gaia/teams_auth.rb +8 -3
- data/lib/legion/gaia/tracker_persistence.rb +50 -11
- data/lib/legion/gaia/version.rb +1 -1
- data/lib/legion/gaia/workflow/instance.rb +11 -1
- data/lib/legion/gaia.rb +309 -28
- metadata +4 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 162def8c335c98ed023f01a4a3a091cc9e6e43a4f965827b2b76b25ea09bbace
|
|
4
|
+
data.tar.gz: cb877734f54f0b877d74489071f46d06b108ebbb555e754fb920f901de27d5c4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: aa7c57507b415ff8ad3b7c1136c4c4180bc760082c21850606af1bb3910bea3422bad9af239b4c7a7ccda0b644e3cd6bfc8ea0a948020508786c71bbe572470a
|
|
7
|
+
data.tar.gz: 5d357170e157577b45eb834496ebc5822c290a063fb5eb6bc235238fbeec38b4609a58268d123478c356cbf7a8865216406f12f456cc244a447959b1fab4ab66
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,50 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.9.44] - 2026-04-02
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
- Route Teams webhook traffic through `Legion::Gaia.ingest` so webhook-delivered frames use normal signal normalization, session tracking, and interlocutor observation instead of pushing raw frames into the sensory buffer
|
|
9
|
+
|
|
10
|
+
## [0.9.43] - 2026-04-02
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- Preserve undelivered proactive intents when pending dispatch stops mid-drain, prevent failed sends from consuming proactive quota, and fail partner-directed dispatches when no partner channel can be resolved
|
|
14
|
+
|
|
15
|
+
## [0.9.42] - 2026-04-02
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
- Keep trackers dirty when Apollo persistence returns a failed upsert response, and avoid advancing the flush timestamp for unsuccessful tracker flushes
|
|
19
|
+
- Refresh repo docs to match the current GAIA version and the 25-phase wiring layout
|
|
20
|
+
|
|
21
|
+
## [0.9.41] - 2026-04-02
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
- Normalize `RouterBridge#route_outbound` delivery results so adapter-reported error hashes remain undelivered instead of being wrapped as successful outbound delivery
|
|
25
|
+
|
|
26
|
+
## [0.9.40] - 2026-04-02
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
- Preserve actual proactive delivery outcomes: `send_message`, `send_to_user`, and `start_conversation` now return failure when adapter or registry delivery reports failure instead of always reporting success
|
|
30
|
+
- Include explicit per-channel `:no_adapter` failures in `send_notification` fanout results instead of silently skipping requested channels
|
|
31
|
+
- Propagate adapter delivery errors through `ChannelRegistry#deliver` so upstream callers can report real delivery outcomes
|
|
32
|
+
|
|
33
|
+
## [0.9.39] - 2026-04-02
|
|
34
|
+
|
|
35
|
+
### Added
|
|
36
|
+
- Partner-absence signal queuing: queues an ambient `InputFrame` when `partner_absence_misses` exceeds `ABSENCE_SIGNAL_THRESHOLD` (5), with a 30-minute cooldown (`ABSENCE_SIGNAL_COOLDOWN`) and salience 0.75 to prevent signal flood
|
|
37
|
+
|
|
38
|
+
### Changed
|
|
39
|
+
- Switch non-API GAIA library logging to `Legion::Logging::Helper`, replacing direct `Legion::Logging.*` calls and legacy `log_*` wrappers with helper-backed `log`
|
|
40
|
+
- Expand `info`, `debug`, and `error` coverage across GAIA runtime booting, routing, proactive delivery, trackers, adapters, and workflow transitions
|
|
41
|
+
- `PhaseWiring` `memory_retrieval` and `prediction_engine` now return `{ skip: true, reason: :idle_no_signals }` when the signals array is empty, skipping expensive phase execution on idle ticks
|
|
42
|
+
- `PhaseWiring` `knowledge_retrieval` now reads `signal[:value]` before falling back to `signal[:content]` for the query text
|
|
43
|
+
- `PhaseWiring` result normalization: `partner_observations` extracted via `partner_observations_from(ctx)` helper instead of direct `ctx.dig` to normalize observation shape
|
|
44
|
+
|
|
45
|
+
### Fixed
|
|
46
|
+
- Route rescued GAIA library exceptions through `handle_exception` so failures are captured consistently with operation context
|
|
47
|
+
- Update GAIA logging specs to support helper-backed tagged logging and keep the full suite green after the logging uplift
|
|
48
|
+
|
|
5
49
|
## [0.9.38] - 2026-04-01
|
|
6
50
|
|
|
7
51
|
### Fixed
|
data/CLAUDE.md
CHANGED
data/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Cognitive coordination layer for the LegionIO framework. GAIA is the mind that inhabits the Legion body.
|
|
4
4
|
|
|
5
|
-
**Version**: 0.9.
|
|
5
|
+
**Version**: 0.9.44
|
|
6
6
|
|
|
7
7
|
GAIA sits on top of LegionIO's infrastructure and coordinates all agentic subordinate functions. It drives the tick cycle, manages extension discovery and wiring, and provides the channel abstraction for multi-interface communication.
|
|
8
8
|
|
|
@@ -23,7 +23,7 @@ Legion::Gaia.ingest OutputRouter -> NotificationGate -> Ren
|
|
|
23
23
|
v |
|
|
24
24
|
SensoryBuffer -----> Heartbeat (1s) --> Cognitive Pipeline
|
|
25
25
|
|
|
|
26
|
-
PhaseWiring (
|
|
26
|
+
PhaseWiring (25 phases)
|
|
27
27
|
|
|
|
28
28
|
Registry -> RunnerHost(s)
|
|
29
29
|
|
|
|
@@ -102,11 +102,11 @@ gaia:
|
|
|
102
102
|
|
|
103
103
|
## Cognitive Phases
|
|
104
104
|
|
|
105
|
-
GAIA wires
|
|
105
|
+
GAIA wires 25 phases across two cycles:
|
|
106
106
|
|
|
107
107
|
**Active Tick (16 phases):** sensory processing, emotional evaluation, memory retrieval, knowledge retrieval, identity entropy check, working memory integration, procedural check, prediction engine, mesh interface, social cognition, theory of mind, gut instinct, action selection, memory consolidation, homeostasis regulation, post-tick reflection.
|
|
108
108
|
|
|
109
|
-
**Dream Cycle (
|
|
109
|
+
**Dream Cycle (9 phases):** memory audit, association walk, contradiction resolution, agenda formation, consolidation commit, knowledge promotion, dream reflection, partner reflection, dream narration.
|
|
110
110
|
|
|
111
111
|
## Channel Adapters
|
|
112
112
|
|
data/legion-gaia.gemspec
CHANGED
|
@@ -28,7 +28,7 @@ Gem::Specification.new do |spec|
|
|
|
28
28
|
|
|
29
29
|
spec.add_dependency 'base64'
|
|
30
30
|
spec.add_dependency 'legion-json', '>= 1.2.0'
|
|
31
|
-
spec.add_dependency 'legion-logging', '>= 1.
|
|
31
|
+
spec.add_dependency 'legion-logging', '>= 1.5.0'
|
|
32
32
|
spec.add_dependency 'legion-settings', '>= 1.3.12'
|
|
33
33
|
spec.add_dependency 'openssl'
|
|
34
34
|
|
data/lib/legion/gaia/advisory.rb
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'legion/logging/helper'
|
|
4
|
+
|
|
3
5
|
module Legion
|
|
4
6
|
module Gaia
|
|
5
7
|
module Advisory
|
|
8
|
+
extend Legion::Logging::Helper
|
|
9
|
+
|
|
6
10
|
module_function
|
|
7
11
|
|
|
8
12
|
# Fast in-memory advisory. NEVER makes LLM calls.
|
|
@@ -15,9 +19,13 @@ module Legion
|
|
|
15
19
|
advisory[:valence] = Gaia.last_valences if Gaia.last_valences
|
|
16
20
|
merge_tick_data!(advisory, Gaia.registry&.tick_host&.last_tick_result)
|
|
17
21
|
merge_observer_data!(advisory, caller)
|
|
18
|
-
advisory.compact
|
|
22
|
+
result = advisory.compact
|
|
23
|
+
if result.any?
|
|
24
|
+
log.info("GAIA advisory generated conversation_id=#{conversation_id} keys=#{result.keys.join(',')}")
|
|
25
|
+
end
|
|
26
|
+
result
|
|
19
27
|
rescue StandardError => e
|
|
20
|
-
|
|
28
|
+
handle_exception(e, level: :warn, operation: 'gaia.advisory.advise', conversation_id: conversation_id)
|
|
21
29
|
nil
|
|
22
30
|
end
|
|
23
31
|
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'singleton'
|
|
4
|
+
require 'legion/logging/helper'
|
|
4
5
|
|
|
5
6
|
module Legion
|
|
6
7
|
module Gaia
|
|
7
8
|
class AuditObserver
|
|
8
9
|
include Singleton
|
|
10
|
+
include Legion::Logging::Helper
|
|
9
11
|
|
|
10
12
|
def initialize
|
|
11
13
|
@user_prefs = {}
|
|
@@ -22,8 +24,13 @@ module Legion
|
|
|
22
24
|
record_tool_patterns(event)
|
|
23
25
|
record_quality(event)
|
|
24
26
|
end
|
|
27
|
+
identity = event.dig(:caller, :requested_by, :identity)
|
|
28
|
+
log.debug(
|
|
29
|
+
'AuditObserver processed event ' \
|
|
30
|
+
"identity=#{identity || 'unknown'} tools=#{Array(event[:tools_used]).size}"
|
|
31
|
+
)
|
|
25
32
|
rescue StandardError => e
|
|
26
|
-
|
|
33
|
+
handle_exception(e, level: :warn, operation: 'gaia.audit_observer.process_event')
|
|
27
34
|
end
|
|
28
35
|
|
|
29
36
|
def user_preferences(identity)
|
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'legion/logging/helper'
|
|
4
|
+
|
|
3
5
|
module Legion
|
|
4
6
|
module Gaia
|
|
5
7
|
module BondRegistry
|
|
8
|
+
extend Legion::Logging::Helper
|
|
9
|
+
|
|
6
10
|
module_function
|
|
7
11
|
|
|
8
12
|
def register(identity, role:, priority: :normal)
|
|
9
13
|
@bonds ||= {}
|
|
10
14
|
@bonds[identity.to_s] = { identity: identity.to_s, role: role.to_sym, priority: priority.to_sym,
|
|
11
15
|
since: Time.now.utc }
|
|
16
|
+
log.info("BondRegistry registered identity=#{identity} role=#{role} priority=#{priority}")
|
|
12
17
|
end
|
|
13
18
|
|
|
14
19
|
def partner?(identity)
|
|
@@ -41,12 +46,14 @@ module Legion
|
|
|
41
46
|
|
|
42
47
|
identities.each { |id| register(id, role: :partner, priority: priority) }
|
|
43
48
|
end
|
|
49
|
+
log.info("BondRegistry hydrated entries=#{result[:results].size}")
|
|
44
50
|
rescue StandardError => e
|
|
45
|
-
|
|
51
|
+
handle_exception(e, level: :warn, operation: 'gaia.bond_registry.hydrate_from_apollo')
|
|
46
52
|
end
|
|
47
53
|
|
|
48
54
|
def reset!
|
|
49
55
|
@bonds = {}
|
|
56
|
+
log.debug('BondRegistry reset')
|
|
50
57
|
end
|
|
51
58
|
|
|
52
59
|
def extract_identity_keys(content)
|
|
@@ -40,12 +40,13 @@ module Legion
|
|
|
40
40
|
|
|
41
41
|
def deliver(output_frame)
|
|
42
42
|
adapter = adapter_for(output_frame.channel_id)
|
|
43
|
-
return { delivered: false, reason: :no_adapter } unless adapter
|
|
44
|
-
|
|
43
|
+
return { delivered: false, reason: :no_adapter, channel_id: output_frame.channel_id } unless adapter
|
|
44
|
+
unless adapter.started?
|
|
45
|
+
return { delivered: false, reason: :adapter_stopped, channel_id: output_frame.channel_id }
|
|
46
|
+
end
|
|
45
47
|
|
|
46
48
|
rendered = adapter.translate_outbound(output_frame)
|
|
47
|
-
adapter.deliver(rendered)
|
|
48
|
-
{ delivered: true, channel_id: output_frame.channel_id }
|
|
49
|
+
normalize_delivery_result(adapter.deliver(rendered), channel_id: output_frame.channel_id)
|
|
49
50
|
end
|
|
50
51
|
|
|
51
52
|
def start_all
|
|
@@ -55,6 +56,19 @@ module Legion
|
|
|
55
56
|
def stop_all
|
|
56
57
|
@mutex.synchronize { @adapters.each_value(&:stop) }
|
|
57
58
|
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
def normalize_delivery_result(result, channel_id:)
|
|
63
|
+
return { delivered: false, reason: :adapter_returned_false, channel_id: channel_id } if result == false
|
|
64
|
+
return { delivered: true, channel_id: channel_id } unless result.is_a?(Hash)
|
|
65
|
+
|
|
66
|
+
normalized = result.dup
|
|
67
|
+
normalized[:channel_id] ||= channel_id
|
|
68
|
+
normalized[:delivered] = false if normalized[:error] && !normalized.key?(:delivered)
|
|
69
|
+
normalized[:delivered] = true unless normalized.key?(:delivered)
|
|
70
|
+
normalized
|
|
71
|
+
end
|
|
58
72
|
end
|
|
59
73
|
end
|
|
60
74
|
end
|
|
@@ -3,12 +3,15 @@
|
|
|
3
3
|
require 'json'
|
|
4
4
|
require 'net/http'
|
|
5
5
|
require 'uri'
|
|
6
|
+
require 'legion/logging/helper'
|
|
6
7
|
require_relative 'slack/signing_verifier'
|
|
7
8
|
|
|
8
9
|
module Legion
|
|
9
10
|
module Gaia
|
|
10
11
|
module Channels
|
|
11
12
|
class SlackAdapter < ChannelAdapter
|
|
13
|
+
include Legion::Logging::Helper
|
|
14
|
+
|
|
12
15
|
CAPABILITIES = %i[rich_text threads reactions mentions file_attachment].freeze
|
|
13
16
|
|
|
14
17
|
attr_reader :signing_secret, :bot_token
|
|
@@ -58,6 +61,10 @@ module Legion
|
|
|
58
61
|
|
|
59
62
|
def deliver(rendered_content, webhook: nil)
|
|
60
63
|
target_webhook = webhook || @default_webhook
|
|
64
|
+
log.info(
|
|
65
|
+
'SlackAdapter delivering ' \
|
|
66
|
+
"mode=#{@bot_token && !target_webhook ? 'api' : 'webhook'} channel=#{channel_id}"
|
|
67
|
+
)
|
|
61
68
|
return deliver_via_api(rendered_content) if @bot_token && !target_webhook
|
|
62
69
|
|
|
63
70
|
deliver_via_webhook(rendered_content, target_webhook)
|
|
@@ -72,6 +79,7 @@ module Legion
|
|
|
72
79
|
|
|
73
80
|
channel = dm_result[:channel_id]
|
|
74
81
|
rendered = translate_outbound(output_frame).merge(channel: channel)
|
|
82
|
+
log.info("SlackAdapter proactive delivery user_id=#{user_id} channel=#{channel}")
|
|
75
83
|
deliver_via_api_to_channel(rendered)
|
|
76
84
|
end
|
|
77
85
|
|
|
@@ -85,9 +93,11 @@ module Legion
|
|
|
85
93
|
)
|
|
86
94
|
return result if result.is_a?(Hash) && result[:error]
|
|
87
95
|
|
|
88
|
-
|
|
96
|
+
channel_id = result[:channel_id] || result['channel']&.dig('id') || result['channel']
|
|
97
|
+
log.info("SlackAdapter opened DM user_id=#{user_id} channel=#{channel_id}")
|
|
98
|
+
{ channel_id: channel_id }
|
|
89
99
|
rescue StandardError => e
|
|
90
|
-
|
|
100
|
+
handle_exception(e, level: :warn, operation: 'gaia.channels.slack_adapter.open_dm', user_id: user_id)
|
|
91
101
|
{ error: :open_dm_failed, message: e.message }
|
|
92
102
|
end
|
|
93
103
|
|
|
@@ -122,11 +132,13 @@ module Legion
|
|
|
122
132
|
|
|
123
133
|
def deliver_via_webhook(content, webhook)
|
|
124
134
|
unless slack_runner_available?
|
|
135
|
+
log.error('SlackAdapter deliver_via_webhook failed error=slack_runner_not_available')
|
|
125
136
|
return { error: :slack_runner_not_available,
|
|
126
137
|
message: 'lex-slack Chat runner not loaded' }
|
|
127
138
|
end
|
|
128
139
|
|
|
129
140
|
message = content.is_a?(Hash) ? content[:text] : content.to_s
|
|
141
|
+
log.info("SlackAdapter delivering via webhook message_length=#{message.length}")
|
|
130
142
|
Legion::Extensions::Slack::Runners::Chat.send(message: message, webhook: webhook)
|
|
131
143
|
end
|
|
132
144
|
|
|
@@ -137,6 +149,8 @@ module Legion
|
|
|
137
149
|
channel = (content.is_a?(Hash) && content[:channel]) || @channel_id.to_s
|
|
138
150
|
post_to_slack_api(channel: channel, text: text)
|
|
139
151
|
rescue StandardError => e
|
|
152
|
+
handle_exception(e, level: :warn, operation: 'gaia.channels.slack_adapter.deliver_via_api',
|
|
153
|
+
channel: channel)
|
|
140
154
|
{ error: :network_error, message: e.message }
|
|
141
155
|
end
|
|
142
156
|
|
|
@@ -154,22 +168,28 @@ module Legion
|
|
|
154
168
|
parsed = ::JSON.parse(response.body, symbolize_names: true)
|
|
155
169
|
|
|
156
170
|
if parsed[:ok]
|
|
171
|
+
log.info("SlackAdapter delivered via API channel=#{channel}")
|
|
157
172
|
{ delivered: true, ts: parsed[:ts] }
|
|
158
173
|
else
|
|
174
|
+
log.error("SlackAdapter API delivery failed channel=#{channel} error=#{parsed[:error] || :unknown_error}")
|
|
159
175
|
{ error: parsed[:error] || :unknown_error }
|
|
160
176
|
end
|
|
161
177
|
rescue StandardError => e
|
|
178
|
+
handle_exception(e, level: :warn, operation: 'gaia.channels.slack_adapter.post_to_slack_api',
|
|
179
|
+
channel: channel)
|
|
162
180
|
{ error: :network_error, message: e.message }
|
|
163
181
|
end
|
|
164
182
|
|
|
165
183
|
def deliver_via_api_to_channel(content)
|
|
166
184
|
unless slack_runner_available?
|
|
185
|
+
log.error('SlackAdapter deliver_via_api_to_channel failed error=slack_runner_not_available')
|
|
167
186
|
return { error: :slack_runner_not_available,
|
|
168
187
|
message: 'lex-slack Chat runner not loaded' }
|
|
169
188
|
end
|
|
170
189
|
|
|
171
190
|
message = content.is_a?(Hash) ? content[:text] : content.to_s
|
|
172
191
|
channel = content.is_a?(Hash) ? content[:channel] : nil
|
|
192
|
+
log.info("SlackAdapter sending proactive API message channel=#{channel}")
|
|
173
193
|
Legion::Extensions::Slack::Runners::Chat.send(
|
|
174
194
|
message: message,
|
|
175
195
|
channel: channel,
|
|
@@ -4,12 +4,15 @@ require 'net/http'
|
|
|
4
4
|
require 'uri'
|
|
5
5
|
require 'openssl'
|
|
6
6
|
require 'base64'
|
|
7
|
+
require 'legion/logging/helper'
|
|
7
8
|
|
|
8
9
|
module Legion
|
|
9
10
|
module Gaia
|
|
10
11
|
module Channels
|
|
11
12
|
module Teams
|
|
12
13
|
module BotFrameworkAuth
|
|
14
|
+
extend Legion::Logging::Helper
|
|
15
|
+
|
|
13
16
|
OPENID_METADATA_URL = 'https://login.botframework.com/v1/.well-known/openidconfiguration'
|
|
14
17
|
BOT_FRAMEWORK_ISSUER = 'https://api.botframework.com'
|
|
15
18
|
EMULATOR_ISSUER = 'https://sts.windows.net/d6d49420-f39b-4df7-a1dc-d59a935871db/'
|
|
@@ -66,9 +69,7 @@ module Legion
|
|
|
66
69
|
decoded = Base64.urlsafe_decode64(padded)
|
|
67
70
|
::JSON.parse(decoded)
|
|
68
71
|
rescue StandardError => e
|
|
69
|
-
|
|
70
|
-
Legion::Logging.debug("BotFrameworkAuth JWT segment decode failed: #{e.message}")
|
|
71
|
-
end
|
|
72
|
+
handle_exception(e, level: :debug, operation: 'gaia.channels.teams.bot_framework_auth.decode_jwt_segment')
|
|
72
73
|
nil
|
|
73
74
|
end
|
|
74
75
|
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'legion/logging/helper'
|
|
4
|
+
|
|
3
5
|
module Legion
|
|
4
6
|
module Gaia
|
|
5
7
|
module Channels
|
|
6
8
|
module Teams
|
|
7
9
|
class WebhookHandler
|
|
10
|
+
include Legion::Logging::Helper
|
|
11
|
+
|
|
8
12
|
attr_reader :adapter
|
|
9
13
|
|
|
10
14
|
def initialize(adapter)
|
|
@@ -22,6 +26,7 @@ module Legion
|
|
|
22
26
|
end
|
|
23
27
|
|
|
24
28
|
activity_type = activity['type'] || activity[:type]
|
|
29
|
+
log.info("WebhookHandler received activity_type=#{activity_type}")
|
|
25
30
|
case activity_type
|
|
26
31
|
when 'message' then handle_message(activity)
|
|
27
32
|
when 'conversationUpdate' then handle_conversation_update(activity)
|
|
@@ -37,6 +42,7 @@ module Legion
|
|
|
37
42
|
return error_response(:translate_failed, 'Could not translate activity') unless frame
|
|
38
43
|
|
|
39
44
|
Legion::Gaia.ingest(frame) if Legion::Gaia.respond_to?(:ingest)
|
|
45
|
+
log.info("WebhookHandler ingested frame_id=#{frame.id}")
|
|
40
46
|
success_response(:message_ingested, frame.id)
|
|
41
47
|
end
|
|
42
48
|
|
|
@@ -68,7 +74,7 @@ module Legion
|
|
|
68
74
|
|
|
69
75
|
::JSON.parse(body)
|
|
70
76
|
rescue StandardError => e
|
|
71
|
-
|
|
77
|
+
handle_exception(e, level: :debug, operation: 'gaia.channels.teams.webhook_handler.parse_activity')
|
|
72
78
|
nil
|
|
73
79
|
end
|
|
74
80
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'legion/logging/helper'
|
|
3
4
|
require_relative 'teams/bot_framework_auth'
|
|
4
5
|
require_relative 'teams/conversation_store'
|
|
5
6
|
require_relative 'teams/webhook_handler'
|
|
@@ -8,6 +9,8 @@ module Legion
|
|
|
8
9
|
module Gaia
|
|
9
10
|
module Channels
|
|
10
11
|
class TeamsAdapter < ChannelAdapter
|
|
12
|
+
include Legion::Logging::Helper
|
|
13
|
+
|
|
11
14
|
CAPABILITIES = %i[rich_text adaptive_cards proactive_messaging mobile desktop mentions].freeze
|
|
12
15
|
MOBILE_CAPABILITIES = %i[rich_text adaptive_cards mobile mentions].freeze
|
|
13
16
|
DESKTOP_CAPABILITIES = %i[rich_text adaptive_cards desktop mentions file_attachment].freeze
|
|
@@ -33,6 +36,7 @@ module Legion
|
|
|
33
36
|
|
|
34
37
|
def update_presence_status(status)
|
|
35
38
|
@last_presence_status = status
|
|
39
|
+
log.info("TeamsAdapter presence updated status=#{status}")
|
|
36
40
|
end
|
|
37
41
|
|
|
38
42
|
def translate_inbound(activity)
|
|
@@ -44,6 +48,7 @@ module Legion
|
|
|
44
48
|
text = strip_mention(text, activity)
|
|
45
49
|
|
|
46
50
|
conversation_store.store_from_activity(activity)
|
|
51
|
+
log.debug("TeamsAdapter translated inbound activity_id=#{activity['id'] || activity[:id]}")
|
|
47
52
|
|
|
48
53
|
InputFrame.new(
|
|
49
54
|
content: text.strip,
|
|
@@ -75,8 +80,12 @@ module Legion
|
|
|
75
80
|
def deliver(rendered_content, conversation_id: nil)
|
|
76
81
|
resolved_id = conversation_id || @default_conversation_id
|
|
77
82
|
ref = resolved_id && conversation_store.lookup(resolved_id)
|
|
78
|
-
|
|
83
|
+
unless ref
|
|
84
|
+
log.error("TeamsAdapter deliver failed conversation_id=#{resolved_id} error=no_conversation_reference")
|
|
85
|
+
return { error: :no_conversation_reference }
|
|
86
|
+
end
|
|
79
87
|
|
|
88
|
+
log.info("TeamsAdapter delivering conversation_id=#{ref.conversation_id}")
|
|
80
89
|
deliver_via_bot(rendered_content, ref)
|
|
81
90
|
end
|
|
82
91
|
|
|
@@ -88,6 +97,7 @@ module Legion
|
|
|
88
97
|
return conversation_id if conversation_id.is_a?(Hash) && conversation_id[:error]
|
|
89
98
|
|
|
90
99
|
rendered = translate_outbound(output_frame)
|
|
100
|
+
log.info("TeamsAdapter proactive delivery user_id=#{user_id} conversation_id=#{conversation_id}")
|
|
91
101
|
deliver(rendered, conversation_id: conversation_id)
|
|
92
102
|
end
|
|
93
103
|
|
|
@@ -113,11 +123,11 @@ module Legion
|
|
|
113
123
|
service_url: service_url,
|
|
114
124
|
tenant_id: resolved_tenant
|
|
115
125
|
)
|
|
126
|
+
log.info("TeamsAdapter created proactive conversation user_id=#{user_id} conversation_id=#{conversation_id}")
|
|
116
127
|
conversation_id
|
|
117
128
|
rescue StandardError => e
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
end
|
|
129
|
+
handle_exception(e, level: :warn, operation: 'gaia.channels.teams_adapter.create_proactive_conversation',
|
|
130
|
+
user_id: user_id, tenant_id: resolved_tenant)
|
|
121
131
|
{ error: :create_conversation_failed, message: e.message }
|
|
122
132
|
end
|
|
123
133
|
|
|
@@ -187,16 +197,19 @@ module Legion
|
|
|
187
197
|
|
|
188
198
|
def deliver_via_bot(rendered_content, ref)
|
|
189
199
|
unless bot_runner_available?
|
|
200
|
+
log.error('TeamsAdapter deliver_via_bot failed error=bot_runner_not_available')
|
|
190
201
|
return { error: :bot_runner_not_available,
|
|
191
202
|
message: 'lex-microsoft_teams Bot runner not loaded' }
|
|
192
203
|
end
|
|
193
204
|
|
|
194
205
|
bot = Legion::Extensions::MicrosoftTeams::Client.new
|
|
195
206
|
if rendered_content.is_a?(Hash) && rendered_content[:type] == 'adaptive_card'
|
|
207
|
+
log.info("TeamsAdapter sending adaptive card conversation_id=#{ref.conversation_id}")
|
|
196
208
|
bot.send_card(service_url: ref.service_url, conversation_id: ref.conversation_id,
|
|
197
209
|
card: rendered_content[:card])
|
|
198
210
|
else
|
|
199
211
|
text = rendered_content.is_a?(Hash) ? rendered_content[:text] : rendered_content.to_s
|
|
212
|
+
log.info("TeamsAdapter sending text conversation_id=#{ref.conversation_id}")
|
|
200
213
|
bot.send_text(service_url: ref.service_url, conversation_id: ref.conversation_id,
|
|
201
214
|
text: text)
|
|
202
215
|
end
|
|
@@ -208,7 +221,13 @@ module Legion
|
|
|
208
221
|
|
|
209
222
|
def resolve_proactive_conversation(user_id)
|
|
210
223
|
existing = conversation_store.conversations_for_user(user_id)
|
|
211
|
-
|
|
224
|
+
unless existing.empty?
|
|
225
|
+
log.debug(
|
|
226
|
+
'TeamsAdapter reused proactive conversation ' \
|
|
227
|
+
"user_id=#{user_id} conversation_id=#{existing.first.conversation_id}"
|
|
228
|
+
)
|
|
229
|
+
return existing.first.conversation_id
|
|
230
|
+
end
|
|
212
231
|
|
|
213
232
|
create_proactive_conversation(user_id: user_id)
|
|
214
233
|
end
|
data/lib/legion/gaia/logging.rb
CHANGED
|
@@ -1,21 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'legion/logging/helper'
|
|
4
|
+
|
|
3
5
|
module Legion
|
|
4
6
|
module Gaia
|
|
5
7
|
module Logging
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
def log_debug(msg)
|
|
9
|
-
Legion::Logging.debug(msg) if Legion.const_defined?(:Logging, false)
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
def log_info(msg)
|
|
13
|
-
Legion::Logging.info(msg) if Legion.const_defined?(:Logging, false)
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
def log_warn(msg)
|
|
17
|
-
Legion::Logging.warn(msg) if Legion.const_defined?(:Logging, false)
|
|
18
|
-
end
|
|
8
|
+
include Legion::Logging::Helper
|
|
19
9
|
end
|
|
20
10
|
end
|
|
21
11
|
end
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'legion/logging/helper'
|
|
4
|
+
|
|
3
5
|
module Legion
|
|
4
6
|
module Gaia
|
|
5
7
|
class NotificationGate
|
|
6
8
|
class ScheduleEvaluator
|
|
9
|
+
include Legion::Logging::Helper
|
|
10
|
+
|
|
7
11
|
DAY_NAMES = %w[sun mon tue wed thu fri sat].freeze
|
|
8
12
|
|
|
9
13
|
# Standard (non-DST) offsets for known IANA zone names.
|
|
@@ -104,7 +108,9 @@ module Legion
|
|
|
104
108
|
abs_h = hours.abs.to_i
|
|
105
109
|
abs_m = ((hours.abs % 1) * 60).round
|
|
106
110
|
format('%<sign>s%<h>02d:%<m>02d', sign: sign, h: abs_h, m: abs_m)
|
|
107
|
-
rescue StandardError
|
|
111
|
+
rescue StandardError => e
|
|
112
|
+
handle_exception(e, level: :debug, operation: 'gaia.notification_gate.schedule_evaluator.tzinfo_offset',
|
|
113
|
+
timezone: timezone)
|
|
108
114
|
nil
|
|
109
115
|
end
|
|
110
116
|
|
|
@@ -1,14 +1,22 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'legion/logging/helper'
|
|
4
|
+
|
|
3
5
|
module Legion
|
|
4
6
|
module Gaia
|
|
5
7
|
module OfflineHandler
|
|
8
|
+
extend Legion::Logging::Helper
|
|
9
|
+
|
|
6
10
|
DEFAULT_OFFLINE_MESSAGE = 'The agent is currently offline. Your message has been queued.'
|
|
7
11
|
|
|
8
12
|
class << self
|
|
9
13
|
def handle_offline_delivery(input_frame, worker_id:)
|
|
10
14
|
queue_message(input_frame, worker_id)
|
|
11
15
|
notify_sender(input_frame)
|
|
16
|
+
log.info(
|
|
17
|
+
'OfflineHandler queued ' \
|
|
18
|
+
"frame_id=#{input_frame.respond_to?(:id) ? input_frame.id : 'unknown'} worker_id=#{worker_id}"
|
|
19
|
+
)
|
|
12
20
|
{ queued: true, worker_id: worker_id }
|
|
13
21
|
end
|
|
14
22
|
|
|
@@ -21,6 +29,7 @@ module Legion
|
|
|
21
29
|
|
|
22
30
|
def record_presence(worker_id)
|
|
23
31
|
presence_store[worker_id] = { last_seen: Time.now }
|
|
32
|
+
log.debug("OfflineHandler recorded presence worker_id=#{worker_id}")
|
|
24
33
|
end
|
|
25
34
|
|
|
26
35
|
def pending_count(worker_id)
|
|
@@ -28,7 +37,9 @@ module Legion
|
|
|
28
37
|
end
|
|
29
38
|
|
|
30
39
|
def drain_pending(worker_id)
|
|
31
|
-
pending_store.delete(worker_id) || []
|
|
40
|
+
drained = pending_store.delete(worker_id) || []
|
|
41
|
+
log.info("OfflineHandler drained pending worker_id=#{worker_id} count=#{drained.size}") if drained.any?
|
|
42
|
+
drained
|
|
32
43
|
end
|
|
33
44
|
|
|
34
45
|
def reset!
|
|
@@ -56,8 +67,10 @@ module Legion
|
|
|
56
67
|
session_continuity_id: frame.respond_to?(:session_continuity_id) ? frame.session_continuity_id : nil
|
|
57
68
|
)
|
|
58
69
|
registry.deliver(output)
|
|
70
|
+
log.info("OfflineHandler notified sender frame_id=#{output.id} channel=#{frame.channel_id}")
|
|
59
71
|
rescue StandardError => e
|
|
60
|
-
|
|
72
|
+
handle_exception(e, level: :warn, operation: 'gaia.offline_handler.notify_sender',
|
|
73
|
+
channel_id: frame.respond_to?(:channel_id) ? frame.channel_id : nil)
|
|
61
74
|
nil
|
|
62
75
|
end
|
|
63
76
|
|
|
@@ -68,9 +81,7 @@ module Legion
|
|
|
68
81
|
60
|
|
69
82
|
end
|
|
70
83
|
rescue StandardError => e
|
|
71
|
-
|
|
72
|
-
Legion::Logging.debug("OfflineHandler offline_threshold settings unavailable, using default: #{e.message}")
|
|
73
|
-
end
|
|
84
|
+
handle_exception(e, level: :debug, operation: 'gaia.offline_handler.offline_threshold')
|
|
74
85
|
60
|
|
75
86
|
end
|
|
76
87
|
|