Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions app/graphql/mutations/ai/generate_flow.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ class GenerateFlow < BaseMutation

def resolve(project_id:, prompt:, model_identifier:, flow_id: nil)
return error_response(:invalid_setting, 'AI is disabled') unless ai_enabled?
return error_response(:missing_parameter, 'Prompt cannot be empty') if prompt.blank?
return error_response(:missing_parameter, 'Model identifier cannot be empty') if model_identifier.blank?

project = SagittariusSchema.object_from_id(project_id)
return error_response(:project_not_found, 'Invalid project id') if project.nil?
Expand All @@ -41,6 +43,9 @@ def resolve(project_id:, prompt:, model_identifier:, flow_id: nil)
return error_response(:no_primary_runtime, 'Project has no primary runtime') if project.primary_runtime.nil?

return error_response(:missing_permission, 'Missing permission') unless allowed?(project, flow)
unless definitions_available?(project.primary_runtime)
return error_response(:no_definitions, 'The primary runtime must provide functions and flow types')
end

execution_identifier = SecureRandom.uuid
VelorumGenerateFlowJob.perform_later(execution_identifier, project.id, prompt, model_identifier, flow&.id)
Expand All @@ -63,6 +68,10 @@ def allowed?(project, flow)
Ability.allowed?(current_authentication, ability, subject)
end

def definitions_available?(runtime)
runtime.function_definitions.exists? && runtime.flow_types.exists?
end

