collavre_completion_api 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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: d657f80e16c1705fa79850aa8445aa53d90112de021c9640221f71b15d73f71e
4
+ data.tar.gz: 4cef77f5d3433dcb90d9c1a1c82c97a8b9c0d56ec727f61e73fab68a3fa68de1
5
+ SHA512:
6
+ metadata.gz: 4a4d8cbd25e4f17e953085529b075b590e4dd390c1eb2d24cce0bd5a09d4c72ba25434a70b4a85e022a2e2ac8792a2c36adca9efdf657f6ec9565674836ef2a4
7
+ data.tar.gz: adc77be5cc324e467f24438d3677777259a4474fd44a151c4f7ea7642fe3b65cfaec60dbe41069104802b843ccfa521b1d48625a3c926f770ff0206f4bf2fabb
data/README.md ADDED
@@ -0,0 +1,41 @@
1
+ # CollavreCompletionApi
2
+
3
+ OpenAI-compatible chat completions API for Collavre.
4
+
5
+ ## Endpoints
6
+
7
+ - `POST /api/v1/chat/completions` — Chat completion (streaming + non-streaming)
8
+ - `GET /api/v1/models` — List accessible AI agents as models
9
+
10
+ ## Authentication
11
+
12
+ Uses Doorkeeper OAuth Bearer tokens. Pass your OAuth access token as:
13
+
14
+ ```
15
+ Authorization: Bearer <oauth_token>
16
+ ```
17
+
18
+ Compatible with OpenAI SDK:
19
+
20
+ ```python
21
+ from openai import OpenAI
22
+ client = OpenAI(
23
+ base_url="https://your-collavre.com/api/v1",
24
+ api_key="<oauth_access_token>"
25
+ )
26
+ ```
27
+
28
+ ## Context Injection
29
+
30
+ Pass optional headers to inject Collavre context into AI prompts:
31
+
32
+ - `X-Collavre-Creative: <creative_id>` — Include creative context
33
+ - `X-Collavre-Topic: <topic_id>` — Filter context to specific topic
34
+
35
+ ## Installation
36
+
37
+ Add to your host app's `Gemfile`:
38
+
39
+ ```ruby
40
+ gem "collavre_completion_api", path: "engines/collavre_completion_api"
41
+ ```
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require "bundler/setup"
2
+ require "bundler/gem_tasks"
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CollavreCompletionApi
4
+ module Api
5
+ module V1
6
+ class BaseController < ActionController::API
7
+ before_action :authenticate!
8
+
9
+ private
10
+
11
+ def authenticate!
12
+ token = extract_bearer_token
13
+ if token.blank?
14
+ render json: { error: { message: "Missing authentication token", type: "invalid_request_error",
15
+ code: "missing_token" } },
16
+ status: :unauthorized
17
+ return
18
+ end
19
+
20
+ unless authenticate_oauth(token)
21
+ render json: { error: { message: "Invalid authentication token", type: "invalid_request_error",
22
+ code: "invalid_token" } },
23
+ status: :unauthorized
24
+ nil
25
+ end
26
+ end
27
+
28
+ def authenticate_oauth(token)
29
+ access_token = Doorkeeper::AccessToken.by_token(token)
30
+ return false unless access_token&.accessible?
31
+
32
+ user = Collavre::User.find_by(id: access_token.resource_owner_id)
33
+ return false unless user
34
+
35
+ Collavre::Current.user = user
36
+ true
37
+ end
38
+
39
+ def extract_bearer_token
40
+ auth_header = request.headers["Authorization"]
41
+ return nil unless auth_header&.start_with?("Bearer ")
42
+
43
+ auth_header.sub("Bearer ", "")
44
+ end
45
+
46
+ def current_user
47
+ Collavre::Current.user
48
+ end
49
+
50
+ def collavre_creative
51
+ @collavre_creative ||= begin
52
+ creative_id = request.headers["X-Collavre-Creative"]
53
+ return nil if creative_id.blank?
54
+
55
+ creative = Collavre::Creative.find_by(id: creative_id)&.effective_origin
56
+ return nil unless creative&.has_permission?(current_user, :member)
57
+
58
+ creative
59
+ end
60
+ end
61
+
62
+ def collavre_topic
63
+ @collavre_topic ||= begin
64
+ topic_id = request.headers["X-Collavre-Topic"]
65
+ return nil if topic_id.blank?
66
+ return nil unless collavre_creative
67
+
68
+ collavre_creative.topics.find_by(id: topic_id)
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,214 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CollavreCompletionApi
4
+ module Api
5
+ module V1
6
+ module Chat
7
+ class CompletionsController < BaseController
8
+ CREATIVE_CONTEXT_COMMENT_LIMIT = 20
9
+
10
+ def create
11
+ messages = params[:messages] || []
12
+ stream = params[:stream] == true || params[:stream] == "true"
13
+
14
+ if messages.empty?
15
+ render json: { error: { message: "messages is required", type: "invalid_request_error" } },
16
+ status: :bad_request
17
+ return
18
+ end
19
+
20
+ agent = resolve_agent
21
+ unless agent
22
+ render json: { error: { message: "Invalid model", type: "invalid_request_error" } },
23
+ status: :bad_request
24
+ return
25
+ end
26
+
27
+ system_prompt = build_system_prompt(messages, agent)
28
+ contents = build_contents(messages)
29
+
30
+ # Use agent owner's llm_api_key for external LLM calls
31
+ api_key = agent.respond_to?(:llm_api_key) ? agent.llm_api_key : nil
32
+ api_key ||= agent.respond_to?(:creator) ? agent.creator&.llm_api_key : nil
33
+
34
+ client = Collavre::AiClient.new(
35
+ vendor: agent.llm_vendor,
36
+ model: agent.llm_model,
37
+ system_prompt: system_prompt,
38
+ llm_api_key: api_key,
39
+ context: { creative: collavre_creative, user: current_user }
40
+ )
41
+
42
+ model_name = params[:model] || "collavre/#{agent.id}"
43
+
44
+ if stream
45
+ stream_response(client, contents, model_name)
46
+ else
47
+ non_stream_response(client, contents, model_name)
48
+ end
49
+ end
50
+
51
+ private
52
+
53
+ def resolve_agent
54
+ model_param = params[:model].to_s
55
+
56
+ return nil unless model_param.start_with?("collavre/")
57
+
58
+ ai_id = model_param.sub("collavre/", "")
59
+ agent = Collavre::User.find_by(id: ai_id)
60
+ return nil unless agent&.ai_user?
61
+ return nil unless agent_accessible?(agent)
62
+
63
+ agent
64
+ end
65
+
66
+ def agent_accessible?(agent)
67
+ agent.created_by_id == current_user.id || agent.searchable?
68
+ end
69
+
70
+ def build_system_prompt(messages, agent)
71
+ parts = []
72
+
73
+ parts << agent.system_prompt if agent.system_prompt.present?
74
+
75
+ system_messages = messages.select { |m| m[:role] == "system" || m["role"] == "system" }
76
+ system_messages.each do |msg|
77
+ parts << (msg[:content] || msg["content"])
78
+ end
79
+
80
+ parts << build_creative_context if collavre_creative
81
+
82
+ parts.compact.join("\n\n").presence || Collavre::AiClient::SYSTEM_INSTRUCTIONS
83
+ end
84
+
85
+ def build_creative_context
86
+ context_parts = []
87
+ context_parts << "## Collavre Context"
88
+ context_parts << "Creative: #{collavre_creative.title}" if collavre_creative.title.present?
89
+
90
+ if collavre_topic
91
+ context_parts << "Topic: #{collavre_topic.title}" if collavre_topic.title.present?
92
+ end
93
+
94
+ scope = collavre_creative.comments
95
+ .where(private: false)
96
+ .order(created_at: :desc)
97
+ scope = scope.where(topic_id: collavre_topic.id) if collavre_topic
98
+ recent_comments = scope.limit(CREATIVE_CONTEXT_COMMENT_LIMIT)
99
+ .includes(:user).to_a.reverse
100
+
101
+ if recent_comments.any?
102
+ context_parts << "\n### Recent Discussion"
103
+ recent_comments.each do |comment|
104
+ author = comment.user&.name || "Unknown"
105
+ context_parts << "- **#{author}**: #{comment.content.to_s.truncate(500)}"
106
+ end
107
+ end
108
+
109
+ context_parts.join("\n")
110
+ end
111
+
112
+ def build_contents(messages)
113
+ messages.reject { |m|
114
+ role = m[:role] || m["role"]
115
+ role == "system"
116
+ }.map { |m|
117
+ role = m[:role] || m["role"]
118
+ content = m[:content] || m["content"]
119
+ { role: role, text: content }
120
+ }
121
+ end
122
+
123
+ def non_stream_response(client, contents, model)
124
+ result = client.chat(contents)
125
+ completion_id = "chatcmpl-#{SecureRandom.hex(12)}"
126
+
127
+ prompt_tokens = client.last_input_tokens
128
+ completion_tokens = client.last_output_tokens
129
+
130
+ render json: {
131
+ id: completion_id,
132
+ object: "chat.completion",
133
+ created: Time.current.to_i,
134
+ model: model,
135
+ choices: [
136
+ {
137
+ index: 0,
138
+ message: { role: "assistant", content: result || "" },
139
+ finish_reason: "stop"
140
+ }
141
+ ],
142
+ usage: {
143
+ prompt_tokens: prompt_tokens,
144
+ completion_tokens: completion_tokens,
145
+ total_tokens: prompt_tokens + completion_tokens
146
+ }
147
+ }
148
+ end
149
+
150
+ def stream_response(client, contents, model)
151
+ completion_id = "chatcmpl-#{SecureRandom.hex(12)}"
152
+
153
+ response.headers["Content-Type"] = "text/event-stream"
154
+ response.headers["Cache-Control"] = "no-cache"
155
+ response.headers["Connection"] = "keep-alive"
156
+
157
+ self.response_body = Enumerator.new do |yielder|
158
+ begin
159
+ client.chat(contents) do |chunk|
160
+ data = {
161
+ id: completion_id,
162
+ object: "chat.completion.chunk",
163
+ created: Time.current.to_i,
164
+ model: model,
165
+ choices: [
166
+ {
167
+ index: 0,
168
+ delta: { content: chunk },
169
+ finish_reason: nil
170
+ }
171
+ ]
172
+ }
173
+ yielder << "data: #{data.to_json}\n\n"
174
+ end
175
+
176
+ final = {
177
+ id: completion_id,
178
+ object: "chat.completion.chunk",
179
+ created: Time.current.to_i,
180
+ model: model,
181
+ choices: [
182
+ {
183
+ index: 0,
184
+ delta: {},
185
+ finish_reason: "stop"
186
+ }
187
+ ]
188
+ }
189
+ yielder << "data: #{final.to_json}\n\n"
190
+ rescue StandardError => e
191
+ error_data = {
192
+ id: completion_id,
193
+ object: "chat.completion.chunk",
194
+ created: Time.current.to_i,
195
+ model: model,
196
+ choices: [
197
+ {
198
+ index: 0,
199
+ delta: { content: "\n\n⚠️ Error: #{e.message}" },
200
+ finish_reason: "stop"
201
+ }
202
+ ]
203
+ }
204
+ yielder << "data: #{error_data.to_json}\n\n"
205
+ ensure
206
+ yielder << "data: [DONE]\n\n"
207
+ end
208
+ end
209
+ end
210
+ end
211
+ end
212
+ end
213
+ end
214
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CollavreCompletionApi
4
+ module Api
5
+ module V1
6
+ class ModelsController < BaseController
7
+ def index
8
+ ai_agents = Collavre::User.accessible_ai_agents_for(current_user)
9
+
10
+ models = ai_agents.map do |agent|
11
+ {
12
+ id: "collavre/#{agent.id}",
13
+ object: "model",
14
+ created: agent.created_at.to_i,
15
+ owned_by: "collavre",
16
+ meta: {
17
+ name: agent.name,
18
+ llm_vendor: agent.llm_vendor,
19
+ llm_model: agent.llm_model
20
+ }
21
+ }
22
+ end
23
+
24
+ render json: { object: "list", data: models }
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
data/config/routes.rb ADDED
@@ -0,0 +1,10 @@
1
+ CollavreCompletionApi::Engine.routes.draw do
2
+ namespace :api do
3
+ namespace :v1 do
4
+ resources :models, only: [ :index ]
5
+ namespace :chat do
6
+ resources :completions, only: [ :create ]
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CollavreCompletionApi
4
+ class Engine < ::Rails::Engine
5
+ isolate_namespace CollavreCompletionApi
6
+
7
+ config.generators do |g|
8
+ g.test_framework :minitest
9
+ end
10
+
11
+ # Auto-mount engine routes under the Collavre engine
12
+ initializer "collavre_completion_api.routes", before: :add_routing_paths do |app|
13
+ app.routes.append do
14
+ mount CollavreCompletionApi::Engine => "/", as: :collavre_completion_api_engine
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CollavreCompletionApi
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "collavre_completion_api/version"
4
+ require "collavre_completion_api/engine"
5
+
6
+ module CollavreCompletionApi
7
+ end
metadata ADDED
@@ -0,0 +1,94 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: collavre_completion_api
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Collavre
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: rails
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '8.0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '8.0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: collavre
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: doorkeeper
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ description: Plugin engine providing OpenAI-compatible /v1/chat/completions and /v1/models
55
+ endpoints using Collavre AI agents with context injection.
56
+ email:
57
+ - support@collavre.com
58
+ executables: []
59
+ extensions: []
60
+ extra_rdoc_files: []
61
+ files:
62
+ - README.md
63
+ - Rakefile
64
+ - app/controllers/collavre_completion_api/api/v1/base_controller.rb
65
+ - app/controllers/collavre_completion_api/api/v1/chat/completions_controller.rb
66
+ - app/controllers/collavre_completion_api/api/v1/models_controller.rb
67
+ - config/routes.rb
68
+ - lib/collavre_completion_api.rb
69
+ - lib/collavre_completion_api/engine.rb
70
+ - lib/collavre_completion_api/version.rb
71
+ homepage: https://collavre.com
72
+ licenses:
73
+ - AGPL
74
+ metadata:
75
+ homepage_uri: https://collavre.com
76
+ source_code_uri: https://github.com/sh1nj1/plan42
77
+ rdoc_options: []
78
+ require_paths:
79
+ - lib
80
+ required_ruby_version: !ruby/object:Gem::Requirement
81
+ requirements:
82
+ - - ">="
83
+ - !ruby/object:Gem::Version
84
+ version: '0'
85
+ required_rubygems_version: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ requirements: []
91
+ rubygems_version: 3.6.7
92
+ specification_version: 4
93
+ summary: OpenAI-compatible chat completions API for Collavre
94
+ test_files: []