lex-pilot-knowledge-assist 0.1.3 → 0.2.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/lib/legion/extensions/pilot_knowledge_assist/actors/question_subscriber.rb +31 -0
- data/lib/legion/extensions/pilot_knowledge_assist/runners/assistant.rb +62 -12
- data/lib/legion/extensions/pilot_knowledge_assist/runners/classifier.rb +45 -0
- data/lib/legion/extensions/pilot_knowledge_assist/runners/feedback.rb +54 -0
- data/lib/legion/extensions/pilot_knowledge_assist/version.rb +1 -1
- data/lib/legion/extensions/pilot_knowledge_assist.rb +4 -0
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e9cd5f20f185374b02c185d07efc68ac953eb96b51dc742dde00060a17dc7f25
|
|
4
|
+
data.tar.gz: b3340c3aaad049daef0d6c102518398b6e10e834a88f8a87849c08a86ba5bdb5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a2d00b05083629bc9b4cf0551e1bbf6783e3aa5695cfb69cba4ad3dfe1b6b8054ea2272199467340ff2ff431064e326918051b502569a39056bd61fbadad6f84
|
|
7
|
+
data.tar.gz: 2567071743f3f751c69f7afdea3f8379b6b0c31823cee33f7a71987d15c1f85079bfef1b5f0359c18d4b9bf86289032712da5c757ccdcb2d530760d32806bb60
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module PilotKnowledgeAssist
|
|
6
|
+
module Actors
|
|
7
|
+
class QuestionSubscriber < Legion::Extensions::Actors::Subscription
|
|
8
|
+
def runner_class
|
|
9
|
+
'Legion::Extensions::PilotKnowledgeAssist::Runners::Assistant'
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def runner_function
|
|
13
|
+
'answer_question'
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def check_subtask?
|
|
17
|
+
false
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def generate_task?
|
|
21
|
+
false
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def use_runner?
|
|
25
|
+
false
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -5,20 +5,51 @@ module Legion
|
|
|
5
5
|
module PilotKnowledgeAssist
|
|
6
6
|
module Runners
|
|
7
7
|
module Assistant
|
|
8
|
+
include Classifier
|
|
9
|
+
|
|
10
|
+
DISCLAIMER_THRESHOLD = 0.5
|
|
11
|
+
ESCALATION_THRESHOLD = 0.2
|
|
12
|
+
DISCLAIMER_PREFIX = "I'm not fully certain about this answer -- please verify:\n\n"
|
|
13
|
+
INTENT_ANSWERS = {
|
|
14
|
+
greeting: 'Hello! I can help with Grid documentation and support questions.',
|
|
15
|
+
out_of_scope: 'I can only help with Grid infrastructure and documentation questions.',
|
|
16
|
+
ops_request: 'I can answer documentation questions but cannot perform operations. ' \
|
|
17
|
+
'Please contact the Grid team.'
|
|
18
|
+
}.freeze
|
|
19
|
+
|
|
8
20
|
def answer_question(question:, agent_id: 'knowledge-assist')
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
confidence:
|
|
17
|
-
|
|
21
|
+
intent_response = classify_and_route(question: question)
|
|
22
|
+
return intent_response if intent_response
|
|
23
|
+
|
|
24
|
+
context_entries = retrieve_context(question: question, agent_id: agent_id)
|
|
25
|
+
confidence = derive_confidence(context_entries)
|
|
26
|
+
|
|
27
|
+
if confidence < ESCALATION_THRESHOLD
|
|
28
|
+
escalate(question: question, confidence: confidence)
|
|
29
|
+
return { question: question, answer: 'This question has been escalated to the support team.',
|
|
30
|
+
sources: [], confidence: confidence, escalated: true, flagged: true }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
answer = generate_answer(question: question, context: context_entries)
|
|
34
|
+
flagged = confidence < DISCLAIMER_THRESHOLD
|
|
35
|
+
final_answer = flagged ? "#{DISCLAIMER_PREFIX}#{answer}" : answer
|
|
36
|
+
|
|
37
|
+
{ question: question, answer: final_answer, sources: context_entries.map { |e| e[:id] },
|
|
38
|
+
confidence: confidence, flagged: flagged, escalated: false }
|
|
18
39
|
end
|
|
19
40
|
|
|
20
41
|
private
|
|
21
42
|
|
|
43
|
+
def classify_and_route(question:)
|
|
44
|
+
classification = classify_intent(message: question)
|
|
45
|
+
intent = classification[:intent]
|
|
46
|
+
answer = INTENT_ANSWERS[intent]
|
|
47
|
+
return unless answer
|
|
48
|
+
|
|
49
|
+
{ question: question, answer: answer, intent: intent,
|
|
50
|
+
confidence: 1.0, sources: [], flagged: false, escalated: false }
|
|
51
|
+
end
|
|
52
|
+
|
|
22
53
|
def derive_confidence(context)
|
|
23
54
|
return 0.3 if context.empty?
|
|
24
55
|
|
|
@@ -28,7 +59,7 @@ module Legion
|
|
|
28
59
|
scores.max.clamp(0.1, 1.0)
|
|
29
60
|
end
|
|
30
61
|
|
|
31
|
-
def retrieve_context(question
|
|
62
|
+
def retrieve_context(question:, agent_id:)
|
|
32
63
|
return [] unless defined?(Legion::Extensions::Apollo::Client)
|
|
33
64
|
|
|
34
65
|
client = Legion::Extensions::Apollo::Client.new(agent_id: agent_id)
|
|
@@ -37,7 +68,7 @@ module Legion
|
|
|
37
68
|
[]
|
|
38
69
|
end
|
|
39
70
|
|
|
40
|
-
def generate_answer(question
|
|
71
|
+
def generate_answer(question:, context:)
|
|
41
72
|
return 'LLM unavailable' unless defined?(Legion::LLM)
|
|
42
73
|
|
|
43
74
|
context_text = context.map { |c| c[:content] }.join("\n\n")
|
|
@@ -48,10 +79,29 @@ module Legion
|
|
|
48
79
|
end
|
|
49
80
|
|
|
50
81
|
result = Legion::LLM.chat(message: prompt, caller: { extension: 'lex-pilot-knowledge-assist' })
|
|
51
|
-
result[:content]
|
|
82
|
+
result.is_a?(Hash) ? result[:content] : result.to_s
|
|
52
83
|
rescue StandardError => e
|
|
53
84
|
"Error: #{e.message}"
|
|
54
85
|
end
|
|
86
|
+
|
|
87
|
+
def escalate(question:, confidence:)
|
|
88
|
+
return unless defined?(Legion::Extensions::Slack::Client)
|
|
89
|
+
|
|
90
|
+
webhook = escalation_webhook
|
|
91
|
+
return unless webhook
|
|
92
|
+
|
|
93
|
+
message = "Knowledge assist escalation:\n*Question:* #{question}\n*Confidence:* #{confidence}"
|
|
94
|
+
Legion::Extensions::Slack::Client.new.send_webhook(message: message, webhook: webhook)
|
|
95
|
+
rescue StandardError => e
|
|
96
|
+
Legion::Logging::Logger.warn("Escalation failed: #{e.message}") if defined?(Legion::Logging)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def escalation_webhook
|
|
100
|
+
return nil unless defined?(Legion::Settings)
|
|
101
|
+
|
|
102
|
+
config = Legion::Settings[:pilot_knowledge_assist] || {}
|
|
103
|
+
config[:escalation_webhook]
|
|
104
|
+
end
|
|
55
105
|
end
|
|
56
106
|
end
|
|
57
107
|
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module PilotKnowledgeAssist
|
|
6
|
+
module Runners
|
|
7
|
+
module Classifier
|
|
8
|
+
VALID_INTENTS = %i[doc_question ops_request greeting out_of_scope].freeze
|
|
9
|
+
|
|
10
|
+
CLASSIFY_PROMPT = <<~PROMPT
|
|
11
|
+
Classify this message into exactly one category. Reply with ONLY the category name:
|
|
12
|
+
- doc_question (asking about documentation, how-to, configuration, architecture)
|
|
13
|
+
- ops_request (asking to perform an action, run a command, make a change)
|
|
14
|
+
- greeting (hello, hi, thanks, goodbye)
|
|
15
|
+
- out_of_scope (weather, sports, personal questions, unrelated topics)
|
|
16
|
+
|
|
17
|
+
Message: %<message>s
|
|
18
|
+
PROMPT
|
|
19
|
+
|
|
20
|
+
def classify_intent(message:)
|
|
21
|
+
return { intent: :doc_question, method: :default } unless llm_available?
|
|
22
|
+
|
|
23
|
+
prompt = format(CLASSIFY_PROMPT, message: message)
|
|
24
|
+
response = Legion::LLM.chat(
|
|
25
|
+
message: prompt,
|
|
26
|
+
caller: { extension: 'lex-pilot-knowledge-assist', function: 'classify_intent' }
|
|
27
|
+
)
|
|
28
|
+
intent = response.to_s.strip.downcase.to_sym
|
|
29
|
+
intent = :doc_question unless VALID_INTENTS.include?(intent)
|
|
30
|
+
|
|
31
|
+
{ intent: intent, method: :llm }
|
|
32
|
+
rescue StandardError
|
|
33
|
+
{ intent: :doc_question, method: :fallback }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def llm_available?
|
|
39
|
+
defined?(Legion::LLM) && Legion::LLM.respond_to?(:started?) && Legion::LLM.started?
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module PilotKnowledgeAssist
|
|
6
|
+
module Runners
|
|
7
|
+
module Feedback
|
|
8
|
+
VALID_RATINGS = %i[positive negative].freeze
|
|
9
|
+
|
|
10
|
+
def record_feedback(question:, answer:, rating:, user_id: nil)
|
|
11
|
+
rating = rating.to_sym
|
|
12
|
+
return { success: false, reason: :invalid_rating } unless VALID_RATINGS.include?(rating)
|
|
13
|
+
|
|
14
|
+
entry = { question: question, answer: answer, rating: rating,
|
|
15
|
+
user_id: user_id, timestamp: Time.now }
|
|
16
|
+
feedback_store << entry
|
|
17
|
+
|
|
18
|
+
store_to_apollo(question: question, answer: answer) if rating == :positive
|
|
19
|
+
|
|
20
|
+
{ success: true, rating: rating }
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def feedback_stats
|
|
24
|
+
total = feedback_store.size
|
|
25
|
+
positive = feedback_store.count { |e| e[:rating] == :positive }
|
|
26
|
+
negative = total - positive
|
|
27
|
+
|
|
28
|
+
{ total: total, positive: positive, negative: negative,
|
|
29
|
+
accuracy: total.positive? ? positive.to_f / total : 0.0 }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def feedback_store
|
|
35
|
+
@feedback_store ||= []
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def store_to_apollo(question:, answer:)
|
|
39
|
+
return unless defined?(Legion::Extensions::Apollo::Client)
|
|
40
|
+
|
|
41
|
+
Legion::Extensions::Apollo::Client.new.handle_ingest(
|
|
42
|
+
content: "Q: #{question}\nA: #{answer}",
|
|
43
|
+
content_type: 'qa_pair',
|
|
44
|
+
confidence: 0.5,
|
|
45
|
+
tags: %w[knowledge_assist qa_pair]
|
|
46
|
+
)
|
|
47
|
+
rescue StandardError
|
|
48
|
+
nil
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative 'pilot_knowledge_assist/version'
|
|
4
|
+
require_relative 'pilot_knowledge_assist/runners/classifier'
|
|
4
5
|
require_relative 'pilot_knowledge_assist/runners/assistant'
|
|
6
|
+
require_relative 'pilot_knowledge_assist/runners/feedback'
|
|
7
|
+
|
|
8
|
+
require_relative 'pilot_knowledge_assist/actors/question_subscriber' if defined?(Legion::Extensions::Actors::Subscription)
|
|
5
9
|
|
|
6
10
|
module Legion
|
|
7
11
|
module Extensions
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: lex-pilot-knowledge-assist
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Esity
|
|
@@ -115,7 +115,10 @@ extensions: []
|
|
|
115
115
|
extra_rdoc_files: []
|
|
116
116
|
files:
|
|
117
117
|
- lib/legion/extensions/pilot_knowledge_assist.rb
|
|
118
|
+
- lib/legion/extensions/pilot_knowledge_assist/actors/question_subscriber.rb
|
|
118
119
|
- lib/legion/extensions/pilot_knowledge_assist/runners/assistant.rb
|
|
120
|
+
- lib/legion/extensions/pilot_knowledge_assist/runners/classifier.rb
|
|
121
|
+
- lib/legion/extensions/pilot_knowledge_assist/runners/feedback.rb
|
|
119
122
|
- lib/legion/extensions/pilot_knowledge_assist/version.rb
|
|
120
123
|
homepage: https://github.com/LegionIO
|
|
121
124
|
licenses:
|