diff --git a/app/graphql/mutations/ai/generate_flow.rb b/app/graphql/mutations/ai/generate_flow.rb index d4d9af59..01675582 100644 --- a/app/graphql/mutations/ai/generate_flow.rb +++ b/app/graphql/mutations/ai/generate_flow.rb @@ -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? @@ -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) @@ -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, diff --git a/app/services/error_code.rb b/app/services/error_code.rb index 06711c73..d33a3c94 100644 --- a/app/services/error_code.rb +++ b/app/services/error_code.rb @@ -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' }, diff --git a/app/services/velorum/generate_flow_service.rb b/app/services/velorum/generate_flow_service.rb index 632572ec..7b11d059 100644 --- a/app/services/velorum/generate_flow_service.rb +++ b/app/services/velorum/generate_flow_service.rb @@ -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( @@ -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 @@ -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', diff --git a/docs/graphql/enum/errorcodeenum.md b/docs/graphql/enum/errorcodeenum.md index 65bd1d66..ae73ded1 100644 --- a/docs/graphql/enum/errorcodeenum.md +++ b/docs/graphql/enum/errorcodeenum.md @@ -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 | diff --git a/spec/jobs/velorum_generate_flow_job_spec.rb b/spec/jobs/velorum_generate_flow_job_spec.rb index b2663b17..38c3145d 100644 --- a/spec/jobs/velorum_generate_flow_job_spec.rb +++ b/spec/jobs/velorum_generate_flow_job_spec.rb @@ -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 diff --git a/spec/requests/graphql/mutation/ai/generate_flow_spec.rb b/spec/requests/graphql/mutation/ai/generate_flow_spec.rb index d070484d..1e9bd36f 100644 --- a/spec/requests/graphql/mutation/ai/generate_flow_spec.rb +++ b/spec/requests/graphql/mutation/ai/generate_flow_spec.rb @@ -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 { @@ -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 diff --git a/spec/services/velorum/generate_flow_service_spec.rb b/spec/services/velorum/generate_flow_service_spec.rb index da29dc7c..c73683c1 100644 --- a/spec/services/velorum/generate_flow_service_spec.rb +++ b/spec/services/velorum/generate_flow_service_spec.rb @@ -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(