lex-rfp 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 (39) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/ci.yml +16 -0
  3. data/.gitignore +12 -0
  4. data/.rspec +3 -0
  5. data/.rubocop.yml +66 -0
  6. data/CHANGELOG.md +15 -0
  7. data/CLAUDE.md +80 -0
  8. data/Dockerfile +6 -0
  9. data/Gemfile +12 -0
  10. data/LICENSE +21 -0
  11. data/README.md +119 -0
  12. data/lex-rfp.gemspec +32 -0
  13. data/lib/legion/extensions/rfp/analytics/client.rb +31 -0
  14. data/lib/legion/extensions/rfp/analytics/helpers/client.rb +24 -0
  15. data/lib/legion/extensions/rfp/analytics/runners/metrics.rb +87 -0
  16. data/lib/legion/extensions/rfp/analytics/runners/quality.rb +121 -0
  17. data/lib/legion/extensions/rfp/analytics/runners/win_rates.rb +88 -0
  18. data/lib/legion/extensions/rfp/analytics.rb +16 -0
  19. data/lib/legion/extensions/rfp/generate/client.rb +31 -0
  20. data/lib/legion/extensions/rfp/generate/helpers/client.rb +24 -0
  21. data/lib/legion/extensions/rfp/generate/runners/drafts.rb +98 -0
  22. data/lib/legion/extensions/rfp/generate/runners/sections.rb +97 -0
  23. data/lib/legion/extensions/rfp/generate/runners/templates.rb +61 -0
  24. data/lib/legion/extensions/rfp/generate.rb +16 -0
  25. data/lib/legion/extensions/rfp/ingest/client.rb +31 -0
  26. data/lib/legion/extensions/rfp/ingest/helpers/client.rb +24 -0
  27. data/lib/legion/extensions/rfp/ingest/runners/corpus.rb +66 -0
  28. data/lib/legion/extensions/rfp/ingest/runners/documents.rb +86 -0
  29. data/lib/legion/extensions/rfp/ingest/runners/parser.rb +84 -0
  30. data/lib/legion/extensions/rfp/ingest.rb +16 -0
  31. data/lib/legion/extensions/rfp/review/client.rb +31 -0
  32. data/lib/legion/extensions/rfp/review/helpers/client.rb +24 -0
  33. data/lib/legion/extensions/rfp/review/runners/approvals.rb +70 -0
  34. data/lib/legion/extensions/rfp/review/runners/comments.rb +76 -0
  35. data/lib/legion/extensions/rfp/review/runners/workflows.rb +86 -0
  36. data/lib/legion/extensions/rfp/review.rb +16 -0
  37. data/lib/legion/extensions/rfp/version.rb +9 -0
  38. data/lib/legion/extensions/rfp.rb +15 -0
  39. metadata +99 -0
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Rfp
6
+ module Ingest
7
+ module Runners
8
+ module Parser
9
+ extend Legion::Extensions::Rfp::Ingest::Helpers::Client
10
+
11
+ def parse_rfp_questions(text:, **)
12
+ questions = []
13
+ current_section = nil
14
+
15
+ text.each_line do |line|
16
+ stripped = line.strip
17
+ next if stripped.empty?
18
+
19
+ if stripped.match?(/\A(?:section|part|category)\s+/i) || stripped.match?(/\A[A-Z][A-Z\s]{2,}[A-Z]\z/)
20
+ current_section = stripped
21
+ elsif stripped.match?(/\A\d+[.)]\s/) || stripped.match?(/\A[a-z][.)]\s/i)
22
+ questions << {
23
+ section: current_section,
24
+ question: stripped.sub(/\A[\da-z][.)]\s*/i, ''),
25
+ raw: stripped
26
+ }
27
+ end
28
+ end
29
+
30
+ { result: questions, count: questions.length }
31
+ end
32
+
33
+ def extract_requirements(text:, **)
34
+ requirements = []
35
+
36
+ text.each_line do |line|
37
+ stripped = line.strip
38
+ next if stripped.empty?
39
+
40
+ if stripped.match?(/\b(?:must|shall|required|mandatory)\b/i)
41
+ requirements << { text: stripped, type: :mandatory }
42
+ elsif stripped.match?(/\b(?:should|preferred|desired|optional)\b/i)
43
+ requirements << { text: stripped, type: :preferred }
44
+ end
45
+ end
46
+
47
+ { result: requirements, mandatory: requirements.count { |r| r[:type] == :mandatory },
48
+ preferred: requirements.count { |r| r[:type] == :preferred } }
49
+ end
50
+
51
+ def extract_sections(text:, **)
52
+ sections = []
53
+ current_title = nil
54
+ current_content = []
55
+
56
+ text.each_line do |line|
57
+ stripped = line.strip
58
+ if stripped.match?(/\A(?:#{section_heading_pattern})/)
59
+ sections << { title: current_title, content: current_content.join("\n").strip } if current_title
60
+ current_title = stripped
61
+ current_content = []
62
+ else
63
+ current_content << line
64
+ end
65
+ end
66
+
67
+ sections << { title: current_title, content: current_content.join("\n").strip } if current_title
68
+ { result: sections, count: sections.length }
69
+ end
70
+
71
+ private
72
+
73
+ def section_heading_pattern
74
+ '(?:section|part|category|chapter)\s+\d|[A-Z][A-Z\s]{2,}[A-Z]|\d+\.\s+[A-Z]'
75
+ end
76
+
77
+ include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers) &&
78
+ Legion::Extensions::Helpers.const_defined?(:Lex)
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/rfp/ingest/helpers/client'
4
+ require 'legion/extensions/rfp/ingest/runners/documents'
5
+ require 'legion/extensions/rfp/ingest/runners/corpus'
6
+ require 'legion/extensions/rfp/ingest/runners/parser'
7
+ require 'legion/extensions/rfp/ingest/client'
8
+
9
+ module Legion
10
+ module Extensions
11
+ module Rfp
12
+ module Ingest
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'helpers/client'
4
+ require_relative 'runners/workflows'
5
+ require_relative 'runners/comments'
6
+ require_relative 'runners/approvals'
7
+
8
+ module Legion
9
+ module Extensions
10
+ module Rfp
11
+ module Review
12
+ class Client
13
+ include Helpers::Client
14
+ include Runners::Workflows
15
+ include Runners::Comments
16
+ include Runners::Approvals
17
+
18
+ attr_reader :opts
19
+
20
+ def initialize(base_url: nil, token: nil, **)
21
+ @opts = { base_url: base_url, token: token }.compact
22
+ end
23
+
24
+ def client(**override)
25
+ super(**@opts, **override)
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'faraday'
4
+
5
+ module Legion
6
+ module Extensions
7
+ module Rfp
8
+ module Review
9
+ module Helpers
10
+ module Client
11
+ def client(base_url: 'http://localhost:4567', token: nil, **)
12
+ Faraday.new(url: base_url) do |conn|
13
+ conn.request :json
14
+ conn.response :json, content_type: /\bjson$/
15
+ conn.headers['Content-Type'] = 'application/json'
16
+ conn.headers['Authorization'] = "Bearer #{token}" if token
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Rfp
6
+ module Review
7
+ module Runners
8
+ module Approvals
9
+ extend Legion::Extensions::Rfp::Review::Helpers::Client
10
+
11
+ def approve_section(workflow_id:, section_name:, approved_by:, notes: nil, **)
12
+ {
13
+ result: {
14
+ workflow_id: workflow_id,
15
+ section: section_name,
16
+ status: :approved,
17
+ approved_by: approved_by,
18
+ notes: notes,
19
+ approved_at: Time.now.iso8601
20
+ }
21
+ }
22
+ end
23
+
24
+ def reject_section(workflow_id:, section_name:, rejected_by:, reason:, **)
25
+ {
26
+ result: {
27
+ workflow_id: workflow_id,
28
+ section: section_name,
29
+ status: :rejected,
30
+ rejected_by: rejected_by,
31
+ reason: reason,
32
+ rejected_at: Time.now.iso8601
33
+ }
34
+ }
35
+ end
36
+
37
+ def approve_proposal(workflow_id:, approved_by:, notes: nil, **)
38
+ {
39
+ result: {
40
+ workflow_id: workflow_id,
41
+ status: :approved,
42
+ approved_by: approved_by,
43
+ notes: notes,
44
+ approved_at: Time.now.iso8601
45
+ }
46
+ }
47
+ end
48
+
49
+ def check_readiness(sections:, **)
50
+ all_approved = sections.all? { |s| s[:status] == :approved }
51
+ pending = sections.reject { |s| s[:status] == :approved }
52
+
53
+ {
54
+ result: {
55
+ ready: all_approved,
56
+ total_sections: sections.length,
57
+ approved: sections.count { |s| s[:status] == :approved },
58
+ pending_sections: pending.map { |s| s[:name] }
59
+ }
60
+ }
61
+ end
62
+
63
+ include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers) &&
64
+ Legion::Extensions::Helpers.const_defined?(:Lex)
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Rfp
6
+ module Review
7
+ module Runners
8
+ module Comments
9
+ extend Legion::Extensions::Rfp::Review::Helpers::Client
10
+
11
+ def add_comment(workflow_id:, section_name:, author:, body:, type: :general, **)
12
+ comment = {
13
+ id: generate_comment_id,
14
+ workflow_id: workflow_id,
15
+ section: section_name,
16
+ author: author,
17
+ body: body,
18
+ type: type.to_sym,
19
+ created_at: Time.now.iso8601
20
+ }
21
+ { result: comment }
22
+ end
23
+
24
+ def list_comments(workflow_id:, section_name: nil, **)
25
+ {
26
+ result: [],
27
+ workflow_id: workflow_id,
28
+ section: section_name,
29
+ error: 'Persistence requires legion-data'
30
+ }
31
+ end
32
+
33
+ def resolve_comment(comment_id:, resolved_by:, **)
34
+ {
35
+ result: {
36
+ id: comment_id,
37
+ resolved: true,
38
+ resolved_by: resolved_by,
39
+ resolved_at: Time.now.iso8601
40
+ }
41
+ }
42
+ end
43
+
44
+ def request_revision(workflow_id:, section_name:, author:, reason:, **)
45
+ comment = add_comment(
46
+ workflow_id: workflow_id,
47
+ section_name: section_name,
48
+ author: author,
49
+ body: reason,
50
+ type: :revision_request
51
+ )
52
+
53
+ {
54
+ result: {
55
+ comment: comment[:result],
56
+ section: section_name,
57
+ new_status: :revision_requested,
58
+ workflow_id: workflow_id
59
+ }
60
+ }
61
+ end
62
+
63
+ private
64
+
65
+ def generate_comment_id
66
+ "rfp-cmt-#{Time.now.to_i}-#{rand(10_000)}"
67
+ end
68
+
69
+ include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers) &&
70
+ Legion::Extensions::Helpers.const_defined?(:Lex)
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Rfp
6
+ module Review
7
+ module Runners
8
+ module Workflows
9
+ extend Legion::Extensions::Rfp::Review::Helpers::Client
10
+
11
+ STATUSES = %i[draft in_review revision_requested approved rejected finalized].freeze
12
+
13
+ def create_workflow(proposal_id:, sections: [], reviewers: [], **)
14
+ workflow = {
15
+ id: generate_workflow_id,
16
+ proposal_id: proposal_id,
17
+ status: :draft,
18
+ sections: sections.map { |s| { name: s, status: :draft, reviewer: nil, comments: [] } },
19
+ reviewers: reviewers,
20
+ created_at: Time.now.iso8601,
21
+ updated_at: Time.now.iso8601
22
+ }
23
+ { result: workflow }
24
+ end
25
+
26
+ def get_workflow(workflow_id:, **)
27
+ { result: { id: workflow_id, status: :draft }, error: 'Persistence requires legion-data' }
28
+ end
29
+
30
+ def update_status(workflow_id:, status:, **)
31
+ status_sym = status.to_sym
32
+ return { result: nil, error: "Invalid status: #{status}. Valid: #{STATUSES.join(', ')}" } unless STATUSES.include?(status_sym)
33
+
34
+ { result: { id: workflow_id, status: status_sym, updated_at: Time.now.iso8601 } }
35
+ end
36
+
37
+ def update_section_status(workflow_id:, section_name:, status:, reviewer: nil, **)
38
+ status_sym = status.to_sym
39
+ return { result: nil, error: "Invalid status: #{status}" } unless STATUSES.include?(status_sym)
40
+
41
+ {
42
+ result: {
43
+ workflow_id: workflow_id,
44
+ section: section_name,
45
+ status: status_sym,
46
+ reviewer: reviewer,
47
+ updated_at: Time.now.iso8601
48
+ }
49
+ }
50
+ end
51
+
52
+ def submit_for_review(workflow_id:, reviewers:, **)
53
+ {
54
+ result: {
55
+ workflow_id: workflow_id,
56
+ status: :in_review,
57
+ reviewers: reviewers,
58
+ submitted_at: Time.now.iso8601
59
+ }
60
+ }
61
+ end
62
+
63
+ def finalize(workflow_id:, **)
64
+ {
65
+ result: {
66
+ workflow_id: workflow_id,
67
+ status: :finalized,
68
+ finalized_at: Time.now.iso8601
69
+ }
70
+ }
71
+ end
72
+
73
+ private
74
+
75
+ def generate_workflow_id
76
+ "rfp-wf-#{Time.now.to_i}-#{rand(10_000)}"
77
+ end
78
+
79
+ include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers) &&
80
+ Legion::Extensions::Helpers.const_defined?(:Lex)
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/rfp/review/helpers/client'
4
+ require 'legion/extensions/rfp/review/runners/workflows'
5
+ require 'legion/extensions/rfp/review/runners/comments'
6
+ require 'legion/extensions/rfp/review/runners/approvals'
7
+ require 'legion/extensions/rfp/review/client'
8
+
9
+ module Legion
10
+ module Extensions
11
+ module Rfp
12
+ module Review
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Rfp
6
+ VERSION = '0.1.0'
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/rfp/version'
4
+ require_relative 'rfp/ingest'
5
+ require_relative 'rfp/generate'
6
+ require_relative 'rfp/review'
7
+ require_relative 'rfp/analytics'
8
+
9
+ module Legion
10
+ module Extensions
11
+ module Rfp
12
+ extend Legion::Extensions::Core if Legion::Extensions.const_defined? :Core
13
+ end
14
+ end
15
+ end
metadata ADDED
@@ -0,0 +1,99 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: lex-rfp
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Esity
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: faraday
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '2.0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '2.0'
26
+ description: Generative AI-powered RFP and proposal automation for LegionIO. Ingests
27
+ past proposals into Apollo, generates draft responses via LLM pipeline with RAG,
28
+ and provides human-in-the-loop review workflows with analytics.
29
+ email:
30
+ - matthewdiverson@gmail.com
31
+ executables: []
32
+ extensions: []
33
+ extra_rdoc_files: []
34
+ files:
35
+ - ".github/workflows/ci.yml"
36
+ - ".gitignore"
37
+ - ".rspec"
38
+ - ".rubocop.yml"
39
+ - CHANGELOG.md
40
+ - CLAUDE.md
41
+ - Dockerfile
42
+ - Gemfile
43
+ - LICENSE
44
+ - README.md
45
+ - lex-rfp.gemspec
46
+ - lib/legion/extensions/rfp.rb
47
+ - lib/legion/extensions/rfp/analytics.rb
48
+ - lib/legion/extensions/rfp/analytics/client.rb
49
+ - lib/legion/extensions/rfp/analytics/helpers/client.rb
50
+ - lib/legion/extensions/rfp/analytics/runners/metrics.rb
51
+ - lib/legion/extensions/rfp/analytics/runners/quality.rb
52
+ - lib/legion/extensions/rfp/analytics/runners/win_rates.rb
53
+ - lib/legion/extensions/rfp/generate.rb
54
+ - lib/legion/extensions/rfp/generate/client.rb
55
+ - lib/legion/extensions/rfp/generate/helpers/client.rb
56
+ - lib/legion/extensions/rfp/generate/runners/drafts.rb
57
+ - lib/legion/extensions/rfp/generate/runners/sections.rb
58
+ - lib/legion/extensions/rfp/generate/runners/templates.rb
59
+ - lib/legion/extensions/rfp/ingest.rb
60
+ - lib/legion/extensions/rfp/ingest/client.rb
61
+ - lib/legion/extensions/rfp/ingest/helpers/client.rb
62
+ - lib/legion/extensions/rfp/ingest/runners/corpus.rb
63
+ - lib/legion/extensions/rfp/ingest/runners/documents.rb
64
+ - lib/legion/extensions/rfp/ingest/runners/parser.rb
65
+ - lib/legion/extensions/rfp/review.rb
66
+ - lib/legion/extensions/rfp/review/client.rb
67
+ - lib/legion/extensions/rfp/review/helpers/client.rb
68
+ - lib/legion/extensions/rfp/review/runners/approvals.rb
69
+ - lib/legion/extensions/rfp/review/runners/comments.rb
70
+ - lib/legion/extensions/rfp/review/runners/workflows.rb
71
+ - lib/legion/extensions/rfp/version.rb
72
+ homepage: https://github.com/LegionIO/lex-rfp
73
+ licenses:
74
+ - MIT
75
+ metadata:
76
+ homepage_uri: https://github.com/LegionIO/lex-rfp
77
+ source_code_uri: https://github.com/LegionIO/lex-rfp
78
+ documentation_uri: https://github.com/LegionIO/lex-rfp
79
+ changelog_uri: https://github.com/LegionIO/lex-rfp/blob/main/CHANGELOG.md
80
+ bug_tracker_uri: https://github.com/LegionIO/lex-rfp/issues
81
+ rubygems_mfa_required: 'true'
82
+ rdoc_options: []
83
+ require_paths:
84
+ - lib
85
+ required_ruby_version: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '3.4'
90
+ required_rubygems_version: !ruby/object:Gem::Requirement
91
+ requirements:
92
+ - - ">="
93
+ - !ruby/object:Gem::Version
94
+ version: '0'
95
+ requirements: []
96
+ rubygems_version: 3.6.9
97
+ specification_version: 4
98
+ summary: LEX Rfp
99
+ test_files: []