def error_response(error_code, message)
{
execution_identifier: nil,
Expand Down
1 change: 1 addition & 0 deletions app/services/error_code.rb
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ def self.error_codes
invalid_attachment: { description: 'The attachment is invalid because of active model errors' },
invalid_flow: { description: 'The flow is invalid because of active model errors' },
flow_generation_failed: { description: 'Flow generation failed' },
no_definitions: { description: 'No definitions are available to generate a flow' },
project_not_found: { description: 'The namespace project with the given identifier was not found' },
runtime_not_found: { description: 'The runtime with the given identifier was not found' },
runtime_not_assigned: { description: 'The runtime is not assigned to the project' },
Expand Down
12 changes: 12 additions & 0 deletions app/services/velorum/generate_flow_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ def execute
return flow_project_mismatch_response if flow.present? && flow.project != project
return missing_permission_response if authorize? && !allowed?
return no_primary_runtime_response if runtime.nil?
return no_definitions_response unless definitions?

response = flow.present? ? client.flow(flow_request) : client.prompt(prompt_request)
logger.debug(
Expand Down Expand Up @@ -123,6 +124,10 @@ def runtime
project.primary_runtime
end

def definitions?
runtime.function_definitions.any? && runtime.flow_types.any?
end

def client
@client ||= Sagittarius::Velorum::Client.new
end
Expand Down Expand Up @@ -154,6 +159,13 @@ def no_primary_runtime_response
ServiceResponse.error(message: 'Project has no primary runtime', error_code: :no_primary_runtime)
end

def no_definitions_response
ServiceResponse.error(
message: 'The primary runtime must provide functions and flow types',
error_code: :no_definitions
)
end

def flow_generation_failed_response(error)
ServiceResponse.error(
message: 'Flow generation failed',
Expand Down
1 change: 1 addition & 0 deletions docs/graphql/enum/errorcodeenum.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ Represents the available error responses
| `NAMESPACE_ROLE_NOT_FOUND` | The namespace role with the given identifier was not found |
| `NODE_NOT_FOUND` | The node with this id does not exist |
| `NO_DATA_TYPE_FOR_IDENTIFIER` | No data type could be found for the given identifier |
| `NO_DEFINITIONS` | No definitions are available to generate a flow |
| `NO_FREE_LICENSE_SEATS` | There are no free license seats to complete this operation |
| `NO_PRIMARY_RUNTIME` | The project does not have a primary runtime |
| `ORGANIZATION_NOT_FOUND` | The organization with the given identifier was not found |
Expand Down
11 changes: 1 addition & 10 deletions spec/jobs/velorum_generate_flow_job_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,21 +32,12 @@
expect(SubscriptionTriggers).to have_received(:ai_generate_flow).with(execution_identifier, flow)
end

it 'does not trigger the subscription when the project is gone' do
perform_enqueued_jobs do
described_class.perform_later(execution_identifier, -1, 'Generate a flow', 'gpt-5')
end

expect(Velorum::GenerateFlowService).not_to have_received(:new)
expect(SubscriptionTriggers).not_to have_received(:ai_generate_flow)
end

context 'when flow generation fails' do
let(:service_response) do
ServiceResponse.error(message: 'Flow generation failed', error_code: :flow_generation_failed)
end

it 'triggers a nil subscription response to close the stream' do
it 'closes the subscription without a flow' do
perform_enqueued_jobs do
described_class.perform_later(execution_identifier, project.id, 'Generate a flow', 'gpt-5')
end
Expand Down
69 changes: 69 additions & 0 deletions spec/requests/graphql/mutation/ai/generate_flow_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,27 @@

let(:current_user) { create(:user) }
let(:runtime) { create(:runtime) }
let(:runtime_module) { create(:runtime_module, runtime: runtime) }
let(:runtime_function_definition) do
create(:runtime_function_definition, runtime: runtime, runtime_module: runtime_module)
end
let(:runtime_flow_type) { create(:runtime_flow_type, runtime: runtime, runtime_module: runtime_module) }
let!(:function_definition) do
create(
:function_definition,
runtime: runtime,
runtime_module: runtime_module,
runtime_function_definition: runtime_function_definition
)
end
let!(:flow_type) do
create(
:flow_type,
runtime: runtime,
runtime_module: runtime_module,
runtime_flow_type: runtime_flow_type
)
end
let(:project) { create(:namespace_project, primary_runtime: runtime) }
let(:variables) do
{
Expand Down Expand Up @@ -69,4 +90,52 @@
expect(VelorumGenerateFlowJob).not_to have_received(:perform_later)
end
end

context 'when the prompt is blank' do
before { variables[:input][:prompt] = ' ' }

it 'returns an error and does not enqueue a job' do
mutate!

expect(graphql_data_at(:ai_generate_flow, :execution_identifier)).to be_nil
expect(graphql_data_at(:ai_generate_flow, :errors, 0, :error_code)).to eq('MISSING_PARAMETER')
expect(VelorumGenerateFlowJob).not_to have_received(:perform_later)
end
end

context 'when the model identifier is blank' do
before { variables[:input][:modelIdentifier] = ' ' }

it 'returns an error and does not enqueue a job' do
mutate!

expect(graphql_data_at(:ai_generate_flow, :execution_identifier)).to be_nil
expect(graphql_data_at(:ai_generate_flow, :errors, 0, :error_code)).to eq('MISSING_PARAMETER')
expect(VelorumGenerateFlowJob).not_to have_received(:perform_later)
end
end

context 'when the primary runtime has no functions' do
before { function_definition.destroy! }

it 'returns an error and does not enqueue a job' do
mutate!

expect(graphql_data_at(:ai_generate_flow, :execution_identifier)).to be_nil
expect(graphql_data_at(:ai_generate_flow, :errors, 0, :error_code)).to eq('NO_DEFINITIONS')
expect(VelorumGenerateFlowJob).not_to have_received(:perform_later)
end
end

context 'when the primary runtime has no flow types' do
before { flow_type.destroy! }

it 'returns an error and does not enqueue a job' do
mutate!

expect(graphql_data_at(:ai_generate_flow, :execution_identifier)).to be_nil
expect(graphql_data_at(:ai_generate_flow, :errors, 0, :error_code)).to eq('NO_DEFINITIONS')
expect(VelorumGenerateFlowJob).not_to have_received(:perform_later)
end
end
end
44 changes: 44 additions & 0 deletions spec/services/velorum/generate_flow_service_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,50 @@
end
end

context 'when the runtime does not have functions and flow types' do
let(:runtime) do
instance_double(
Runtime,
id: 9,
function_definitions: [],
data_types: [],
flow_types: []
)
end

it 'returns an error without calling Velorum' do
expect(service_response).to be_error
expect(service_response.message).to eq('The primary runtime must provide functions and flow types')
expect(service_response.payload[:error_code]).to eq(:no_definitions)
expect(client).not_to have_received(:prompt)
expect(client).not_to have_received(:flow)
end
end

context 'when the runtime only has functions' do
let(:runtime) do
instance_double(Runtime, id: 9, function_definitions: [function_definition], data_types: [], flow_types: [])
end

it 'returns an error without calling Velorum' do
expect(service_response).to be_error
expect(service_response.payload[:error_code]).to eq(:no_definitions)
expect(client).not_to have_received(:prompt)
end
end

context 'when the runtime only has flow types' do
let(:runtime) do
instance_double(Runtime, id: 9, function_definitions: [], data_types: [], flow_types: [flow_type])
end

it 'returns an error without calling Velorum' do
expect(service_response).to be_error
expect(service_response.payload[:error_code]).to eq(:no_definitions)
expect(client).not_to have_received(:prompt)
end
end

context 'when Velorum returns a gRPC error' do
before do
allow(client).to receive(:prompt).and_raise(
Expand Down