rapitapir 0.1.2 → 2.0.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 (109) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +7 -7
  3. data/.rubocop_todo.yml +83 -0
  4. data/README.md +1319 -235
  5. data/RUBY_WEEKLY_LAUNCH_POST.md +219 -0
  6. data/docs/RAILS_INTEGRATION_IMPLEMENTATION.md +209 -0
  7. data/docs/SINATRA_EXTENSION.md +399 -348
  8. data/docs/STRICT_VALIDATION.md +229 -0
  9. data/docs/VALIDATION_IMPROVEMENTS.md +218 -0
  10. data/docs/ai-integration-plan.md +112 -0
  11. data/docs/auto-derivation.md +505 -92
  12. data/docs/endpoint-definition.md +536 -129
  13. data/docs/n8n-integration.md +212 -0
  14. data/docs/observability.md +810 -500
  15. data/docs/using-mcp.md +93 -0
  16. data/examples/ai/knowledge_base_rag.rb +83 -0
  17. data/examples/ai/user_management_mcp.rb +92 -0
  18. data/examples/ai/user_validation_llm.rb +187 -0
  19. data/examples/rails/RAILS_8_GUIDE.md +165 -0
  20. data/examples/rails/RAILS_LOADING_FIX.rb +35 -0
  21. data/examples/rails/README.md +497 -0
  22. data/examples/rails/comprehensive_test.rb +91 -0
  23. data/examples/rails/config/routes.rb +48 -0
  24. data/examples/rails/debug_controller.rb +63 -0
  25. data/examples/rails/detailed_test.rb +46 -0
  26. data/examples/rails/enhanced_users_controller.rb +278 -0
  27. data/examples/rails/final_server_test.rb +50 -0
  28. data/examples/rails/hello_world_app.rb +116 -0
  29. data/examples/rails/hello_world_controller.rb +186 -0
  30. data/examples/rails/hello_world_routes.rb +28 -0
  31. data/examples/rails/rails8_minimal_demo.rb +132 -0
  32. data/examples/rails/rails8_simple_demo.rb +140 -0
  33. data/examples/rails/rails8_working_demo.rb +255 -0
  34. data/examples/rails/real_world_blog_api.rb +510 -0
  35. data/examples/rails/server_test.rb +46 -0
  36. data/examples/rails/test_direct_processing.rb +41 -0
  37. data/examples/rails/test_hello_world.rb +80 -0
  38. data/examples/rails/test_rails_integration.rb +54 -0
  39. data/examples/rails/traditional_app/Gemfile +37 -0
  40. data/examples/rails/traditional_app/README.md +265 -0
  41. data/examples/rails/traditional_app/app/controllers/api/v1/posts_controller.rb +254 -0
  42. data/examples/rails/traditional_app/app/controllers/api/v1/users_controller.rb +220 -0
  43. data/examples/rails/traditional_app/app/controllers/application_controller.rb +86 -0
  44. data/examples/rails/traditional_app/app/controllers/application_controller_simplified.rb +87 -0
  45. data/examples/rails/traditional_app/app/controllers/documentation_controller.rb +149 -0
  46. data/examples/rails/traditional_app/app/controllers/health_controller.rb +42 -0
  47. data/examples/rails/traditional_app/config/routes.rb +25 -0
  48. data/examples/rails/traditional_app/config/routes_best_practice.rb +25 -0
  49. data/examples/rails/traditional_app/config/routes_simplified.rb +36 -0
  50. data/examples/rails/traditional_app_runnable.rb +406 -0
  51. data/examples/rails/users_controller.rb +4 -1
  52. data/examples/serverless/Gemfile +43 -0
  53. data/examples/serverless/QUICKSTART.md +331 -0
  54. data/examples/serverless/README.md +520 -0
  55. data/examples/serverless/aws_lambda_example.rb +307 -0
  56. data/examples/serverless/aws_sam_template.yaml +215 -0
  57. data/examples/serverless/azure_functions_example.rb +407 -0
  58. data/examples/serverless/deploy.rb +204 -0
  59. data/examples/serverless/gcp_cloud_functions_example.rb +367 -0
  60. data/examples/serverless/gcp_function.yaml +23 -0
  61. data/examples/serverless/host.json +24 -0
  62. data/examples/serverless/package.json +32 -0
  63. data/examples/serverless/spec/aws_lambda_spec.rb +196 -0
  64. data/examples/serverless/spec/spec_helper.rb +89 -0
  65. data/examples/serverless/vercel.json +31 -0
  66. data/examples/serverless/vercel_example.rb +404 -0
  67. data/examples/strict_validation_examples.rb +104 -0
  68. data/examples/validation_error_examples.rb +173 -0
  69. data/lib/rapitapir/ai/llm_instruction.rb +456 -0
  70. data/lib/rapitapir/ai/mcp.rb +134 -0
  71. data/lib/rapitapir/ai/rag.rb +287 -0
  72. data/lib/rapitapir/ai/rag_middleware.rb +147 -0
  73. data/lib/rapitapir/auth/oauth2.rb +43 -57
  74. data/lib/rapitapir/cli/command.rb +362 -2
  75. data/lib/rapitapir/cli/mcp_export.rb +18 -0
  76. data/lib/rapitapir/cli/validator.rb +2 -6
  77. data/lib/rapitapir/core/endpoint.rb +59 -6
  78. data/lib/rapitapir/core/enhanced_endpoint.rb +2 -6
  79. data/lib/rapitapir/dsl/fluent_endpoint_builder.rb +53 -0
  80. data/lib/rapitapir/endpoint_registry.rb +47 -0
  81. data/lib/rapitapir/observability/health_check.rb +4 -4
  82. data/lib/rapitapir/observability/logging.rb +10 -10
  83. data/lib/rapitapir/schema.rb +2 -2
  84. data/lib/rapitapir/server/rack_adapter.rb +1 -3
  85. data/lib/rapitapir/server/rails/configuration.rb +77 -0
  86. data/lib/rapitapir/server/rails/controller_base.rb +185 -0
  87. data/lib/rapitapir/server/rails/documentation_helpers.rb +76 -0
  88. data/lib/rapitapir/server/rails/resource_builder.rb +181 -0
  89. data/lib/rapitapir/server/rails/routes.rb +114 -0
  90. data/lib/rapitapir/server/rails_adapter.rb +10 -3
  91. data/lib/rapitapir/server/rails_adapter_class.rb +1 -3
  92. data/lib/rapitapir/server/rails_controller.rb +1 -3
  93. data/lib/rapitapir/server/rails_integration.rb +67 -0
  94. data/lib/rapitapir/server/rails_response_handler.rb +16 -3
  95. data/lib/rapitapir/server/sinatra_adapter.rb +29 -5
  96. data/lib/rapitapir/server/sinatra_integration.rb +4 -4
  97. data/lib/rapitapir/sinatra/extension.rb +2 -2
  98. data/lib/rapitapir/sinatra/oauth2_helpers.rb +34 -40
  99. data/lib/rapitapir/types/array.rb +4 -0
  100. data/lib/rapitapir/types/auto_derivation.rb +4 -18
  101. data/lib/rapitapir/types/datetime.rb +1 -3
  102. data/lib/rapitapir/types/float.rb +2 -6
  103. data/lib/rapitapir/types/hash.rb +40 -2
  104. data/lib/rapitapir/types/integer.rb +4 -12
  105. data/lib/rapitapir/types/object.rb +6 -2
  106. data/lib/rapitapir/types.rb +6 -2
  107. data/lib/rapitapir/version.rb +1 -1
  108. data/lib/rapitapir.rb +5 -3
  109. metadata +74 -2
