Class: Legate::CLI::AgentCommands
- Inherits:
-
BaseCommand
- Object
- Thor
- BaseCommand
- Legate::CLI::AgentCommands
- Includes:
- OutputHelper
- Defined in:
- lib/legate/cli/agent_commands.rb
Overview
CLI commands for agent definition management AND temporary execution
Constant Summary collapse
- @@session_service_for_execute =
In-memory session service backing the ‘execute` command. Overridable in tests.
Legate::SessionService::InMemory.new
Class Method Summary collapse
-
.exit_on_failure? ⇒ Boolean
— END CHAT COMMAND —.
Instance Method Summary collapse
- #ai_generate ⇒ Object
- #chat(agent_name_str) ⇒ Object
- #delete(name) ⇒ Object
- #execute(name, task) ⇒ Object
- #export(name) ⇒ Object
- #generate(name) ⇒ Object
- #list ⇒ Object
- #save(name) ⇒ Object
- #start(name) ⇒ Object
- #status(name) ⇒ Object
- #stop(name) ⇒ Object
Methods included from OutputHelper
#output_error, #output_result, #status_message
Methods inherited from BaseCommand
Class Method Details
.exit_on_failure? ⇒ Boolean
— END CHAT COMMAND —
945 946 947 |
# File 'lib/legate/cli/agent_commands.rb', line 945 def self.exit_on_failure? true end |
Instance Method Details
#ai_generate ⇒ Object
411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 |
# File 'lib/legate/cli/agent_commands.rb', line 411 def ai_generate require_relative '../generators' description = nil from_stdin = false # Priority: --description > --prompt-file > stdin if [:description] && ![:description].strip.empty? description = [:description].strip elsif [:prompt_file] unless File.exist?([:prompt_file]) say "Error: Prompt file '#{[:prompt_file]}' not found.", :red exit(1) end description = File.read([:prompt_file]).strip elsif !$stdin.tty? # Reading from stdin (piped input) description = $stdin.read.strip from_stdin = true end if description.nil? || description.empty? say 'Error: No description provided. Use --description, --prompt-file, or pipe via stdin.', :red exit(1) end # Determine output mode output_to_stdout = [:stdout] || from_stdin say 'Generating agent code via AI...', :cyan unless output_to_stdout begin result = Legate::Generators::AgentGenerator.generate(description: description) code = result[:code] suggested_name = result[:suggested_name] if output_to_stdout puts code else # Write to file file_path = [:output] || "./#{suggested_name}_agent.rb" if File.exist?(file_path) && ![:force] && !yes?("File '#{file_path}' already exists. Overwrite? [y/N]", :yellow) say 'Generation cancelled.', :yellow exit(0) end File.write(file_path, code) say "Agent definition generated and saved to '#{file_path}'", :green say " Suggested name: #{suggested_name}", :cyan end rescue Legate::Generators::AgentGenerator::ApiKeyMissingError => e say "Error: #{e.}", :red exit(1) rescue Legate::Generators::AgentGenerator::ApiError => e say "Error: #{e.}", :red exit(1) rescue Legate::Generators::AgentGenerator::GenerationError => e say "Error: #{e.}", :red exit(1) rescue StandardError => e say "Unexpected error: #{e.class} - #{e.}", :red exit(1) end end |
#chat(agent_name_str) ⇒ Object
818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 |
# File 'lib/legate/cli/agent_commands.rb', line 818 def chat(agent_name_str) ::CLI::UI::StdoutRouter.enable agent_name_sym = agent_name_str.to_sym agent_definition_object = Legate::GlobalDefinitionRegistry.find(agent_name_sym) unless agent_definition_object ::CLI::UI.puts "{{red:Error: Agent definition '#{agent_name_str}' not found.}}" exit(1) end # Convert AgentDefinition to hash for display/compatibility definition = Legate::GlobalDefinitionRegistry.get_definition(agent_name_sym) session_service_instance = Legate::SessionService::InMemory.new current_session_id = [:session_id] legate_session = nil # Will hold the loaded Legate::Session object ::CLI::UI::Frame.open("Chat Session with #{agent_name_str}", color: :blue) do ::CLI::UI.puts "{{bold:Agent Description:}} #{definition[:description]}" if current_session_id legate_session = session_service_instance.get_session(session_id: current_session_id) if legate_session ::CLI::UI.puts "{{green:Resuming session:}} #{current_session_id} (in-memory)" # --- MODIFIED: Display history if session is loaded and has events --- if legate_session.events && !legate_session.events.empty? ::CLI::UI.puts "\n{{bold:━━━ Recent Conversation History ━━━}}" # Group events by conversation turns history_events = legate_session.events.last(20) # Show more history items current_date = nil history_events.each do |event| # Extract timestamp from event and format it event_time = event. ? Time.at(event.) : Time.now formatted_time = event_time.strftime('%H:%M:%S') # Show date separator if this is a new day event_date = event_time.strftime('%Y-%m-%d') if current_date != event_date current_date = event_date ::CLI::UI.puts "\n{{bold:┅┅┅ #{event_time.strftime('%B %d, %Y')} ┅┅┅}}" end if event.role == :user # For user role, event.content is the string message _format_chat_turn_output_cli_ui(event.content, :user, formatted_time) elsif event.role == :agent # For agent role, event.content is the hash {status:, result:, ...} _format_chat_turn_output_cli_ui(event.content, :agent, formatted_time) end # Tool events are generally not shown in simple chat history end ::CLI::UI.puts "{{bold:━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━}}\n" else ::CLI::UI.puts '{{italic:No previous messages in this session.}}' end # --- END MODIFICATION --- else ::CLI::UI.puts "{{yellow:Warning: Session ID '#{current_session_id}' not found. Starting new session.}}" current_session_id = nil # Force new session creation end end unless legate_session # If still no session (either not provided, or provided but not found) legate_session = session_service_instance.create_session(app_name: agent_name_str, user_id: [:user_id]) current_session_id = legate_session.id # Update current_session_id with the new one ::CLI::UI.puts "{{green:Started new session:}} #{current_session_id} (in-memory)" end ::CLI::UI.puts "{{gray:Type 'exit' or 'quit' to end the chat.}}" end ::CLI::UI.puts '---' agent = nil begin # Instantiate the agent with its definition object and the session service agent = Legate::Agent.new( definition: agent_definition_object, session_service: session_service_instance ) # Tool setup is now handled within Legate::Agent#initialize based on the definition object. agent.start rescue StandardError => e ::CLI::UI.puts "{{red:Error initializing or starting agent: #{e.}}}" exit(1) end loop do user_input = ::CLI::UI::Prompt.ask('You') break if user_input.nil? user_input.strip! break if %w[exit quit].include?(user_input.downcase) next if user_input.empty? final_event = nil session_lost_flag = false begin ::CLI::UI::Spinner.spin('Agent thinking...') do |_spinner| current_legate_session_for_task = session_service_instance.get_session(session_id: current_session_id) unless current_legate_session_for_task ::CLI::UI.puts "{{red:Error: Session '#{current_session_id}' lost. Please restart chat.}}" session_lost_flag = true break end final_event = agent.run_task( session_id: current_session_id, user_input: user_input, session_service: session_service_instance ) end break if session_lost_flag _format_chat_turn_output_cli_ui(final_event) rescue StandardError => e ::CLI::UI::Frame.open('Error During Task', color: :red) do ::CLI::UI.puts e. end end end ensure agent&.stop if agent&.running? ::CLI::UI.puts '{{yellow:Chat ended.}}' end |
#delete(name) ⇒ Object
284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 |
# File 'lib/legate/cli/agent_commands.rb', line 284 def delete(name) name_sym = name.to_sym definition_exists = Legate::GlobalDefinitionRegistry.definition_exists?(name_sym) unless definition_exists say "Error: Agent definition '#{name}' not found.", :red exit(1) end if yes?("Are you sure you want to permanently delete agent definition '#{name}'? [y/N]", :yellow) Legate::GlobalDefinitionRegistry.delete_definition(name_sym) say "Agent definition '#{name}' deleted successfully.", :green else say 'Deletion cancelled.', :yellow end end |
#execute(name, task) ⇒ Object
725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 |
# File 'lib/legate/cli/agent_commands.rb', line 725 def execute(name, task) # Suppress all logging in JSON mode for clean output Legate.logger.level = Logger::FATAL if json_mode? name_sym = name.to_sym ("Loading agent '#{name}' to execute task: \"#{task}\"...") agent_definition_object = Legate::GlobalDefinitionRegistry.find(name_sym) unless agent_definition_object output_error("Agent definition '#{name}' not found.", metadata: { agent: name }) exit(1) end session_service_instance = @@session_service_for_execute session_id_opt = [:session_id] legate_session = nil if session_id_opt legate_session = session_service_instance.get_session(session_id: session_id_opt) if legate_session then ("Continuing session: #{session_id_opt}", :cyan) else ("Warning: Session ID '#{session_id_opt}' provided but not found. Starting a new session.", :yellow) session_id_opt = nil end end unless legate_session legate_session = session_service_instance.create_session(app_name: name, user_id: [:user_id]) session_id_opt = legate_session.id ("Started new session: #{session_id_opt}", :cyan) (' (Using in-memory session storage)', :cyan) end agent = nil e_outer = nil begin # Pass the definition object. Session service for the agent instance itself will use global default # or the one passed if Legate::Agent.new supported it directly for its own session_service attr. # The run_task method will use the session_service_instance passed to it for actual session operations. agent = Legate::Agent.new( definition: agent_definition_object, session_service: session_service_instance ) (" - Agent uses model: #{agent.model_name}", :cyan) # Tool loading is now handled by Legate::Agent#initialize via the definition. loaded_tool_instances = agent.tools loaded_tool_names = loaded_tool_instances.map(&:name) defined_tool_names = agent_definition_object.tool_names.to_a missing_tools = defined_tool_names - loaded_tool_names (" - Warning: Tools defined but not loaded/found: [#{missing_tools.join(', ')}]", :yellow) unless missing_tools.empty? (" - Loaded tools: [#{loaded_tool_names.join(', ')}]", :cyan) (' - Starting agent runtime...', :cyan) agent.start ('started.', :cyan) (" - Running task in session #{session_id_opt}: '#{task}'...", :cyan) final_event_or_error = agent.run_task( session_id: session_id_opt, user_input: task, session_service: session_service_instance ) ('finished.', :cyan) ("\nTask Result:", :bold) output_result(final_event_or_error, metadata: { session_id: session_id_opt, agent: name }, format_method: :format_cli_result) rescue StandardError => e e_outer = e output_error(e, metadata: { agent: name, session_id: session_id_opt }) puts e.backtrace.first(5).join("\n") unless json_mode? ensure if agent&.running? (' - Stopping agent runtime...', :cyan) agent.stop ('stopped.', :cyan) end exit(1) if e_outer end end |
#export(name) ⇒ Object
667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 |
# File 'lib/legate/cli/agent_commands.rb', line 667 def export(name) name_sym = name.to_sym # Load definition from registry definition = Legate::GlobalDefinitionRegistry.get_definition(name_sym) unless definition say "Error: Agent definition '#{name}' not found.", :red exit(1) end # Clean up internal fields before export export_data = definition.dup export_data.delete(:persistent_status) # implementation detail # Ensure keys are strings for cleaner JSON/YAML export_data = export_data.transform_keys(&:to_s) # Format output output_content = '' case [:format].downcase when 'json' output_content = JSON.pretty_generate(export_data) when 'yaml' output_content = export_data.to_yaml end # Write to file or stdout if [:output] begin File.write([:output], output_content) say "Agent definition exported to #{[:output]}", :green rescue StandardError => e say "Error writing to file: #{e.}", :red exit(1) end else say output_content end end |
#generate(name) ⇒ Object
309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 |
# File 'lib/legate/cli/agent_commands.rb', line 309 def generate(name) agent_name_sym = name.to_sym dir_path = File.([:dir]) file_path = File.join(dir_path, "#{name}_agent.rb") if File.exist?(file_path) && ![:force] && !yes?("Agent file '#{file_path}' already exists. Overwrite? [y/N]", :yellow) say 'Generation cancelled.', :yellow exit(0) end begin FileUtils.mkdir_p(dir_path) rescue SystemCallError => e say "Error: Could not create directory '#{dir_path}': #{e.}", :red exit(1) end agent_name_str = name description = [:description] instruction = [:instruction] tools_list = [:tools].split(',').map(&:strip).reject(&:empty?).map(&:to_sym) model_str = [:model] webhook_enabled = [:webhook_enabled] code = <<~RUBY require 'legate' Legate::Agent.define do |a| a.name :#{agent_name_sym} a.description "#{description}" a.instruction "#{instruction}" #{' '} RUBY if model_str && !model_str.empty? code += " # Optional: Specify model (defaults to Legate.config.default_model_name)\n" code += " a.model_name '#{model_str}'\n\n" else code += " # Model will use framework default: #{Legate.config.default_model_name}\n\n" end code += " # Define tools the agent can use\n" if tools_list.empty? code += " # a.use_tool :echo # Example\n" else tools_list.each { |tool| code += " a.use_tool :#{tool}\n" } end code += "\n" if webhook_enabled code += <<~WEBHOOK # --- Webhook Configuration ---#{' '} # This agent can be triggered by POST /webhooks/agents/#{agent_name_sym}/trigger # (Assuming default listener base_path and dynamic_agent_route_pattern in Legate.configure) a.webhook_enabled true a.webhook_transformer ->(request_body) do#{' '} raise NotImplementedError, "Please implement the webhook_transformer proc to convert request_body into agent user_input." end a.webhook_session_extractor ->(request_body) do raise NotImplementedError, "Please implement the webhook_session_extractor proc to extract a session ID." end WEBHOOK end code += "end\n" begin File.write(file_path, code) say "Agent definition file created at '#{file_path}'", :green if webhook_enabled say "\nWebhook configuration placeholders added. Please implement the required transformer and extractor procs.", :yellow say 'Remember to configure validation and secrets for production use!', :yellow end rescue SystemCallError => e say "Error: Could not write file '#{file_path}': #{e.}", :red exit(1) end end |
#list ⇒ Object
207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 |
# File 'lib/legate/cli/agent_commands.rb', line 207 def list definitions = Legate::GlobalDefinitionRegistry.all if json_mode? agents = definitions.sort_by { |name, _| name.to_s }.map do |name, defn| { name: name.to_s, description: defn.description || nil, model: (defn.model_name || Legate::Agent::DEFAULT_MODEL).to_s, tools: defn.tool_names.to_a.map(&:to_s) } end puts JSON.generate({ agents: agents }) elsif definitions.empty? say 'No agent definitions found.' else say 'Defined Agents:', :bold definitions.sort_by { |name, _| name.to_s }.each do |name, defn| description = defn.description || '[No description]' tools = defn.tool_names.to_a model = (defn.model_name || "#{Legate::Agent::DEFAULT_MODEL} (Default)").to_s tools_str = tools.empty? ? 'None' : tools.join(', ') say "- #{name}: #{description} (Model: #{model}, Tools: #{tools_str})" end end end |
#save(name) ⇒ Object
242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 |
# File 'lib/legate/cli/agent_commands.rb', line 242 def save(name) name_sym = name.to_sym description = [:description] model_to_save = [:model] && ![:model].empty? ? [:model] : Legate::Agent::DEFAULT_MODEL instruction_to_save = [:instruction] selected_tools = [] valid_tools = Legate::GlobalToolManager.registered_tool_names.map(&:to_s) if [:tools] requested_tools = [:tools].split(',').map(&:strip).reject(&:empty?) requested_tools.each do |tool_name| if valid_tools.include?(tool_name) selected_tools << tool_name unless selected_tools.include?(tool_name) else say "Warning: Unknown globally registered tool '#{tool_name}', ignoring.", :yellow end end end definition = { description: description, tools: selected_tools, model: model_to_save, instruction: instruction_to_save, fallback_mode: :error, mcp_servers_json: [:mcp_servers_json] || '[]', webhook_enabled: [:webhook_enabled], webhook_secret: [:webhook_secret] } # Build an AgentDefinition object and register it agent_def = Legate::AgentDefinition.from_hash(definition.merge(name: name_sym)) unless agent_def say 'Error creating agent definition. Aborting.', :red exit(1) end Legate::GlobalDefinitionRegistry.register(agent_def) tools_msg = selected_tools.empty? ? 'None' : selected_tools.join(', ') say "Agent definition '#{name}' saved (Model: #{model_to_save}, Tools: #{tools_msg}, Instruction: #{instruction_to_save ? 'Set' : 'Not Set'}).", :green end |
#start(name) ⇒ Object
488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 |
# File 'lib/legate/cli/agent_commands.rb', line 488 def start(name) # Suppress all logging in JSON mode for clean output Legate.logger.level = Logger::FATAL if json_mode? name_sym = name.to_sym ("Loading agent '#{name}'...") # First check the global registry agent_definition_object = Legate::GlobalDefinitionRegistry.find(name_sym) unless agent_definition_object output_error("Agent definition '#{name}' not found.", metadata: { agent: name }) exit(1) end agent = nil result_data = nil begin # Pass the definition object directly. Session service will use global default. agent = Legate::Agent.new(definition: agent_definition_object) # Tool loading is now handled by Legate::Agent#initialize via the definition. loaded_tool_names = agent.tools.map(&:name) defined_tool_names = agent_definition_object.tool_names.to_a missing_tools = defined_tool_names - loaded_tool_names (" - Agent uses model: #{agent.model_name}", :cyan) (" - Agent instruction: #{agent.instruction.inspect}", :cyan) (" - Warning: Tools defined but not loaded/found: [#{missing_tools.join(', ')}]", :yellow) unless missing_tools.empty? (" - Loaded tools: [#{loaded_tool_names.join(', ')}]", :cyan) (' - Starting agent runtime...', :cyan) agent.start ('started.', :cyan) ("\nAgent '#{name}' is ready.", :green) result_data = { status: 'ready', agent: name, model: agent.model_name, tools: loaded_tool_names.map(&:to_s), missing_tools: missing_tools.map(&:to_s) } rescue StandardError => e output_error(e, metadata: { agent: name }) puts e.backtrace.first(5).join("\n") unless json_mode? exit(1) ensure if agent&.running? (' - Stopping agent runtime...', :cyan) agent.stop ('stopped.', :cyan) end end # Output final result in JSON mode return unless json_mode? && result_data puts JSON.generate(result_data) end |
#status(name) ⇒ Object
620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 |
# File 'lib/legate/cli/agent_commands.rb', line 620 def status(name) # Suppress all logging in JSON mode for clean output Legate.logger.level = Logger::FATAL if json_mode? name_sym = name.to_sym ("Checking status of agent '#{name}'...") # Load definition from registry definition = Legate::GlobalDefinitionRegistry.get_definition(name_sym) unless definition output_error("Agent definition '#{name}' not found.", metadata: { agent: name }) exit(1) end persistent_status = definition[:persistent_status] || 'stopped' model = definition[:model] || Legate::Agent::DEFAULT_MODEL description = definition[:description] || '[No description]' tools = definition[:tools] || [] if json_mode? puts JSON.generate({ agent: name, status: persistent_status, model: model, description: description, tools: tools }) else say "Agent: #{name}", :bold case persistent_status when 'running' say " Status: #{persistent_status}", :green when 'stopped' say " Status: #{persistent_status}", :yellow else say " Status: #{persistent_status}", :cyan end say " Model: #{model}" say " Description: #{description}" say " Tools: #{tools.empty? ? 'None' : tools.join(', ')}" end end |
#stop(name) ⇒ Object
565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 |
# File 'lib/legate/cli/agent_commands.rb', line 565 def stop(name) # Suppress all logging in JSON mode for clean output Legate.logger.level = Logger::FATAL if json_mode? name_sym = name.to_sym ("Stopping agent '#{name}'...") # Load definition from registry definition = Legate::GlobalDefinitionRegistry.get_definition(name_sym) unless definition output_error("Agent definition '#{name}' not found.", metadata: { agent: name }) exit(1) end # Check current status current_status = definition[:persistent_status] || 'stopped' if current_status == 'stopped' if json_mode? puts JSON.generate({ status: 'already_stopped', agent: name }) else say "Agent '#{name}' is already stopped.", :yellow end return end # Confirm if not forced (skip in quiet/json mode) if !([:force] || quiet_mode?) && !yes?("Agent '#{name}' is currently marked as '#{current_status}'. Stop it? [y/N]", :yellow) say 'Stop cancelled.', :yellow return end # Update the persistent_status to stopped Legate::GlobalDefinitionRegistry.update_definition(name_sym, { persistent_status: 'stopped' }) if json_mode? puts JSON.generate({ status: 'stopped', agent: name, previous_status: current_status }) else say "Agent '#{name}' has been marked as stopped.", :green say ' Note: If running in a web server, the agent will stop on next status check or server restart.', :cyan end end |