Module: LanguageOperator::CLI::Commands::System::Synthesize

Included in:
Base
Defined in:
lib/language_operator/cli/commands/system/synthesize.rb

Overview

Agent code synthesis command

Class Method Summary collapse

Class Method Details

.included(base) ⇒ Object



9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
# File 'lib/language_operator/cli/commands/system/synthesize.rb', line 9

def self.included(base)
  base.class_eval do
    desc 'synthesize [INSTRUCTIONS]', 'Synthesize agent code from natural language instructions'
    long_desc <<-DESC
      Synthesize agent code by converting natural language instructions
      into Ruby DSL code without creating an actual agent.

      This command uses a LanguageModel resource from your cluster to generate
      agent code. If --model is not specified, the first available model will
      be auto-selected.

      Instructions can be provided either as a command argument or via STDIN.
      If no argument is provided, the command will read from STDIN.

      This command helps you validate your instructions and understand how the
      synthesis engine interprets them. Use --dry-run to see the prompt that
      would be sent to the LLM, or run without it to generate actual code.

      Examples:
        # Test with dry-run (show prompt only)
        langop system synthesize "Monitor GitHub issues daily" --dry-run

        # Generate code from instructions (auto-selects first available model)
        langop system synthesize "Send daily reports to Slack"

        # Use a specific cluster model
        langop system synthesize "Process webhooks from GitHub" --model my-claude

        # Output raw code without formatting (useful for piping to files)
        langop system synthesize "Monitor logs" --raw > agent.rb

        # Read instructions from STDIN
        cat instructions.txt | langop system synthesize > agent.rb

        # Read from STDIN with pipe
        echo "Monitor GitHub issues" | langop system synthesize --raw

        # Specify custom agent name and tools
        langop system synthesize "Process webhooks from GitHub" \\
          --agent-name github-processor \\
          --tools github,slack \\
          --model my-gpt4
    DESC
    option :agent_name, type: :string, default: 'test-agent', desc: 'Name for the test agent'
    option :tools, type: :string, desc: 'Comma-separated list of available tools'
    option :models, type: :string, desc: 'Comma-separated list of available models (from cluster)'
    option :model, type: :string, desc: 'Model to use for synthesis (defaults to first available in cluster)'
    option :dry_run, type: :boolean, default: false, desc: 'Show prompt without calling LLM'
    option :raw, type: :boolean, default: false, desc: 'Output only the raw code without formatting'

    def synthesize(instructions = nil)
      handle_command_error('synthesize agent') do
        # Read instructions from STDIN if not provided as argument
        if instructions.nil? || instructions.strip.empty?
          if $stdin.tty?
            Formatters::ProgressFormatter.error('No instructions provided')
            puts
            puts 'Provide instructions either as an argument or via STDIN:'
            puts '  langop system synthesize "Your instructions here"'
            puts '  cat instructions.txt | langop system synthesize'
            exit 1
          else
            instructions = $stdin.read.strip
            if instructions.empty?
              Formatters::ProgressFormatter.error('No instructions provided')
              puts
              puts 'Provide instructions either as an argument or via STDIN:'
              puts '  langop system synthesize "Your instructions here"'
              puts '  cat instructions.txt | langop system synthesize'
              exit 1
            end
          end
        end
        # Select model to use for synthesis
        selected_model = select_synthesis_model

        # Load synthesis template
        template_content = load_bundled_template('agent')

        # Detect temporal intent from instructions
        temporal_intent = detect_temporal_intent(instructions)

        # Prepare template data
        template_data = {
          'Instructions' => instructions,
          'AgentName' => options[:agent_name],
          'ToolsList' => format_tools_list(options[:tools]),
          'ModelsList' => format_models_list(options[:models]),
          'TemporalIntent' => temporal_intent,
          'PersonaSection' => '',
          'ScheduleSection' => temporal_intent == 'scheduled' ? '  schedule "0 */1 * * *"  # Example hourly schedule' : '',
          'ScheduleRules' => temporal_intent == 'scheduled' ? "\n2. Include schedule with cron expression\n3. Set mode to :scheduled\n4. " : "\n2. ",
          'ConstraintsSection' => '',
          'ErrorContext' => nil
        }

        # Render template (Go-style template syntax)
        rendered_prompt = render_go_template(template_content, template_data)

        if options[:dry_run]
          # Show the prompt that would be sent
          puts 'Synthesis Prompt Preview'
          puts '=' * 80
          puts
          puts rendered_prompt
          puts
          puts '=' * 80
          Formatters::ProgressFormatter.success('Dry-run complete - prompt displayed above')
          return
        end

        # Call LLM to generate code (no output - just do it)
        llm_response = call_llm_for_synthesis(rendered_prompt, selected_model)

        # Extract Ruby code from response
        generated_code = extract_ruby_code(llm_response)

        if generated_code.nil?
          Formatters::ProgressFormatter.error('Failed to extract Ruby code from LLM response')
          puts
          puts 'LLM Response:'
          puts llm_response
          exit 1
        end

        # Handle raw output
        if options[:raw]
          puts generated_code
          return
        end

        # Display formatted code
        highlighted_code = highlight_ruby_code(generated_code)

        puts highlighted_code
      end
    end

    private

    # Detect temporal intent from instructions (scheduled vs autonomous)
    def detect_temporal_intent(instructions)
      temporal_keywords = {
        scheduled: %w[daily weekly hourly monthly schedule cron every day week hour minute],
        autonomous: %w[monitor watch continuously constantly always loop]
      }

      instructions_lower = instructions.downcase

      # Check for scheduled keywords
      scheduled_matches = temporal_keywords[:scheduled].count { |keyword| instructions_lower.include?(keyword) }
      autonomous_matches = temporal_keywords[:autonomous].count { |keyword| instructions_lower.include?(keyword) }

      scheduled_matches > autonomous_matches ? 'scheduled' : 'autonomous'
    end

    # Format tools list for template
    def format_tools_list(tools_str)
      return 'No tools specified' if tools_str.nil? || tools_str.strip.empty?

      tools = tools_str.split(',').map(&:strip)
      tools.map { |tool| "- #{tool}" }.join("\n")
    end

    # Format models list for template
    def format_models_list(models_str)
      # If not specified, try to detect from cluster
      if models_str.nil? || models_str.strip.empty?
        models = detect_available_models
        return models.map { |model| "- #{model}" }.join("\n") unless models.empty?

        return 'No models available (run: langop model list)'
      end

      models = models_str.split(',').map(&:strip)
      models.map { |model| "- #{model}" }.join("\n")
    end

    # Detect available models from cluster
    def detect_available_models
      models = ctx.client.list_resources('LanguageModel', namespace: ctx.namespace)
      models.map { |m| m.dig('metadata', 'name') }
    rescue StandardError => e
      Formatters::ProgressFormatter.error("Failed to list models from cluster: #{e.message}")
      []
    end

    # Select model to use for synthesis
    def select_synthesis_model
      # If --model option specified, use it
      return options[:model] if options[:model]

      # Otherwise, auto-select from available cluster models
      available_models = detect_available_models

      if available_models.empty?
        Formatters::ProgressFormatter.error('No models available in cluster')
        puts
        puts 'Please create a model first:'
        puts '  langop model create'
        puts
        puts 'Or list existing models:'
        puts '  langop model list'
        exit 1
      end

      # Auto-select first available model (silently)
      available_models.first
    end
  end
end