mirror of
https://github.com/we-promise/sure.git
synced 2026-04-09 15:24:48 +00:00
* Document admin-only reset auth in OpenAPI docs The DELETE /api/v1/users/reset endpoint now requires admin role (ensure_admin). Update the rswag spec to: - Set default user role to admin so the 200 test passes - Add a 403 response case for non-admin users with read_write scope - Clarify the description notes admin requirement - Add SuccessMessage schema and users paths to openapi.yaml https://claude.ai/code/session_01Tj8ToLRmVg5HLmHwq9KKDY * Consolidate duplicate 403 responses for reset endpoint OpenAPI keys responses by status code, so two 403 blocks caused the first (insufficient scope) to be silently overwritten by the second (non-admin). Merge into a single 403 whose description covers both causes: requires read_write scope and admin role. The test exercises the read-only key path which hits 403 via scope check. https://claude.ai/code/session_01Tj8ToLRmVg5HLmHwq9KKDY * Em-dash out of messages. * Fix tests * Fix tests --------- Co-authored-by: Claude <noreply@anthropic.com>
111 lines
3.4 KiB
Ruby
111 lines
3.4 KiB
Ruby
class Assistant::External < Assistant::Base
|
|
Config = Struct.new(:url, :token, :agent_id, :session_key, keyword_init: true)
|
|
MAX_CONVERSATION_MESSAGES = 20
|
|
|
|
class << self
|
|
def for_chat(chat)
|
|
new(chat)
|
|
end
|
|
|
|
def configured?
|
|
config.url.present? && config.token.present?
|
|
end
|
|
|
|
def available_for?(user)
|
|
configured? && allowed_user?(user)
|
|
end
|
|
|
|
def allowed_user?(user)
|
|
allowed = ENV["EXTERNAL_ASSISTANT_ALLOWED_EMAILS"]
|
|
return true if allowed.blank?
|
|
return false if user&.email.blank?
|
|
|
|
allowed.split(",").map { |e| e.strip.downcase }.include?(user.email.downcase)
|
|
end
|
|
|
|
def config
|
|
Config.new(
|
|
url: ENV["EXTERNAL_ASSISTANT_URL"].presence || Setting.external_assistant_url.presence,
|
|
token: ENV["EXTERNAL_ASSISTANT_TOKEN"].presence || Setting.external_assistant_token.presence,
|
|
agent_id: ENV["EXTERNAL_ASSISTANT_AGENT_ID"].presence || Setting.external_assistant_agent_id.presence || "main",
|
|
session_key: ENV.fetch("EXTERNAL_ASSISTANT_SESSION_KEY", "agent:main:main")
|
|
)
|
|
end
|
|
end
|
|
|
|
def respond_to(message)
|
|
response_completed = false
|
|
|
|
unless self.class.configured?
|
|
raise Assistant::Error,
|
|
"External assistant is not configured. Set the URL and token in Settings > Self-Hosting or via environment variables."
|
|
end
|
|
|
|
unless self.class.allowed_user?(chat.user)
|
|
raise Assistant::Error, "Your account is not authorized to use the external assistant."
|
|
end
|
|
|
|
assistant_message = AssistantMessage.new(
|
|
chat: chat,
|
|
content: "",
|
|
ai_model: "external-agent"
|
|
)
|
|
|
|
client = build_client
|
|
messages = build_conversation_messages
|
|
|
|
model = client.chat(
|
|
messages: messages,
|
|
user: "sure-family-#{chat.user.family_id}"
|
|
) do |text|
|
|
if assistant_message.content.blank?
|
|
stop_thinking
|
|
assistant_message.content = text
|
|
assistant_message.save!
|
|
else
|
|
assistant_message.append_text!(text)
|
|
end
|
|
end
|
|
|
|
if assistant_message.new_record?
|
|
stop_thinking
|
|
raise Assistant::Error, "External assistant returned an empty response."
|
|
end
|
|
|
|
response_completed = true
|
|
assistant_message.update!(ai_model: model) if model.present?
|
|
rescue Assistant::Error, ActiveRecord::ActiveRecordError => e
|
|
cleanup_partial_response(assistant_message) unless response_completed
|
|
stop_thinking
|
|
chat.add_error(e)
|
|
rescue => e
|
|
Rails.logger.error("[Assistant::External] Unexpected error: #{e.class} - #{e.message}")
|
|
cleanup_partial_response(assistant_message) unless response_completed
|
|
stop_thinking
|
|
chat.add_error(Assistant::Error.new("Something went wrong with the external assistant. Check server logs for details."))
|
|
end
|
|
|
|
private
|
|
|
|
def cleanup_partial_response(assistant_message)
|
|
assistant_message&.destroy! if assistant_message&.persisted?
|
|
rescue ActiveRecord::ActiveRecordError => e
|
|
Rails.logger.warn("[Assistant::External] Failed to clean up partial response: #{e.message}")
|
|
end
|
|
|
|
def build_client
|
|
Assistant::External::Client.new(
|
|
url: self.class.config.url,
|
|
token: self.class.config.token,
|
|
agent_id: self.class.config.agent_id,
|
|
session_key: self.class.config.session_key
|
|
)
|
|
end
|
|
|
|
def build_conversation_messages
|
|
chat.conversation_messages.ordered.last(MAX_CONVERSATION_MESSAGES).map do |msg|
|
|
{ role: msg.role, content: msg.content }
|
|
end
|
|
end
|
|
end
|