| 1 | # frozen_string_literal: true |
| 2 | |
| 3 | module Api |
| 4 | module V1 |
| 5 | # siGit Code Cloud Sessions (Cloud Session): named, resumable conversations |
| 6 | # persisted server-side so they sync across the user's signed-in surfaces. |
| 7 | # Token-based, scoped to current_user; a user can only see and mutate their |
| 8 | # own sessions. |
| 9 | # |
| 10 | # GET /api/v1/sessions → list (most recent first) |
| 11 | # POST /api/v1/sessions → create { title?, model? } |
| 12 | # GET /api/v1/sessions/:id → session + messages |
| 13 | # PATCH /api/v1/sessions/:id → update { title?, model? } |
| 14 | # DELETE /api/v1/sessions/:id → 204 |
| 15 | # POST /api/v1/sessions/:id/messages → append { role, content } |
| 16 | # |
| 17 | # Streaming inference is unchanged: the client still calls |
| 18 | # /api/v1/chat/completions. These endpoints only persist the transcript, so a |
| 19 | # session can be resumed later. Mapped at path "sessions" but kept a distinct |
| 20 | # controller from the auth SessionsController (sign_in/sign_out). |
| 21 | class CloudSessionsController < Api::BaseController |
| 22 | before_action :authenticate_token! |
| 23 | before_action :set_session, only: %i[show update destroy create_message] |
| 24 | |
| 25 | def index |
| 26 | sessions = current_user.cloud_sessions.order(updated_at: :desc) |
| 27 | render json: sessions.map { |s| session_json(s) }, status: :ok |
| 28 | end |
| 29 | |
| 30 | def create |
| 31 | session = current_user.cloud_sessions.create!(create_params) |
| 32 | render json: session_json(session), status: :created |
| 33 | end |
| 34 | |
| 35 | def show |
| 36 | render json: session_json(@session, include_messages: true), status: :ok |
| 37 | end |
| 38 | |
| 39 | def update |
| 40 | @session.update!(update_params) |
| 41 | render json: session_json(@session), status: :ok |
| 42 | end |
| 43 | |
| 44 | def destroy |
| 45 | @session.destroy! |
| 46 | head :no_content |
| 47 | end |
| 48 | |
| 49 | # Append one message to the session. The client persists the user message |
| 50 | # when it sends, and the assistant message once streaming completes. |
| 51 | def create_message |
| 52 | message = @session.append_message!( |
| 53 | role: message_params[:role], |
| 54 | content: message_params[:content] |
| 55 | ) |
| 56 | render json: { session: session_json(@session.reload), message: message_json(message) }, |
| 57 | status: :created |
| 58 | end |
| 59 | |
| 60 | private |
| 61 | |
| 62 | def set_session |
| 63 | @session = current_user.cloud_sessions.find_by(id: params[:id]) |
| 64 | return if @session |
| 65 | |
| 66 | render_error(ERR_INVALID, "Session not found.", status: :not_found) |
| 67 | end |
| 68 | |
| 69 | def create_params |
| 70 | params.permit(:title, :model).to_h.slice("title", "model").compact_blank |
| 71 | end |
| 72 | alias update_params create_params |
| 73 | |
| 74 | def message_params |
| 75 | params.permit(:role, :content) |
| 76 | end |
| 77 | |
| 78 | def session_json(session, include_messages: false) |
| 79 | json = { |
| 80 | id: session.id, |
| 81 | title: session.title, |
| 82 | model: session.model, |
| 83 | last_message_at: session.last_message_at&.iso8601, |
| 84 | created_at: session.created_at.iso8601, |
| 85 | updated_at: session.updated_at.iso8601 |
| 86 | } |
| 87 | json[:messages] = session.cloud_messages.map { |m| message_json(m) } if include_messages |
| 88 | json |
| 89 | end |
| 90 | |
| 91 | def message_json(message) |
| 92 | { |
| 93 | id: message.id, |
| 94 | role: message.role, |
| 95 | content: message.content, |
| 96 | created_at: message.created_at.iso8601 |
| 97 | } |
| 98 | end |
| 99 | end |
| 100 | end |
| 101 | end |