diff --git a/enterprise/app/services/captain/assistant/base_rewrite_service.rb b/enterprise/app/services/captain/assistant/base_rewrite_service.rb new file mode 100644 index 000000000..a131868d7 --- /dev/null +++ b/enterprise/app/services/captain/assistant/base_rewrite_service.rb @@ -0,0 +1,95 @@ +require 'agents' + +class Captain::Assistant::BaseRewriteService + def initialize(text:) + @text = text + end + + def execute + agent = build_agent + runner = Agents::Runner.with_agents(agent) + + result = runner.run(@text) + + # Check if result has an error field + return error_response(result.error) if result.respond_to?(:error) && result.error + + process_result(result) + rescue StandardError => e + Rails.logger.error "[Captain V2] #{self.class.name} error: #{e.message}" + Rails.logger.error e.backtrace.join("\n") + + error_response(e.message) + end + + protected + + def build_agent + Agents::Agent.new( + name: agent_name, + instructions: build_instructions, + model: agent_model, + response_schema: response_schema + ) + end + + def agent_model + InstallationConfig.find_by(name: 'CAPTAIN_OPEN_AI_MODEL')&.value.presence || OpenAiConstants::DEFAULT_MODEL + end + + # To be implemented by subclasses + def agent_name + raise NotImplementedError, "#{self.class} must implement agent_name" + end + + def build_instructions + raise NotImplementedError, "#{self.class} must implement build_instructions" + end + + def response_schema + raise NotImplementedError, "#{self.class} must implement response_schema" + end + + def process_result(result) + output = result.output + + # Check if output itself has an error + return error_response(output[:error] || output['error']) if output.is_a?(Hash) && (output[:error] || output['error']) + + build_success_response(output) + end + + # Override in subclasses for custom response structure + def build_success_response(output) + { + success: true, + result: extract_primary_field(output), + original_text: @text + } + end + + def error_response(error_message) + { + success: false, + error: error_message, + original_text: @text + } + end + + # Helper methods for extracting data from output + def extract_field(output, *field_names) + return output.to_s unless output.is_a?(Hash) + + field_names.each do |field| + value = output[field.to_sym] || output[field.to_s] + return value if value + end + + nil + end + + # Override in subclasses to specify the primary field to extract + def extract_primary_field(output) + output + end +end diff --git a/enterprise/app/services/captain/assistant/change_tone_service.rb b/enterprise/app/services/captain/assistant/change_tone_service.rb new file mode 100644 index 000000000..13468d1a8 --- /dev/null +++ b/enterprise/app/services/captain/assistant/change_tone_service.rb @@ -0,0 +1,56 @@ +class Captain::Assistant::ChangeToneService < Captain::Assistant::BaseRewriteService + SUPPORTED_TONES = %w[professional casual straightforward confident friendly].freeze + + def initialize(text:, tone:) + super(text: text) + @tone = validate_tone(tone) + end + + protected + + def agent_name + 'ToneChanger' + end + + def build_instructions + context = { tone: @tone } + Captain::PromptRenderer.render('rewrite/tone', context.with_indifferent_access) + end + + def response_schema + { + type: 'object', + properties: { + rewritten_text: { + type: 'string', + description: 'The rewritten text with the requested tone applied' + }, + tone_applied: { + type: 'string', + description: 'The tone that was applied to the text' + } + }, + required: %w[rewritten_text tone_applied], + additionalProperties: false + } + end + + def build_success_response(output) + { + success: true, + rewritten_text: extract_field(output, 'rewritten_text'), + tone: @tone, + original_text: @text + } + end + + private + + def validate_tone(tone) + tone_str = tone.to_s.downcase + + raise ArgumentError, "Unsupported tone: #{tone}. Supported tones: #{SUPPORTED_TONES.join(', ')}" unless SUPPORTED_TONES.include?(tone_str) + + tone_str + end +end diff --git a/enterprise/app/services/captain/assistant/fix_grammar_service.rb b/enterprise/app/services/captain/assistant/fix_grammar_service.rb new file mode 100644 index 000000000..c2fb4a3fe --- /dev/null +++ b/enterprise/app/services/captain/assistant/fix_grammar_service.rb @@ -0,0 +1,49 @@ +class Captain::Assistant::FixGrammarService < Captain::Assistant::BaseRewriteService + protected + + def agent_name + 'GrammarFixer' + end + + def build_instructions + Captain::PromptRenderer.render('rewrite/grammar', {}) + end + + def response_schema + { + type: 'object', + properties: { + corrected_text: { + type: 'string', + description: 'The text with corrected grammar, spelling, and punctuation' + }, + corrections_made: { + type: 'array', + description: 'List of corrections that were made', + items: { + type: 'string' + } + } + }, + required: ['corrected_text'], + additionalProperties: false + } + end + + def build_success_response(output) + { + success: true, + corrected_text: extract_field(output, 'corrected_text'), + corrections_made: extract_corrections(output), + original_text: @text + } + end + + private + + def extract_corrections(output) + return [] unless output.is_a?(Hash) + + output[:corrections_made] || output['corrections_made'] || [] + end +end diff --git a/enterprise/lib/captain/prompts/rewrite/grammar.liquid b/enterprise/lib/captain/prompts/rewrite/grammar.liquid new file mode 100644 index 000000000..4fa144f49 --- /dev/null +++ b/enterprise/lib/captain/prompts/rewrite/grammar.liquid @@ -0,0 +1,8 @@ +You are an expert writing editor. Rewrite the provided text to correct grammar, spelling, and punctuation while fully preserving the original meaning and style. + +Rules: +• Preserve tone, language, formatting, structure, proper nouns, URLs, quotes, numbers, IDs, addresses. +• Do not modify content inside code blocks or backticks. +• Do not add content or opinions. Keep length similar. +• Minimal edits if already correct. +• Do not alter legal, medical, or compliance meaning. diff --git a/enterprise/lib/captain/prompts/rewrite/tone.liquid b/enterprise/lib/captain/prompts/rewrite/tone.liquid new file mode 100644 index 000000000..0680065a6 --- /dev/null +++ b/enterprise/lib/captain/prompts/rewrite/tone.liquid @@ -0,0 +1,17 @@ +You are an expert writing editor. Rewrite the provided text according to the selected tone while keeping the original intent. + +{% case tone %} +{% when "professional" %}Rewrite in a formal and precise tone. +{% when "casual" %}Rewrite in a relaxed and conversational tone. +{% when "straightforward" %}Rewrite concise and direct. Remove filler. +{% when "confident" %}Rewrite bold and assured. +{% when "friendly" %}Rewrite warm and positive. +{% else %}Rewrite to improve clarity and smoothness. +{% endcase %} + +Rules: +• Preserve original language, meaning, formatting, structure, proper nouns, URLs, quotes, numbers, IDs, addresses. +• Do not modify content inside code blocks or backticks. +• Do not add content or opinions. Keep length similar. +• Minimal edits if already correct. +• Do not alter legal, medical, or compliance meaning.