lex-agentic-language 0.1.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.
Files changed (126) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +6 -0
  3. data/Gemfile +5 -0
  4. data/LICENSE +21 -0
  5. data/README.md +13 -0
  6. data/lex-agentic-language.gemspec +30 -0
  7. data/lib/legion/extensions/agentic/language/conceptual_blending/client.rb +25 -0
  8. data/lib/legion/extensions/agentic/language/conceptual_blending/helpers/blend.rb +91 -0
  9. data/lib/legion/extensions/agentic/language/conceptual_blending/helpers/blending_engine.rb +171 -0
  10. data/lib/legion/extensions/agentic/language/conceptual_blending/helpers/constants.rb +35 -0
  11. data/lib/legion/extensions/agentic/language/conceptual_blending/helpers/mental_space.rb +51 -0
  12. data/lib/legion/extensions/agentic/language/conceptual_blending/runners/conceptual_blending.rb +106 -0
  13. data/lib/legion/extensions/agentic/language/conceptual_blending/version.rb +13 -0
  14. data/lib/legion/extensions/agentic/language/conceptual_blending.rb +20 -0
  15. data/lib/legion/extensions/agentic/language/conceptual_metaphor/client.rb +19 -0
  16. data/lib/legion/extensions/agentic/language/conceptual_metaphor/helpers/constants.rb +49 -0
  17. data/lib/legion/extensions/agentic/language/conceptual_metaphor/helpers/metaphor.rb +109 -0
  18. data/lib/legion/extensions/agentic/language/conceptual_metaphor/helpers/metaphor_engine.rb +154 -0
  19. data/lib/legion/extensions/agentic/language/conceptual_metaphor/runners/conceptual_metaphor.rb +107 -0
  20. data/lib/legion/extensions/agentic/language/conceptual_metaphor/version.rb +13 -0
  21. data/lib/legion/extensions/agentic/language/conceptual_metaphor.rb +19 -0
  22. data/lib/legion/extensions/agentic/language/frame_semantics/helpers/client.rb +23 -0
  23. data/lib/legion/extensions/agentic/language/frame_semantics/helpers/constants.rb +32 -0
  24. data/lib/legion/extensions/agentic/language/frame_semantics/helpers/frame.rb +109 -0
  25. data/lib/legion/extensions/agentic/language/frame_semantics/helpers/frame_engine.rb +139 -0
  26. data/lib/legion/extensions/agentic/language/frame_semantics/helpers/frame_instance.rb +51 -0
  27. data/lib/legion/extensions/agentic/language/frame_semantics/runners/frame_semantics.rb +108 -0
  28. data/lib/legion/extensions/agentic/language/frame_semantics/version.rb +13 -0
  29. data/lib/legion/extensions/agentic/language/frame_semantics.rb +20 -0
  30. data/lib/legion/extensions/agentic/language/grammar/client.rb +29 -0
  31. data/lib/legion/extensions/agentic/language/grammar/helpers/constants.rb +38 -0
  32. data/lib/legion/extensions/agentic/language/grammar/helpers/construal.rb +68 -0
  33. data/lib/legion/extensions/agentic/language/grammar/helpers/construction.rb +68 -0
  34. data/lib/legion/extensions/agentic/language/grammar/helpers/grammar_engine.rb +119 -0
  35. data/lib/legion/extensions/agentic/language/grammar/runners/cognitive_grammar.rb +100 -0
  36. data/lib/legion/extensions/agentic/language/grammar/version.rb +13 -0
  37. data/lib/legion/extensions/agentic/language/grammar.rb +20 -0
  38. data/lib/legion/extensions/agentic/language/inner_speech/client.rb +19 -0
  39. data/lib/legion/extensions/agentic/language/inner_speech/helpers/constants.rb +53 -0
  40. data/lib/legion/extensions/agentic/language/inner_speech/helpers/inner_voice.rb +141 -0
  41. data/lib/legion/extensions/agentic/language/inner_speech/helpers/speech_stream.rb +128 -0
  42. data/lib/legion/extensions/agentic/language/inner_speech/helpers/utterance.rb +90 -0
  43. data/lib/legion/extensions/agentic/language/inner_speech/runners/inner_speech.rb +87 -0
  44. data/lib/legion/extensions/agentic/language/inner_speech/version.rb +13 -0
  45. data/lib/legion/extensions/agentic/language/inner_speech.rb +20 -0
  46. data/lib/legion/extensions/agentic/language/language/client.rb +26 -0
  47. data/lib/legion/extensions/agentic/language/language/helpers/constants.rb +43 -0
  48. data/lib/legion/extensions/agentic/language/language/helpers/lexicon.rb +63 -0
  49. data/lib/legion/extensions/agentic/language/language/helpers/summarizer.rb +167 -0
  50. data/lib/legion/extensions/agentic/language/language/runners/language.rb +134 -0
  51. data/lib/legion/extensions/agentic/language/language/version.rb +13 -0
  52. data/lib/legion/extensions/agentic/language/language.rb +19 -0
  53. data/lib/legion/extensions/agentic/language/narrative_reasoning/client.rb +28 -0
  54. data/lib/legion/extensions/agentic/language/narrative_reasoning/helpers/narrative.rb +123 -0
  55. data/lib/legion/extensions/agentic/language/narrative_reasoning/helpers/narrative_engine.rb +122 -0
  56. data/lib/legion/extensions/agentic/language/narrative_reasoning/helpers/narrative_event.rb +41 -0
  57. data/lib/legion/extensions/agentic/language/narrative_reasoning/runners/narrative_reasoning.rb +122 -0
  58. data/lib/legion/extensions/agentic/language/narrative_reasoning/version.rb +13 -0
  59. data/lib/legion/extensions/agentic/language/narrative_reasoning.rb +18 -0
  60. data/lib/legion/extensions/agentic/language/narrator/client.rb +27 -0
  61. data/lib/legion/extensions/agentic/language/narrator/helpers/constants.rb +69 -0
  62. data/lib/legion/extensions/agentic/language/narrator/helpers/journal.rb +68 -0
  63. data/lib/legion/extensions/agentic/language/narrator/helpers/llm_enhancer.rb +105 -0
  64. data/lib/legion/extensions/agentic/language/narrator/helpers/prose.rb +122 -0
  65. data/lib/legion/extensions/agentic/language/narrator/helpers/synthesizer.rb +138 -0
  66. data/lib/legion/extensions/agentic/language/narrator/runners/narrator.rb +196 -0
  67. data/lib/legion/extensions/agentic/language/narrator/version.rb +13 -0
  68. data/lib/legion/extensions/agentic/language/narrator.rb +21 -0
  69. data/lib/legion/extensions/agentic/language/pragmatic_inference/client.rb +28 -0
  70. data/lib/legion/extensions/agentic/language/pragmatic_inference/helpers/constants.rb +52 -0
  71. data/lib/legion/extensions/agentic/language/pragmatic_inference/helpers/pragmatic_engine.rb +164 -0
  72. data/lib/legion/extensions/agentic/language/pragmatic_inference/helpers/utterance.rb +84 -0
  73. data/lib/legion/extensions/agentic/language/pragmatic_inference/runners/pragmatic_inference.rb +136 -0
  74. data/lib/legion/extensions/agentic/language/pragmatic_inference/version.rb +13 -0
  75. data/lib/legion/extensions/agentic/language/pragmatic_inference.rb +18 -0
  76. data/lib/legion/extensions/agentic/language/version.rb +11 -0
  77. data/lib/legion/extensions/agentic/language.rb +28 -0
  78. data/spec/legion/extensions/agentic/language/conceptual_blending/client_spec.rb +78 -0
  79. data/spec/legion/extensions/agentic/language/conceptual_blending/helpers/blend_spec.rb +141 -0
  80. data/spec/legion/extensions/agentic/language/conceptual_blending/helpers/blending_engine_spec.rb +211 -0
  81. data/spec/legion/extensions/agentic/language/conceptual_blending/helpers/mental_space_spec.rb +85 -0
  82. data/spec/legion/extensions/agentic/language/conceptual_blending/runners/conceptual_blending_spec.rb +162 -0
  83. data/spec/legion/extensions/agentic/language/conceptual_metaphor/client_spec.rb +29 -0
  84. data/spec/legion/extensions/agentic/language/conceptual_metaphor/helpers/metaphor_engine_spec.rb +166 -0
  85. data/spec/legion/extensions/agentic/language/conceptual_metaphor/helpers/metaphor_spec.rb +133 -0
  86. data/spec/legion/extensions/agentic/language/conceptual_metaphor/runners/conceptual_metaphor_spec.rb +133 -0
  87. data/spec/legion/extensions/agentic/language/frame_semantics/helpers/frame_engine_spec.rb +227 -0
  88. data/spec/legion/extensions/agentic/language/frame_semantics/helpers/frame_instance_spec.rb +83 -0
  89. data/spec/legion/extensions/agentic/language/frame_semantics/helpers/frame_spec.rb +213 -0
  90. data/spec/legion/extensions/agentic/language/frame_semantics/runners/frame_semantics_spec.rb +155 -0
  91. data/spec/legion/extensions/agentic/language/grammar/client_spec.rb +121 -0
  92. data/spec/legion/extensions/agentic/language/grammar/cognitive_grammar_spec.rb +18 -0
  93. data/spec/legion/extensions/agentic/language/grammar/helpers/constants_spec.rb +67 -0
  94. data/spec/legion/extensions/agentic/language/grammar/helpers/construal_spec.rb +124 -0
  95. data/spec/legion/extensions/agentic/language/grammar/helpers/construction_spec.rb +155 -0
  96. data/spec/legion/extensions/agentic/language/grammar/helpers/grammar_engine_spec.rb +206 -0
  97. data/spec/legion/extensions/agentic/language/grammar/runners/cognitive_grammar_spec.rb +189 -0
  98. data/spec/legion/extensions/agentic/language/inner_speech/client_spec.rb +39 -0
  99. data/spec/legion/extensions/agentic/language/inner_speech/helpers/inner_voice_spec.rb +185 -0
  100. data/spec/legion/extensions/agentic/language/inner_speech/helpers/speech_stream_spec.rb +158 -0
  101. data/spec/legion/extensions/agentic/language/inner_speech/helpers/utterance_spec.rb +121 -0
  102. data/spec/legion/extensions/agentic/language/inner_speech/runners/inner_speech_spec.rb +102 -0
  103. data/spec/legion/extensions/agentic/language/language/client_spec.rb +20 -0
  104. data/spec/legion/extensions/agentic/language/language/helpers/constants_spec.rb +31 -0
  105. data/spec/legion/extensions/agentic/language/language/helpers/lexicon_spec.rb +116 -0
  106. data/spec/legion/extensions/agentic/language/language/helpers/summarizer_spec.rb +224 -0
  107. data/spec/legion/extensions/agentic/language/language/runners/language_spec.rb +169 -0
  108. data/spec/legion/extensions/agentic/language/narrative_reasoning/client_spec.rb +19 -0
  109. data/spec/legion/extensions/agentic/language/narrative_reasoning/helpers/narrative_engine_spec.rb +182 -0
  110. data/spec/legion/extensions/agentic/language/narrative_reasoning/helpers/narrative_event_spec.rb +61 -0
  111. data/spec/legion/extensions/agentic/language/narrative_reasoning/helpers/narrative_spec.rb +168 -0
  112. data/spec/legion/extensions/agentic/language/narrative_reasoning/runners/narrative_reasoning_spec.rb +174 -0
  113. data/spec/legion/extensions/agentic/language/narrator/client_spec.rb +24 -0
  114. data/spec/legion/extensions/agentic/language/narrator/helpers/journal_spec.rb +95 -0
  115. data/spec/legion/extensions/agentic/language/narrator/helpers/llm_enhancer_spec.rb +107 -0
  116. data/spec/legion/extensions/agentic/language/narrator/helpers/prose_spec.rb +134 -0
  117. data/spec/legion/extensions/agentic/language/narrator/helpers/synthesizer_spec.rb +89 -0
  118. data/spec/legion/extensions/agentic/language/narrator/runners/narrator_llm_spec.rb +74 -0
  119. data/spec/legion/extensions/agentic/language/narrator/runners/narrator_spec.rb +126 -0
  120. data/spec/legion/extensions/agentic/language/pragmatic_inference/client_spec.rb +19 -0
  121. data/spec/legion/extensions/agentic/language/pragmatic_inference/helpers/constants_spec.rb +73 -0
  122. data/spec/legion/extensions/agentic/language/pragmatic_inference/helpers/pragmatic_engine_spec.rb +185 -0
  123. data/spec/legion/extensions/agentic/language/pragmatic_inference/helpers/utterance_spec.rb +111 -0
  124. data/spec/legion/extensions/agentic/language/pragmatic_inference/runners/pragmatic_inference_spec.rb +231 -0
  125. data/spec/spec_helper.rb +33 -0
  126. metadata +210 -0
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/agentic/language/language/helpers/constants'
4
+ require 'legion/extensions/agentic/language/language/helpers/summarizer'
5
+ require 'legion/extensions/agentic/language/language/helpers/lexicon'
6
+ require 'legion/extensions/agentic/language/language/runners/language'
7
+
8
+ module Legion
9
+ module Extensions
10
+ module Agentic
11
+ module Language
12
+ module Language
13
+ class Client
14
+ include Runners::Language
15
+
16
+ attr_reader :lexicon
17
+
18
+ def initialize(lexicon: nil, **)
19
+ @lexicon = lexicon || Helpers::Lexicon.new
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Agentic
6
+ module Language
7
+ module Language
8
+ module Helpers
9
+ module Constants
10
+ # Maximum traces to consider in a single summarization
11
+ MAX_TRACES_PER_SUMMARY = 50
12
+
13
+ # Minimum strength threshold for including a trace in a summary
14
+ MIN_SUMMARY_STRENGTH = 0.1
15
+
16
+ # Summary depth levels
17
+ DEPTHS = %i[brief standard detailed].freeze
18
+
19
+ # Trace type priority for summary ordering
20
+ TYPE_PRIORITY = {
21
+ firmware: 0,
22
+ identity: 1,
23
+ procedural: 2,
24
+ semantic: 3,
25
+ trust: 4,
26
+ episodic: 5,
27
+ sensory: 6
28
+ }.freeze
29
+
30
+ # Knowledge quality thresholds
31
+ KNOWLEDGE_RICH = 10 # traces for "rich knowledge"
32
+ KNOWLEDGE_MODERATE = 5 # traces for "moderate knowledge"
33
+ KNOWLEDGE_SPARSE = 1 # traces for "sparse knowledge"
34
+
35
+ # Wonder resolution: minimum traces to consider a domain "known"
36
+ RESOLUTION_THRESHOLD = 3
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Agentic
6
+ module Language
7
+ module Language
8
+ module Helpers
9
+ class Lexicon
10
+ attr_reader :domain_summaries
11
+
12
+ def initialize
13
+ @domain_summaries = {}
14
+ end
15
+
16
+ def store_summary(domain, summary)
17
+ domain = domain.to_sym
18
+ @domain_summaries[domain] = summary.merge(cached_at: Time.now.utc)
19
+ end
20
+
21
+ def get_summary(domain)
22
+ @domain_summaries[domain.to_sym]
23
+ end
24
+
25
+ def known_domains
26
+ @domain_summaries.keys
27
+ end
28
+
29
+ def knowledge_map
30
+ @domain_summaries.transform_values do |summary|
31
+ {
32
+ knowledge_level: summary[:knowledge_level],
33
+ trace_count: summary[:trace_count],
34
+ cached_at: summary[:cached_at]
35
+ }
36
+ end
37
+ end
38
+
39
+ def stale?(domain, max_age: 300)
40
+ summary = get_summary(domain)
41
+ return true unless summary
42
+
43
+ (Time.now.utc - summary[:cached_at]) > max_age
44
+ end
45
+
46
+ def clear(domain = nil)
47
+ if domain
48
+ @domain_summaries.delete(domain.to_sym)
49
+ else
50
+ @domain_summaries.clear
51
+ end
52
+ end
53
+
54
+ def size
55
+ @domain_summaries.size
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,167 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Agentic
6
+ module Language
7
+ module Language
8
+ module Helpers
9
+ module Summarizer
10
+ module_function
11
+
12
+ def summarize_domain(traces, domain:, depth: :standard)
13
+ return empty_summary(domain) if traces.empty?
14
+
15
+ grouped = group_by_type(traces)
16
+ knowledge_level = classify_knowledge(traces.size)
17
+
18
+ {
19
+ domain: domain,
20
+ knowledge_level: knowledge_level,
21
+ trace_count: traces.size,
22
+ type_breakdown: type_breakdown(grouped),
23
+ key_facts: extract_key_facts(grouped, depth: depth),
24
+ strength_stats: strength_stats(traces),
25
+ emotional_tone: emotional_tone(traces),
26
+ freshness: freshness_assessment(traces)
27
+ }
28
+ end
29
+
30
+ def group_by_type(traces)
31
+ grouped = traces.group_by { |t| t[:trace_type] || :unknown }
32
+ grouped.sort_by { |type, _| Constants::TYPE_PRIORITY[type] || 99 }.to_h
33
+ end
34
+
35
+ def type_breakdown(grouped)
36
+ grouped.transform_values(&:size)
37
+ end
38
+
39
+ def extract_key_facts(grouped, depth: :standard)
40
+ limit = case depth
41
+ when :brief then 3
42
+ when :detailed then 15
43
+ else 7
44
+ end
45
+
46
+ facts = []
47
+ grouped.each do |type, traces|
48
+ sorted = traces.sort_by { |t| -(t[:strength] || 0) }
49
+ count = [sorted.size, limit_per_type(type, limit)].min
50
+ sorted.first(count).each do |trace|
51
+ facts << format_fact(trace, type)
52
+ end
53
+ end
54
+
55
+ facts.first(limit)
56
+ end
57
+
58
+ def format_fact(trace, type)
59
+ content = extract_content_text(trace[:content_payload])
60
+ {
61
+ trace_id: trace[:trace_id],
62
+ type: type,
63
+ content: content,
64
+ strength: (trace[:strength] || 0).round(3),
65
+ confidence: (trace[:confidence] || 0.5).round(3),
66
+ domain_tags: trace[:domain_tags] || []
67
+ }
68
+ end
69
+
70
+ def extract_content_text(payload)
71
+ case payload
72
+ when String then payload
73
+ when Hash then payload[:text] || payload[:content] || payload[:summary] || payload.to_s
74
+ when Array then payload.first.to_s
75
+ else payload.to_s
76
+ end
77
+ end
78
+
79
+ def classify_knowledge(count)
80
+ if count >= Constants::KNOWLEDGE_RICH then :rich
81
+ elsif count >= Constants::KNOWLEDGE_MODERATE then :moderate
82
+ elsif count >= Constants::KNOWLEDGE_SPARSE then :sparse
83
+ else :none
84
+ end
85
+ end
86
+
87
+ def strength_stats(traces)
88
+ strengths = traces.map { |t| t[:strength] || 0 }
89
+ {
90
+ mean: (strengths.sum / strengths.size).round(3),
91
+ max: strengths.max.round(3),
92
+ min: strengths.min.round(3),
93
+ strong: strengths.count { |s| s >= 0.5 },
94
+ weak: strengths.count { |s| s < 0.3 }
95
+ }
96
+ end
97
+
98
+ def emotional_tone(traces)
99
+ valences = traces.map { |t| t[:emotional_valence] || 0.0 }
100
+ intensities = traces.map { |t| t[:emotional_intensity] || 0.0 }
101
+
102
+ avg_valence = valences.sum / valences.size
103
+ avg_intensity = intensities.sum / intensities.size
104
+
105
+ {
106
+ avg_valence: avg_valence.round(3),
107
+ avg_intensity: avg_intensity.round(3),
108
+ tone: tone_label(avg_valence, avg_intensity)
109
+ }
110
+ end
111
+
112
+ def tone_label(valence, intensity)
113
+ if intensity < 0.2 then :neutral
114
+ elsif valence > 0.3 then :positive
115
+ elsif valence < -0.3 then :negative
116
+ else :mixed
117
+ end
118
+ end
119
+
120
+ def freshness_assessment(traces)
121
+ now = Time.now.utc
122
+ ages = traces.map { |t| now - (t[:last_reinforced] || t[:created_at] || now) }
123
+ avg_age = ages.sum / ages.size
124
+
125
+ {
126
+ avg_age_seconds: avg_age.round(1),
127
+ freshest: ages.min.round(1),
128
+ stalest: ages.max.round(1),
129
+ label: freshness_label(avg_age)
130
+ }
131
+ end
132
+
133
+ def freshness_label(avg_age)
134
+ if avg_age < 60 then :very_fresh
135
+ elsif avg_age < 3600 then :fresh
136
+ elsif avg_age < 86_400 then :aging
137
+ else :stale
138
+ end
139
+ end
140
+
141
+ def empty_summary(domain)
142
+ {
143
+ domain: domain,
144
+ knowledge_level: :none,
145
+ trace_count: 0,
146
+ type_breakdown: {},
147
+ key_facts: [],
148
+ strength_stats: { mean: 0.0, max: 0.0, min: 0.0, strong: 0, weak: 0 },
149
+ emotional_tone: { avg_valence: 0.0, avg_intensity: 0.0, tone: :neutral },
150
+ freshness: { avg_age_seconds: 0.0, freshest: 0.0, stalest: 0.0, label: :stale }
151
+ }
152
+ end
153
+
154
+ def limit_per_type(type, total_limit)
155
+ case type
156
+ when :firmware, :identity then [total_limit, 3].min
157
+ when :procedural then [total_limit, 2].min
158
+ else [total_limit, 5].min
159
+ end
160
+ end
161
+ end
162
+ end
163
+ end
164
+ end
165
+ end
166
+ end
167
+ end
@@ -0,0 +1,134 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Agentic
6
+ module Language
7
+ module Language
8
+ module Runners
9
+ module Language
10
+ include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers) &&
11
+ Legion::Extensions::Helpers.const_defined?(:Lex)
12
+
13
+ def summarize(domain:, depth: :standard, traces: [], **)
14
+ traces = filter_traces(traces, domain)
15
+ summary = Helpers::Summarizer.summarize_domain(traces, domain: domain.to_sym, depth: depth.to_sym)
16
+ lexicon.store_summary(domain, summary)
17
+
18
+ Legion::Logging.debug "[language] summarize domain=#{domain} traces=#{traces.size} " \
19
+ "knowledge=#{summary[:knowledge_level]}"
20
+ summary
21
+ end
22
+
23
+ def what_do_i_know(domain:, depth: :standard, traces: [], **)
24
+ summary = if lexicon.stale?(domain)
25
+ summarize(domain: domain, depth: depth, traces: traces)
26
+ else
27
+ lexicon.get_summary(domain)
28
+ end
29
+
30
+ prose = generate_knowledge_prose(summary)
31
+
32
+ {
33
+ domain: domain.to_sym,
34
+ knowledge_level: summary[:knowledge_level],
35
+ prose: prose,
36
+ fact_count: summary[:key_facts]&.size || 0,
37
+ summary: summary
38
+ }
39
+ end
40
+
41
+ def can_answer_wonder?(wonder:, traces: [], **)
42
+ domain = wonder.is_a?(Hash) ? wonder[:domain] : :general
43
+ relevant = filter_traces(traces, domain)
44
+
45
+ answerable = relevant.size >= Helpers::Constants::RESOLUTION_THRESHOLD
46
+ confidence = answerable ? compute_answer_confidence(relevant) : 0.0
47
+
48
+ {
49
+ answerable: answerable,
50
+ confidence: confidence.round(3),
51
+ domain: domain,
52
+ trace_count: relevant.size,
53
+ threshold: Helpers::Constants::RESOLUTION_THRESHOLD
54
+ }
55
+ end
56
+
57
+ def knowledge_map(**)
58
+ {
59
+ domains: lexicon.knowledge_map,
60
+ known_domains: lexicon.known_domains,
61
+ total_domains: lexicon.size
62
+ }
63
+ end
64
+
65
+ def language_stats(**)
66
+ {
67
+ cached_domains: lexicon.size,
68
+ known_domains: lexicon.known_domains,
69
+ knowledge_map: lexicon.knowledge_map
70
+ }
71
+ end
72
+
73
+ private
74
+
75
+ def lexicon
76
+ @lexicon ||= Helpers::Lexicon.new
77
+ end
78
+
79
+ def filter_traces(traces, domain)
80
+ return [] unless traces.is_a?(Array)
81
+
82
+ domain_sym = domain.to_sym
83
+ domain_str = domain.to_s
84
+
85
+ matching = traces.select do |t|
86
+ tags = t[:domain_tags] || []
87
+ tags.any? { |tag| tag.to_s == domain_str || tag.to_sym == domain_sym }
88
+ end
89
+
90
+ matching
91
+ .select { |t| (t[:strength] || 0) >= Helpers::Constants::MIN_SUMMARY_STRENGTH }
92
+ .sort_by { |t| -(t[:strength] || 0) }
93
+ .first(Helpers::Constants::MAX_TRACES_PER_SUMMARY)
94
+ end
95
+
96
+ def generate_knowledge_prose(summary)
97
+ domain = summary[:domain]
98
+ level = summary[:knowledge_level]
99
+ count = summary[:trace_count]
100
+ facts = summary[:key_facts] || []
101
+
102
+ base = "About #{domain}: I have #{level} knowledge (#{count} traces)."
103
+
104
+ if facts.empty?
105
+ "#{base} No specific facts available."
106
+ else
107
+ fact_lines = facts.first(5).map { |f| "- #{truncate(f[:content], 120)}" }
108
+ "#{base}\nKey facts:\n#{fact_lines.join("\n")}"
109
+ end
110
+ end
111
+
112
+ def compute_answer_confidence(traces)
113
+ return 0.0 if traces.empty?
114
+
115
+ strengths = traces.map { |t| t[:strength] || 0.0 }
116
+ confidences = traces.map { |t| t[:confidence] || 0.5 }
117
+
118
+ avg_strength = strengths.sum / strengths.size
119
+ avg_confidence = confidences.sum / confidences.size
120
+
121
+ ((avg_strength * 0.5) + (avg_confidence * 0.5)).clamp(0.0, 1.0)
122
+ end
123
+
124
+ def truncate(text, max_length)
125
+ text = text.to_s
126
+ text.length > max_length ? "#{text[0...max_length]}..." : text
127
+ end
128
+ end
129
+ end
130
+ end
131
+ end
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Agentic
6
+ module Language
7
+ module Language
8
+ VERSION = '0.1.0'
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/agentic/language/language/version'
4
+ require 'legion/extensions/agentic/language/language/helpers/constants'
5
+ require 'legion/extensions/agentic/language/language/helpers/summarizer'
6
+ require 'legion/extensions/agentic/language/language/helpers/lexicon'
7
+ require 'legion/extensions/agentic/language/language/runners/language'
8
+ require 'legion/extensions/agentic/language/language/client'
9
+
10
+ module Legion
11
+ module Extensions
12
+ module Agentic
13
+ module Language
14
+ module Language
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/agentic/language/narrative_reasoning/helpers/narrative_event'
4
+ require 'legion/extensions/agentic/language/narrative_reasoning/helpers/narrative'
5
+ require 'legion/extensions/agentic/language/narrative_reasoning/helpers/narrative_engine'
6
+ require 'legion/extensions/agentic/language/narrative_reasoning/runners/narrative_reasoning'
7
+
8
+ module Legion
9
+ module Extensions
10
+ module Agentic
11
+ module Language
12
+ module NarrativeReasoning
13
+ class Client
14
+ include Runners::NarrativeReasoning
15
+
16
+ def initialize(**)
17
+ @narrative_engine = Helpers::NarrativeEngine.new
18
+ end
19
+
20
+ private
21
+
22
+ attr_reader :narrative_engine
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+
5
+ module Legion
6
+ module Extensions
7
+ module Agentic
8
+ module Language
9
+ module NarrativeReasoning
10
+ module Helpers
11
+ class Narrative
12
+ ARC_STAGES = %i[beginning rising_action climax falling_action resolution].freeze
13
+
14
+ COHERENCE_LABELS = {
15
+ (0.8..) => :compelling,
16
+ (0.6...0.8) => :coherent,
17
+ (0.4...0.6) => :developing,
18
+ (0.2...0.4) => :fragmented,
19
+ (..0.2) => :incoherent
20
+ }.freeze
21
+
22
+ DEFAULT_COHERENCE = 0.5
23
+ COHERENCE_FLOOR = 0.0
24
+ COHERENCE_CEILING = 1.0
25
+ COHERENCE_BOOST = 0.1
26
+ DECAY_RATE = 0.02
27
+
28
+ attr_reader :id, :title, :domain, :events, :characters, :themes,
29
+ :arc_stage, :coherence, :created_at, :last_updated_at
30
+
31
+ def initialize(title:, domain: nil, id: nil)
32
+ @id = id || SecureRandom.uuid
33
+ @title = title
34
+ @domain = domain
35
+ @events = []
36
+ @characters = []
37
+ @themes = []
38
+ @arc_stage = ARC_STAGES.first
39
+ @coherence = DEFAULT_COHERENCE
40
+ @created_at = Time.now.utc
41
+ @last_updated_at = Time.now.utc
42
+ end
43
+
44
+ def add_event(event)
45
+ @events << event
46
+ event.characters.each { |chr| @characters << chr unless @characters.include?(chr) }
47
+ auto_advance_arc
48
+ @coherence = (@coherence + COHERENCE_BOOST).clamp(COHERENCE_FLOOR, COHERENCE_CEILING)
49
+ @last_updated_at = Time.now.utc
50
+ event
51
+ end
52
+
53
+ def add_theme(theme)
54
+ @themes << theme unless @themes.include?(theme)
55
+ @last_updated_at = Time.now.utc
56
+ theme
57
+ end
58
+
59
+ def advance_arc!
60
+ idx = ARC_STAGES.index(@arc_stage)
61
+ return @arc_stage if idx.nil? || idx >= ARC_STAGES.size - 1
62
+
63
+ @arc_stage = ARC_STAGES[idx + 1]
64
+ @last_updated_at = Time.now.utc
65
+ @arc_stage
66
+ end
67
+
68
+ def causal_chain
69
+ chain = []
70
+ @events.each do |event|
71
+ event.causes.each do |cause_id|
72
+ cause = @events.find { |e| e.id == cause_id }
73
+ chain << { cause: cause&.to_h, effect: event.to_h } if cause
74
+ end
75
+ end
76
+ chain
77
+ end
78
+
79
+ def coherence_label
80
+ COHERENCE_LABELS.find { |range, _label| range.cover?(@coherence) }&.last || :incoherent
81
+ end
82
+
83
+ def complete?
84
+ @arc_stage == :resolution
85
+ end
86
+
87
+ def decay_coherence
88
+ @coherence = (@coherence - DECAY_RATE).clamp(COHERENCE_FLOOR, COHERENCE_CEILING)
89
+ end
90
+
91
+ def to_h
92
+ {
93
+ id: @id,
94
+ title: @title,
95
+ domain: @domain,
96
+ events: @events.map(&:to_h),
97
+ characters: @characters,
98
+ themes: @themes,
99
+ arc_stage: @arc_stage,
100
+ coherence: @coherence,
101
+ coherence_label: coherence_label,
102
+ complete: complete?,
103
+ created_at: @created_at,
104
+ last_updated_at: @last_updated_at
105
+ }
106
+ end
107
+
108
+ private
109
+
110
+ def auto_advance_arc
111
+ return if complete?
112
+
113
+ events_per_stage = [(@events.size / 5.0).ceil, 1].max
114
+ stage_idx = [(@events.size / events_per_stage), ARC_STAGES.size - 1].min
115
+ @arc_stage = ARC_STAGES[stage_idx]
116
+ end
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end
122
+ end
123
+ end