@@ -0,0 +1,134 @@
1
+ # frozen_string_literal: true
2
+
3
+ # RapiTapir::AI::MCP
4
+ #
5
+ # Provides Model Context Protocol (MCP) export capabilities for RapiTapir endpoints.
6
+ #
7
+ # Usage:
8
+ # - Use `.mcp_export` in endpoint DSL to mark endpoints for MCP context export.
9
+ # - Use MCPExporter to generate MCP-compatible JSON for all marked endpoints.
10
+
11
+ module RapiTapir
12
+ module AI
13
+ module MCP
14
+ # Collects and serializes endpoint context for LLM/agent consumption
15
+ class Exporter
16
+ def initialize(endpoints)
17
+ @endpoints = endpoints
18
+ end
19
+
20
+ # Returns a hash representing the MCP context for all marked endpoints
21
+ def as_mcp_context
22
+ mcp_endpoints = @endpoints.select(&:mcp_export?)
23
+
24
+ context = {
25
+ service: {
26
+ name: 'RapiTapir API',
27
+ version: '1.0.0',
28
+ description: 'RapiTapir API with exported endpoints for MCP context'
29
+ },
30
+ endpoints: [],
31
+ schemas: extract_all_schemas(mcp_endpoints),
32
+ metadata: {
33
+ generated_at: Time.now.iso8601,
34
+ generator: 'RapiTapir MCP Exporter',
35
+ mcp_version: '1.0'
36
+ }
37
+ }
38
+
39
+ context[:endpoints] = mcp_endpoints.map do |ep|
40
+ {
41
+ name: endpoint_name(ep),
42
+ method: ep.method&.to_s&.upcase,
43
+ path: ep.path,
44
+ summary: ep.metadata[:summary],
45
+ description: ep.metadata[:description],
46
+ input_schema: extract_input_schema(ep),
47
+ output_schema: extract_output_schema(ep),
48
+ examples: ep.metadata[:examples] || []
49
+ }
50
+ end
51
+
52
+ context
53
+ end
54
+
55
+ # Test-compatible methods (aliases and wrappers)
56
+ def export_context
57
+ as_mcp_context
58
+ end
59
+
60
+ def export_json(pretty: true)
61
+ context = as_mcp_context
62
+ if pretty
63
+ JSON.pretty_generate(context)
64
+ else
65
+ JSON.generate(context)
66
+ end
67
+ end
68
+
69
+ def mcp_endpoints
70
+ @endpoints.select(&:mcp_export?)
71
+ end
72
+
73
+ private
74
+
75
+ def endpoint_name(endpoint)
76
+ # Generate a readable name from method and path
77
+ method = endpoint.method&.to_s || 'unknown'
78
+ path = endpoint.path&.gsub(%r{[{}/]}, '_')&.gsub(/_+/, '_')&.strip || 'unknown'
79
+ "#{method}_#{path}".downcase
80
+ end
81
+
82
+ def extract_input_schema(endpoint)
83
+ return {} unless endpoint.inputs
84
+
85
+ schema = {}
86
+ endpoint.inputs.each do |input|
87
+ next unless input.respond_to?(:name)
88
+
89
+ # Handle both old and new input structures
90
+ required = if input.respond_to?(:required?)
91
+ input.required?
92
+ elsif input.respond_to?(:options) && input.options
93
+ input.options[:required] != false
94
+ else
95
+ true # default to required
96
+ end
97
+
98
+ schema[input.name] = {
99
+ type: input.type,
100
+ kind: input.kind,
101
+ required: required
102
+ }
103
+ end
104
+ schema
105
+ end
106
+
107
+ def extract_output_schema(endpoint)
108
+ return {} unless endpoint.outputs
109
+
110
+ schema = {}
111
+ endpoint.outputs.each do |output|
112
+ schema[output.kind] = {
113
+ type: output.type
114
+ }
115
+ end
116
+ schema
117
+ end
118
+
119
+ def extract_all_schemas(endpoints)
120
+ schemas = {}
121
+ endpoints.each do |endpoint|
122
+ input_schema = extract_input_schema(endpoint)
123
+ output_schema = extract_output_schema(endpoint)
124
+
125
+ schemas["#{endpoint_name(endpoint)}_input"] = input_schema unless input_schema.empty?
126
+
127
+ schemas["#{endpoint_name(endpoint)}_output"] = output_schema unless output_schema.empty?
128
+ end
129
+ schemas
130
+ end
131
+ end
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,287 @@
1
+ # frozen_string_literal: true
2
+
3
+ # RapiTapir::AI::RAG
4
+ #
5
+ # Provides Retrieval-Augmented Generation (RAG) pipeline support for RapiTapir endpoints.
6
+ #
7
+ # Usage:
8
+ # - Use `.rag_inference(llm:, retrieval:, context_fields:)` in endpoint DSL
9
+ # - Configure LLM and retrieval backends
10
+ # - Process user queries with retrieved context
11
+
12
+ module RapiTapir
13
+ module AI
14
+ module RAG
15
+ # Base class for LLM providers
16
+ class LLMProvider
17
+ def initialize(config = {})
18
+ @config = config
19
+ end
20
+
21
+ def generate(prompt, context = {})
22
+ raise NotImplementedError, 'Subclasses must implement #generate'
23
+ end
24
+ end
25
+
26
+ # OpenAI LLM provider
27
+ class OpenAIProvider < LLMProvider
28
+ def initialize(config = {})
29
+ super
30
+ @api_key = config[:api_key] || ENV.fetch('OPENAI_API_KEY', nil)
31
+ @model = config[:model] || 'gpt-3.5-turbo'
32
+ @base_url = config[:base_url] || 'https://api.openai.com/v1'
33
+ end
34
+
35
+ def generate(prompt, context = {})
36
+ # Mock implementation for now - replace with actual OpenAI API call
37
+ if @api_key && @api_key != 'mock'
38
+ make_openai_request(prompt, context)
39
+ else
40
+ mock_response(prompt, context)
41
+ end
42
+ end
43
+
44
+ private
45
+
46
+ def make_openai_request(prompt, context)
47
+ # This would make actual HTTP request to OpenAI
48
+ # For now, return a mock response
49
+ mock_response(prompt, context)
50
+ end
51
+
52
+ def mock_response(prompt, context)
53
+ "AI Response: Based on the query '#{prompt}' and context #{context.keys.join(', ')}, here is a generated response."
54
+ end
55
+ end
56
+
57
+ # Base class for retrieval backends
58
+ class RetrievalBackend
59
+ def initialize(config = {})
60
+ @config = config
61
+ end
62
+
63
+ def retrieve(query, context_fields = [])
64
+ raise NotImplementedError, 'Subclasses must implement #retrieve'
65
+ end
66
+ end
67
+
68
+ # PostgreSQL retrieval backend
69
+ class PostgresBackend < RetrievalBackend
70
+ def initialize(config = {})
71
+ super
72
+ @connection_config = config[:connection] || {}
73
+ @table = config[:table] || 'documents'
74
+ @search_column = config[:search_column] || 'content'
75
+ end
76
+
77
+ def retrieve(query, context_fields = [])
78
+ # Mock implementation - replace with actual DB query
79
+ mock_retrieval(query, context_fields)
80
+ end
81
+
82
+ private
83
+
84
+ def mock_retrieval(query, context_fields)
85
+ [
86
+ {
87
+ content: "Sample document content related to: #{query}",
88
+ metadata: context_fields.to_h { |field| [field, "sample_#{field}_value"] },
89
+ score: 0.85
90
+ },
91
+ {
92
+ content: "Another relevant document for: #{query}",
93
+ metadata: context_fields.to_h { |field| [field, "another_#{field}_value"] },
94
+ score: 0.72
95
+ }
96
+ ]
97
+ end
98
+ end
99
+
100
+ # Memory/hash-based retrieval backend for testing
101
+ class MemoryBackend < RetrievalBackend
102
+ def initialize(config = {})
103
+ super
104
+ @documents = config[:documents] || []
105
+ end
106
+
107
+ def retrieve(query, _context_fields = [])
108
+ # Simple text matching for demo purposes
109
+ matching_docs = @documents.select do |doc|
110
+ doc[:content]&.downcase&.include?(query.downcase)
111
+ end
112
+
113
+ matching_docs.map.with_index do |doc, index|
114
+ {
115
+ content: doc[:content],
116
+ metadata: doc[:metadata] || {},
117
+ score: 1.0 - (index * 0.1) # Simple scoring
118
+ }
119
+ end
120
+ end
121
+ end
122
+
123
+ # RAG Pipeline orchestrator
124
+ class Pipeline
125
+ attr_reader :llm_provider, :retrieval_backend
126
+
127
+ def initialize(llm:, retrieval:, config: {})
128
+ @llm_provider = create_llm_provider(llm, config[:llm] || {})
129
+ @retrieval_backend = create_retrieval_backend(retrieval, config[:retrieval] || {})
130
+ @config = config
131
+ end
132
+
133
+ def process(query, context_fields: [], user_context: {})
134
+ # Step 1: Retrieve relevant documents
135
+ retrieved_docs = @retrieval_backend.retrieve(query, context_fields)
136
+
137
+ # Step 2: Build context for LLM
138
+ llm_context = build_llm_context(retrieved_docs, user_context, context_fields)
139
+
140
+ # Step 3: Generate response using LLM
141
+ prompt = build_prompt(query, llm_context)
142
+ response = @llm_provider.generate(prompt, llm_context)
143
+
144
+ # Step 4: Return structured result
145
+ {
146
+ answer: response,
147
+ sources: retrieved_docs,
148
+ context: llm_context,
149
+ query: query
150
+ }
151
+ end
152
+
153
+ private
154
+
155
+ def create_llm_provider(type, config)
156
+ case type.to_sym
157
+ when :openai
158
+ OpenAIProvider.new(config)
159
+ else
160
+ raise ArgumentError, "Unknown LLM provider: #{type}"
161
+ end
162
+ end
163
+
164
+ def create_retrieval_backend(type, config)
165
+ case type.to_sym
166
+ when :postgres, :postgresql
167
+ PostgresBackend.new(config)
168
+ when :memory
169
+ MemoryBackend.new(config)
170
+ else
171
+ raise ArgumentError, "Unknown retrieval backend: #{type}"
172
+ end
173
+ end
174
+
175
+ def build_llm_context(retrieved_docs, user_context, context_fields)
176
+ {
177
+ retrieved_documents: retrieved_docs,
178
+ user_context: user_context,
179
+ context_fields: context_fields,
180
+ document_count: retrieved_docs.length
181
+ }
182
+ end
183
+
184
+ def build_prompt(query, context)
185
+ documents_text = context[:retrieved_documents]
186
+ .map { |doc| doc[:content] }
187
+ .join("\n\n")
188
+
189
+ <<~PROMPT
190
+ You are an AI assistant that answers questions based on the provided context.
191
+
192
+ Context Documents:
193
+ #{documents_text}
194
+
195
+ User Question: #{query}
196
+
197
+ Please provide a helpful and accurate answer based on the context provided.
198
+ If the context doesn't contain enough information, say so clearly.
199
+ PROMPT
200
+ end
201
+ end
202
+
203
+ # Rack middleware for handling RAG inference requests
204
+ class Middleware
205
+ def initialize(app)
206
+ @app = app
207
+ end
208
+
209
+ def call(env)
210
+ endpoint = env['rapitapir.endpoint']
211
+
212
+ # Pass through if not a RAG endpoint
213
+ return @app.call(env) unless endpoint&.rag_inference?
214
+
215
+ # Process RAG request
216
+ process_rag_request(env, endpoint)
217
+ end
218
+
219
+ private
220
+
221
+ def process_rag_request(env, endpoint)
222
+ # Parse request body
223
+ request_body = env['rack.input']&.read
224
+ env['rack.input']&.rewind if env['rack.input'].respond_to?(:rewind)
225
+
226
+ query_data = request_body ? JSON.parse(request_body) : {}
227
+ question = query_data['question'] || query_data[:question]
228
+
229
+ # Get RAG config from endpoint
230
+ rag_config = endpoint.metadata[:rag_inference]
231
+
232
+ # Create RAG pipeline
233
+ pipeline = Pipeline.new(
234
+ llm: rag_config[:llm],
235
+ retrieval: rag_config[:retrieval],
236
+ config: rag_config[:config] || {}
237
+ )
238
+
239
+ # Process the query
240
+ result = pipeline.process(
241
+ question,
242
+ context_fields: rag_config[:context_fields] || [],
243
+ user_context: extract_user_context(env)
244
+ )
245
+
246
+ # Return JSON response
247
+ response_body = JSON.generate(
248
+ answer: result[:answer],
249
+ sources: result[:sources],
250
+ metadata: {
251
+ query: result[:query],
252
+ context_fields: result[:context][:context_fields],
253
+ document_count: result[:context][:document_count]
254
+ }
255
+ )
256
+
257
+ [
258
+ 200,
259
+ { 'Content-Type' => 'application/json' },
260
+ [response_body]
261
+ ]
262
+ rescue StandardError => e
263
+ error_response = JSON.generate(
264
+ error: 'RAG processing failed',
265
+ message: e.message
266
+ )
267
+
268
+ [
269
+ 500,
270
+ { 'Content-Type' => 'application/json' },
271
+ [error_response]
272
+ ]
273
+ end
274
+
275
+ def extract_user_context(env)
276
+ # Extract relevant context from request environment
277
+ {
278
+ user_agent: env['HTTP_USER_AGENT'],
279
+ remote_ip: env['REMOTE_ADDR'],
280
+ method: env['REQUEST_METHOD'],
281
+ path: env['PATH_INFO']
282
+ }
283
+ end
284
+ end
285
+ end
286
+ end
287
+ end
@@ -0,0 +1,147 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'rag'
4
+
5
+ module RapiTapir
6
+ module AI
7
+ module RAG
8
+ # Middleware for handling RAG-enabled endpoints
9
+ # Integrates with RapiTapir server adapters to process RAG requests
10
+ class Middleware
11
+ def initialize(app)
12
+ @app = app
13
+ end
14
+
15
+ def call(env)
16
+ # Check if this is a RAG-enabled endpoint
17
+ endpoint = env['rapitapir.endpoint']
18
+ if endpoint&.rag_inference?
19
+ handle_rag_request(env, endpoint)
20
+ else
21
+ @app.call(env)
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def handle_rag_request(env, endpoint)
28
+ # Extract request data
29
+ request_data = extract_request_data(env)
30
+
31
+ # Get the query from the request (assuming it's in the body)
32
+ query = request_data[:question] || request_data[:query] || request_data.values.first
33
+
34
+ # Create RAG pipeline from endpoint configuration
35
+ rag_config = endpoint.rag_config
36
+ pipeline = create_rag_pipeline(rag_config)
37
+
38
+ # Process the query through RAG pipeline
39
+ result = pipeline.process(
40
+ query,
41
+ context_fields: rag_config[:context_fields],
42
+ user_context: extract_user_context(env, rag_config[:context_fields])
43
+ )
44
+
45
+ # Return RAG response
46
+ build_rag_response(result)
47
+ rescue StandardError => e
48
+ build_error_response(e)
49
+ end
50
+
51
+ def extract_request_data(env)
52
+ # Simple JSON parsing - in real implementation would be more robust
53
+ if env['CONTENT_TYPE']&.include?('application/json')
54
+ body = env['rack.input']&.read || '{}'
55
+ env['rack.input']&.rewind
56
+ JSON.parse(body, symbolize_names: true)
57
+ else
58
+ {}
59
+ end
60
+ rescue JSON::ParserError
61
+ {}
62
+ end
63
+
64
+ def extract_user_context(env, context_fields)
65
+ # Extract user context from headers, session, etc.
66
+ context = {}
67
+
68
+ context_fields.each do |field|
69
+ case field
70
+ when :user_id
71
+ context[:user_id] = env['HTTP_X_USER_ID'] || env['HTTP_AUTHORIZATION']&.split&.last
72
+ when :session_id
73
+ context[:session_id] = env['HTTP_X_SESSION_ID']
74
+ when :tenant_id
75
+ context[:tenant_id] = env['HTTP_X_TENANT_ID']
76
+ end
77
+ end
78
+
79
+ context
80
+ end
81
+
82
+ def create_rag_pipeline(config)
83
+ Pipeline.new(
84
+ llm: config[:llm],
85
+ retrieval: config[:retrieval],
86
+ config: config[:config] || {}
87
+ )
88
+ end
89
+
90
+ def build_rag_response(result)
91
+ response_body = {
92
+ answer: result[:answer],
93
+ sources: result[:sources],
94
+ metadata: {
95
+ query: result[:query],
96
+ source_count: result[:sources].length,
97
+ timestamp: Time.now.iso8601
98
+ }
99
+ }
100
+
101
+ [
102
+ 200,
103
+ { 'Content-Type' => 'application/json' },
104
+ [JSON.generate(response_body)]
105
+ ]
106
+ end
107
+
108
+ def build_error_response(error)
109
+ response_body = {
110
+ error: 'RAG processing failed',
111
+ message: error.message,
112
+ timestamp: Time.now.iso8601
113
+ }
114
+
115
+ [
116
+ 500,
117
+ { 'Content-Type' => 'application/json' },
118
+ [JSON.generate(response_body)]
119
+ ]
120
+ end
121
+ end
122
+
123
+ # Helper for mounting RAG endpoints in server adapters
124
+ class EndpointHandler
125
+ def self.handle(endpoint, request_data, user_context = {})
126
+ return nil unless endpoint.rag_inference?
127
+
128
+ rag_config = endpoint.rag_config
129
+ pipeline = Pipeline.new(
130
+ llm: rag_config[:llm],
131
+ retrieval: rag_config[:retrieval],
132
+ config: rag_config[:config] || {}
133
+ )
134
+
135
+ # Extract query from request data
136
+ query = request_data[:question] || request_data[:query] || request_data.to_s
137
+
138
+ pipeline.process(
139
+ query,
140
+ context_fields: rag_config[:context_fields],
141
+ user_context: user_context
142
+ )
143
+ end
144
+ end
145
+ end
146
+ end
147
+ end