agentic 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.agentic.yml +2 -0
- data/.architecture/decisions/ArchitecturalFeatureBuilder.md +136 -0
- data/.architecture/decisions/ArchitectureConsiderations.md +200 -0
- data/.architecture/decisions/adr_001_observer_pattern_implementation.md +196 -0
- data/.architecture/decisions/adr_002_plan_orchestrator.md +320 -0
- data/.architecture/decisions/adr_003_plan_orchestrator_interface.md +179 -0
- data/.architecture/decisions/adrs/ADR-001-dependency-management.md +147 -0
- data/.architecture/decisions/adrs/ADR-002-system-boundaries.md +162 -0
- data/.architecture/decisions/adrs/ADR-003-content-safety.md +158 -0
- data/.architecture/decisions/adrs/ADR-004-agent-permissions.md +161 -0
- data/.architecture/decisions/adrs/ADR-005-adaptation-engine.md +127 -0
- data/.architecture/decisions/adrs/ADR-006-extension-system.md +273 -0
- data/.architecture/decisions/adrs/ADR-007-learning-system.md +156 -0
- data/.architecture/decisions/adrs/ADR-008-prompt-generation.md +325 -0
- data/.architecture/decisions/adrs/ADR-009-task-failure-handling.md +353 -0
- data/.architecture/decisions/adrs/ADR-010-task-input-handling.md +251 -0
- data/.architecture/decisions/adrs/ADR-011-task-observable-pattern.md +391 -0
- data/.architecture/decisions/adrs/ADR-012-task-output-handling.md +205 -0
- data/.architecture/decisions/adrs/ADR-013-architecture-alignment.md +211 -0
- data/.architecture/decisions/adrs/ADR-014-agent-capability-registry.md +80 -0
- data/.architecture/decisions/adrs/ADR-015-persistent-agent-store.md +100 -0
- data/.architecture/decisions/adrs/ADR-016-agent-assembly-engine.md +117 -0
- data/.architecture/decisions/adrs/ADR-017-streaming-observability.md +171 -0
- data/.architecture/decisions/capability_tools_distinction.md +150 -0
- data/.architecture/decisions/cli_command_structure.md +61 -0
- data/.architecture/implementation/agent_self_assembly_implementation.md +267 -0
- data/.architecture/implementation/agent_self_assembly_summary.md +138 -0
- data/.architecture/members.yml +187 -0
- data/.architecture/planning/self_implementation_exercise.md +295 -0
- data/.architecture/planning/session_compaction_rule.md +43 -0
- data/.architecture/planning/streaming_observability_feature.md +223 -0
- data/.architecture/principles.md +151 -0
- data/.architecture/recalibration/0-2-0.md +92 -0
- data/.architecture/recalibration/agent_self_assembly.md +238 -0
- data/.architecture/recalibration/cli_command_structure.md +91 -0
- data/.architecture/recalibration/implementation_roadmap_0-2-0.md +301 -0
- data/.architecture/recalibration/progress_tracking_0-2-0.md +114 -0
- data/.architecture/recalibration_process.md +127 -0
- data/.architecture/reviews/0-2-0.md +181 -0
- data/.architecture/reviews/cli_command_duplication.md +98 -0
- data/.architecture/templates/adr.md +105 -0
- data/.architecture/templates/implementation_roadmap.md +125 -0
- data/.architecture/templates/progress_tracking.md +89 -0
- data/.architecture/templates/recalibration_plan.md +70 -0
- data/.architecture/templates/version_comparison.md +124 -0
- data/.claude/settings.local.json +13 -0
- data/.claude-sessions/001-task-class-architecture-implementation.md +129 -0
- data/.claude-sessions/002-plan-orchestrator-interface-review.md +105 -0
- data/.claude-sessions/architecture-governance-implementation.md +37 -0
- data/.claude-sessions/architecture-review-session.md +27 -0
- data/ArchitecturalFeatureBuilder.md +136 -0
- data/ArchitectureConsiderations.md +229 -0
- data/CHANGELOG.md +57 -2
- data/CLAUDE.md +111 -0
- data/CONTRIBUTING.md +286 -0
- data/MAINTAINING.md +301 -0
- data/README.md +582 -28
- data/docs/agent_capabilities_api.md +259 -0
- data/docs/artifact_extension_points.md +757 -0
- data/docs/artifact_generation_architecture.md +323 -0
- data/docs/artifact_implementation_plan.md +596 -0
- data/docs/artifact_integration_points.md +345 -0
- data/docs/artifact_verification_strategies.md +581 -0
- data/docs/streaming_observability_architecture.md +510 -0
- data/exe/agentic +6 -1
- data/lefthook.yml +5 -0
- data/lib/agentic/adaptation_engine.rb +124 -0
- data/lib/agentic/agent.rb +181 -4
- data/lib/agentic/agent_assembly_engine.rb +442 -0
- data/lib/agentic/agent_capability_registry.rb +260 -0
- data/lib/agentic/agent_config.rb +63 -0
- data/lib/agentic/agent_specification.rb +46 -0
- data/lib/agentic/capabilities/examples.rb +530 -0
- data/lib/agentic/capabilities.rb +14 -0
- data/lib/agentic/capability_provider.rb +146 -0
- data/lib/agentic/capability_specification.rb +118 -0
- data/lib/agentic/cli/agent.rb +31 -0
- data/lib/agentic/cli/capabilities.rb +191 -0
- data/lib/agentic/cli/config.rb +134 -0
- data/lib/agentic/cli/execution_observer.rb +796 -0
- data/lib/agentic/cli.rb +1068 -0
- data/lib/agentic/default_agent_provider.rb +35 -0
- data/lib/agentic/errors/llm_error.rb +184 -0
- data/lib/agentic/execution_plan.rb +53 -0
- data/lib/agentic/execution_result.rb +91 -0
- data/lib/agentic/expected_answer_format.rb +46 -0
- data/lib/agentic/extension/domain_adapter.rb +109 -0
- data/lib/agentic/extension/plugin_manager.rb +163 -0
- data/lib/agentic/extension/protocol_handler.rb +116 -0
- data/lib/agentic/extension.rb +45 -0
- data/lib/agentic/factory_methods.rb +9 -1
- data/lib/agentic/generation_stats.rb +61 -0
- data/lib/agentic/learning/README.md +84 -0
- data/lib/agentic/learning/capability_optimizer.rb +613 -0
- data/lib/agentic/learning/execution_history_store.rb +251 -0
- data/lib/agentic/learning/pattern_recognizer.rb +500 -0
- data/lib/agentic/learning/strategy_optimizer.rb +706 -0
- data/lib/agentic/learning.rb +131 -0
- data/lib/agentic/llm_assisted_composition_strategy.rb +188 -0
- data/lib/agentic/llm_client.rb +215 -15
- data/lib/agentic/llm_config.rb +65 -1
- data/lib/agentic/llm_response.rb +163 -0
- data/lib/agentic/logger.rb +1 -1
- data/lib/agentic/observable.rb +51 -0
- data/lib/agentic/persistent_agent_store.rb +385 -0
- data/lib/agentic/plan_execution_result.rb +129 -0
- data/lib/agentic/plan_orchestrator.rb +464 -0
- data/lib/agentic/plan_orchestrator_config.rb +57 -0
- data/lib/agentic/retry_config.rb +63 -0
- data/lib/agentic/retry_handler.rb +125 -0
- data/lib/agentic/structured_outputs.rb +1 -1
- data/lib/agentic/task.rb +193 -0
- data/lib/agentic/task_definition.rb +39 -0
- data/lib/agentic/task_execution_result.rb +92 -0
- data/lib/agentic/task_failure.rb +66 -0
- data/lib/agentic/task_output_schemas.rb +112 -0
- data/lib/agentic/task_planner.rb +54 -19
- data/lib/agentic/task_result.rb +48 -0
- data/lib/agentic/ui.rb +244 -0
- data/lib/agentic/verification/critic_framework.rb +116 -0
- data/lib/agentic/verification/llm_verification_strategy.rb +60 -0
- data/lib/agentic/verification/schema_verification_strategy.rb +47 -0
- data/lib/agentic/verification/verification_hub.rb +62 -0
- data/lib/agentic/verification/verification_result.rb +50 -0
- data/lib/agentic/verification/verification_strategy.rb +26 -0
- data/lib/agentic/version.rb +1 -1
- data/lib/agentic.rb +74 -2
- data/plugins/README.md +41 -0
- metadata +245 -6
data/lib/agentic/agent.rb
CHANGED
@@ -1,17 +1,194 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative "llm_client"
|
4
|
+
require_relative "llm_config"
|
5
|
+
|
3
6
|
module Agentic
|
4
7
|
class Agent
|
5
8
|
include FactoryMethods
|
6
9
|
|
7
|
-
configurable :role, :purpose, :backstory, :tools
|
10
|
+
configurable :id, :name, :role, :purpose, :backstory, :instructions, :tools, :capabilities, :llm_client
|
8
11
|
|
9
|
-
|
10
|
-
|
12
|
+
# Initialize with default values
|
13
|
+
def initialize
|
14
|
+
@capabilities = {}
|
15
|
+
@tools = Set.new
|
16
|
+
@llm_client = nil
|
11
17
|
end
|
12
18
|
|
19
|
+
# Executes a task using this agent
|
20
|
+
# @param task [String, Task] The task to execute, either a task object or a prompt
|
21
|
+
# @return [Object] The result of the task execution
|
13
22
|
def execute(task)
|
14
|
-
task.
|
23
|
+
if task.is_a?(String)
|
24
|
+
# Simple string prompt
|
25
|
+
execute_prompt(task)
|
26
|
+
else
|
27
|
+
# Task object
|
28
|
+
task.perform(self)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# Executes a prompt with structured output schema
|
33
|
+
# @param prompt [String] The prompt to execute
|
34
|
+
# @param schema [Agentic::StructuredOutputs::Schema] The output schema
|
35
|
+
# @return [Object] The structured response
|
36
|
+
def execute_with_schema(prompt, schema)
|
37
|
+
# If the agent has a text_generation capability, use it
|
38
|
+
if has_capability?("text_generation")
|
39
|
+
# For now, text_generation capabilities don't support schemas
|
40
|
+
# Fall back to regular execution
|
41
|
+
execute_capability("text_generation", {prompt: prompt})[:response]
|
42
|
+
elsif @llm_client
|
43
|
+
# Use the configured LLM client with structured output
|
44
|
+
response = @llm_client.complete(build_messages(prompt), output_schema: schema)
|
45
|
+
if response.successful?
|
46
|
+
response.content
|
47
|
+
else
|
48
|
+
raise "LLM execution failed: #{response.error.message}"
|
49
|
+
end
|
50
|
+
else
|
51
|
+
# Fallback error - agent not properly configured
|
52
|
+
raise "Agent not configured with LLM capabilities. Use DefaultAgentProvider or configure llm_client directly."
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# Adds a capability to the agent
|
57
|
+
# @param capability_name [String] The name of the capability
|
58
|
+
# @param version [String, nil] The version of the capability, or nil for latest
|
59
|
+
# @return [Boolean] True if the capability was added successfully
|
60
|
+
def add_capability(capability_name, version = nil)
|
61
|
+
# Get the capability from the registry
|
62
|
+
registry = AgentCapabilityRegistry.instance
|
63
|
+
capability = registry.get(capability_name, version)
|
64
|
+
raise "Capability not found: #{capability_name}" unless capability
|
65
|
+
|
66
|
+
# Get the provider
|
67
|
+
provider = registry.get_provider(capability_name, version)
|
68
|
+
raise "Provider not found for capability: #{capability_name}" unless provider
|
69
|
+
|
70
|
+
# Add to the agent's capabilities
|
71
|
+
@capabilities[capability_name] = {
|
72
|
+
specification: capability,
|
73
|
+
provider: provider
|
74
|
+
}
|
75
|
+
|
76
|
+
# Also add to tools for backward compatibility
|
77
|
+
@tools.add(capability_name.to_sym)
|
78
|
+
|
79
|
+
true
|
80
|
+
end
|
81
|
+
|
82
|
+
# Checks if the agent has a capability
|
83
|
+
# @param capability_name [String] The name of the capability
|
84
|
+
# @return [Boolean] True if the agent has the capability
|
85
|
+
def has_capability?(capability_name)
|
86
|
+
@capabilities.key?(capability_name)
|
87
|
+
end
|
88
|
+
|
89
|
+
# Gets the specification for a capability
|
90
|
+
# @param capability_name [String] The name of the capability
|
91
|
+
# @return [CapabilitySpecification, nil] The capability specification or nil if not found
|
92
|
+
def capability_specification(capability_name)
|
93
|
+
return nil unless @capabilities[capability_name]
|
94
|
+
@capabilities[capability_name][:specification]
|
95
|
+
end
|
96
|
+
|
97
|
+
# Executes a capability
|
98
|
+
# @param capability_name [String] The name of the capability
|
99
|
+
# @param inputs [Hash] The inputs for the capability
|
100
|
+
# @return [Hash] The outputs from the capability
|
101
|
+
def execute_capability(capability_name, inputs = {})
|
102
|
+
raise "Capability not available: #{capability_name}" unless @capabilities.key?(capability_name)
|
103
|
+
|
104
|
+
# Get the provider
|
105
|
+
provider = @capabilities[capability_name][:provider]
|
106
|
+
|
107
|
+
# Execute the capability
|
108
|
+
provider.execute(inputs)
|
109
|
+
end
|
110
|
+
|
111
|
+
# Converts the agent to a hash representation
|
112
|
+
# @return [Hash] The hash representation
|
113
|
+
def to_h
|
114
|
+
{
|
115
|
+
role: @role,
|
116
|
+
purpose: @purpose,
|
117
|
+
backstory: @backstory,
|
118
|
+
capability_names: @capabilities.keys
|
119
|
+
}
|
120
|
+
end
|
121
|
+
|
122
|
+
# Creates an agent from a hash representation
|
123
|
+
# @param hash [Hash] The hash representation
|
124
|
+
# @return [Agent] The agent
|
125
|
+
def self.from_h(hash)
|
126
|
+
new do |a|
|
127
|
+
a.role = hash[:role] || hash["role"]
|
128
|
+
a.purpose = hash[:purpose] || hash["purpose"]
|
129
|
+
a.backstory = hash[:backstory] || hash["backstory"]
|
130
|
+
end
|
131
|
+
|
132
|
+
# Note: Capabilities need to be added separately after creation
|
133
|
+
# since they require the registry to be available
|
134
|
+
end
|
135
|
+
|
136
|
+
private
|
137
|
+
|
138
|
+
# Executes a simple string prompt
|
139
|
+
# @param prompt [String] The prompt to execute
|
140
|
+
# @return [String] The response
|
141
|
+
def execute_prompt(prompt)
|
142
|
+
# If the agent has a text_generation capability, use it
|
143
|
+
if has_capability?("text_generation")
|
144
|
+
execute_capability("text_generation", {prompt: prompt})[:response]
|
145
|
+
elsif @llm_client
|
146
|
+
# Use the configured LLM client
|
147
|
+
response = @llm_client.complete(build_messages(prompt))
|
148
|
+
if response.successful?
|
149
|
+
response.content
|
150
|
+
else
|
151
|
+
raise "LLM execution failed: #{response.error.message}"
|
152
|
+
end
|
153
|
+
else
|
154
|
+
# Fallback error - agent not properly configured
|
155
|
+
raise "Agent not configured with LLM capabilities. Use DefaultAgentProvider or configure llm_client directly."
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
# Builds messages array for LLM completion
|
160
|
+
# @param prompt [String] The user prompt
|
161
|
+
# @return [Array<Hash>] The messages array
|
162
|
+
def build_messages(prompt)
|
163
|
+
messages = []
|
164
|
+
|
165
|
+
# Add system message with agent context
|
166
|
+
system_content = build_system_message
|
167
|
+
messages << {role: "system", content: system_content} if system_content && !system_content.empty?
|
168
|
+
|
169
|
+
# Add user prompt
|
170
|
+
messages << {role: "user", content: prompt}
|
171
|
+
|
172
|
+
messages
|
173
|
+
end
|
174
|
+
|
175
|
+
# Builds system message with agent personality and instructions
|
176
|
+
# @return [String] The system message content
|
177
|
+
def build_system_message
|
178
|
+
parts = []
|
179
|
+
|
180
|
+
parts << "You are #{@role}" if @role && !@role.empty?
|
181
|
+
parts << "Purpose: #{@purpose}" if @purpose && !@purpose.empty?
|
182
|
+
parts << "Background: #{@backstory}" if @backstory && !@backstory.empty?
|
183
|
+
parts << "Instructions: #{@instructions}" if @instructions && !@instructions.empty?
|
184
|
+
|
185
|
+
# Add capability information
|
186
|
+
if @capabilities && !@capabilities.empty?
|
187
|
+
capabilities_list = @capabilities.keys.join(", ")
|
188
|
+
parts << "Available capabilities: #{capabilities_list}"
|
189
|
+
end
|
190
|
+
|
191
|
+
parts.join("\n\n")
|
15
192
|
end
|
16
193
|
end
|
17
194
|
end
|
@@ -0,0 +1,442 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Agentic
|
4
|
+
# Engine for assembling agents based on task requirements
|
5
|
+
# @attr_reader [AgentCapabilityRegistry] registry The capability registry
|
6
|
+
# @attr_reader [PersistentAgentStore, nil] agent_store The agent store for persistence
|
7
|
+
class AgentAssemblyEngine
|
8
|
+
attr_reader :registry, :agent_store
|
9
|
+
|
10
|
+
# Initialize a new agent assembly engine
|
11
|
+
# @param registry [AgentCapabilityRegistry] The capability registry to use
|
12
|
+
# @param agent_store [PersistentAgentStore, nil] The agent store for persistence
|
13
|
+
def initialize(registry = AgentCapabilityRegistry.instance, agent_store = nil)
|
14
|
+
@registry = registry
|
15
|
+
@agent_store = agent_store
|
16
|
+
end
|
17
|
+
|
18
|
+
# Assemble an agent for a task
|
19
|
+
# @param task [Task] The task to assemble an agent for
|
20
|
+
# @param strategy [AgentCompositionStrategy, nil] The strategy to use for assembly
|
21
|
+
# @param store [Boolean] Whether to use the agent store for finding and storing agents
|
22
|
+
# @return [Agent] The assembled agent
|
23
|
+
def assemble_agent(task, strategy: nil, store: true)
|
24
|
+
# Check if we should try to find an existing agent in the store
|
25
|
+
if store && @agent_store
|
26
|
+
existing_agent = find_suitable_agent(task)
|
27
|
+
if existing_agent
|
28
|
+
Agentic.logger.info("Using existing agent from store for task: #{task.id}")
|
29
|
+
return existing_agent
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# Use the default strategy if none provided
|
34
|
+
strategy ||= DefaultCompositionStrategy.new
|
35
|
+
|
36
|
+
# Analyze task requirements
|
37
|
+
requirements = analyze_requirements(task)
|
38
|
+
|
39
|
+
# Select capabilities based on requirements
|
40
|
+
capabilities = select_capabilities(requirements, strategy)
|
41
|
+
|
42
|
+
# Create a new agent with the selected capabilities
|
43
|
+
agent = build_agent(task, capabilities)
|
44
|
+
|
45
|
+
# Store the assembled agent if requested
|
46
|
+
if store && @agent_store
|
47
|
+
store_agent(agent, task, requirements)
|
48
|
+
end
|
49
|
+
|
50
|
+
# Return the assembled agent
|
51
|
+
agent
|
52
|
+
end
|
53
|
+
|
54
|
+
# Analyze task requirements to determine needed capabilities
|
55
|
+
# @param task [Task] The task to analyze
|
56
|
+
# @return [Hash] The capability requirements
|
57
|
+
def analyze_requirements(task)
|
58
|
+
# Extract requirements from the task description and agent specification
|
59
|
+
requirements = {}
|
60
|
+
|
61
|
+
# Use the task description to infer capabilities
|
62
|
+
infer_capabilities_from_description(task.description, requirements)
|
63
|
+
|
64
|
+
# Use the agent specification to infer capabilities
|
65
|
+
if task.agent_spec
|
66
|
+
infer_capabilities_from_agent_spec(task.agent_spec, requirements)
|
67
|
+
end
|
68
|
+
|
69
|
+
# Use the task input to infer capabilities
|
70
|
+
if task.input && !task.input.empty?
|
71
|
+
infer_capabilities_from_input(task.input, requirements)
|
72
|
+
end
|
73
|
+
|
74
|
+
requirements
|
75
|
+
end
|
76
|
+
|
77
|
+
# Select capabilities based on requirements
|
78
|
+
# @param requirements [Hash] The capability requirements
|
79
|
+
# @param strategy [AgentCompositionStrategy] The strategy to use for selection
|
80
|
+
# @return [Array<Hash>] The selected capabilities
|
81
|
+
def select_capabilities(requirements, strategy)
|
82
|
+
# Use the strategy to select capabilities
|
83
|
+
strategy.select_capabilities(requirements, @registry)
|
84
|
+
end
|
85
|
+
|
86
|
+
# Build an agent with the selected capabilities
|
87
|
+
# @param task [Task] The task the agent will perform
|
88
|
+
# @param capabilities [Array<Hash>] The selected capabilities
|
89
|
+
# @return [Agent] The assembled agent
|
90
|
+
def build_agent(task, capabilities)
|
91
|
+
# Create a new agent
|
92
|
+
agent = Agent.build do |a|
|
93
|
+
# Set basic agent properties from the task agent spec
|
94
|
+
if task.agent_spec
|
95
|
+
a.role = task.agent_spec.name
|
96
|
+
a.purpose = task.agent_spec.description
|
97
|
+
a.backstory = task.agent_spec.instructions
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
# Add capabilities to the agent
|
102
|
+
capabilities.each do |capability_info|
|
103
|
+
agent.add_capability(capability_info[:name], capability_info[:version])
|
104
|
+
rescue => e
|
105
|
+
Agentic.logger.warn("Failed to add capability: #{capability_info[:name]} v#{capability_info[:version]} - #{e.message}")
|
106
|
+
end
|
107
|
+
|
108
|
+
agent
|
109
|
+
end
|
110
|
+
|
111
|
+
# Find a suitable agent in the store for a task
|
112
|
+
# @param task [Task] The task to find an agent for
|
113
|
+
# @return [Agent, nil] A suitable agent or nil if none found
|
114
|
+
def find_suitable_agent(task)
|
115
|
+
return nil unless @agent_store
|
116
|
+
|
117
|
+
# Analyze task requirements
|
118
|
+
requirements = analyze_requirements(task)
|
119
|
+
|
120
|
+
# Get required capabilities
|
121
|
+
required_capabilities = requirements.keys
|
122
|
+
return nil if required_capabilities.empty?
|
123
|
+
|
124
|
+
# Find agents with matching capabilities
|
125
|
+
matching_agents = []
|
126
|
+
|
127
|
+
# Start with the most important capabilities
|
128
|
+
primary_capabilities = requirements.select { |_, info| info[:importance] >= 0.8 }.keys
|
129
|
+
return nil if primary_capabilities.empty?
|
130
|
+
|
131
|
+
# Find agents with the primary capabilities
|
132
|
+
primary_capabilities.each do |capability|
|
133
|
+
# Find agents with this capability
|
134
|
+
agents = @agent_store.all(capability: capability)
|
135
|
+
|
136
|
+
# Add to matching agents
|
137
|
+
matching_agents.concat(agents)
|
138
|
+
end
|
139
|
+
|
140
|
+
# Return nil if no matching agents found
|
141
|
+
return nil if matching_agents.empty?
|
142
|
+
|
143
|
+
# Score each agent based on how well it matches the requirements
|
144
|
+
scored_agents = matching_agents.map do |agent_config|
|
145
|
+
score = calculate_agent_match_score(agent_config, requirements)
|
146
|
+
{config: agent_config, score: score}
|
147
|
+
end
|
148
|
+
|
149
|
+
# Sort by score (highest first)
|
150
|
+
scored_agents.sort_by! { |a| -a[:score] }
|
151
|
+
|
152
|
+
# Get the best match
|
153
|
+
best_match = scored_agents.first
|
154
|
+
|
155
|
+
# If the best match has a score below threshold, don't use it
|
156
|
+
return nil if best_match[:score] < 0.5
|
157
|
+
|
158
|
+
# Build the agent from the stored configuration
|
159
|
+
@agent_store.build_agent(best_match[:config][:id])
|
160
|
+
end
|
161
|
+
|
162
|
+
# Store an assembled agent in the persistent store
|
163
|
+
# @param agent [Agent] The agent to store
|
164
|
+
# @param task [Task] The task the agent was assembled for
|
165
|
+
# @param requirements [Hash] The capability requirements
|
166
|
+
# @return [String, nil] The ID of the stored agent or nil if not stored
|
167
|
+
def store_agent(agent, task, requirements)
|
168
|
+
return nil unless @agent_store
|
169
|
+
|
170
|
+
# Generate a name for the agent based on the task
|
171
|
+
name = generate_agent_name(task)
|
172
|
+
|
173
|
+
# Generate metadata for the agent
|
174
|
+
metadata = {
|
175
|
+
task_id: task.id,
|
176
|
+
task_description: task.description,
|
177
|
+
assembled_at: Time.now.iso8601,
|
178
|
+
requirements: requirements,
|
179
|
+
assembly_engine_version: "1.0.0" # Add version tracking
|
180
|
+
}
|
181
|
+
|
182
|
+
# Store the agent
|
183
|
+
@agent_store.store(agent, name: name, metadata: metadata)
|
184
|
+
end
|
185
|
+
|
186
|
+
private
|
187
|
+
|
188
|
+
# Calculate a score for how well an agent matches requirements
|
189
|
+
# @param agent_config [Hash] The agent configuration
|
190
|
+
# @param requirements [Hash] The capability requirements
|
191
|
+
# @return [Float] The match score (0.0 to 1.0)
|
192
|
+
def calculate_agent_match_score(agent_config, requirements)
|
193
|
+
# Start with a base score
|
194
|
+
score = 0.0
|
195
|
+
max_score = 0.0
|
196
|
+
|
197
|
+
# Get the agent's capabilities
|
198
|
+
agent_capabilities = agent_config[:capabilities].map { |c| c[:name] }
|
199
|
+
|
200
|
+
# Score each requirement
|
201
|
+
requirements.each do |capability_name, info|
|
202
|
+
# Add the importance to the max possible score
|
203
|
+
max_score += info[:importance]
|
204
|
+
|
205
|
+
# If the agent has the capability, add its importance to the score
|
206
|
+
if agent_capabilities.include?(capability_name)
|
207
|
+
score += info[:importance]
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
# Normalize the score (0.0 to 1.0)
|
212
|
+
(max_score > 0) ? score / max_score : 0.0
|
213
|
+
end
|
214
|
+
|
215
|
+
# Generate a name for an agent based on a task
|
216
|
+
# @param task [Task] The task the agent is for
|
217
|
+
# @return [String] A name for the agent
|
218
|
+
def generate_agent_name(task)
|
219
|
+
# Extract key words from the task description
|
220
|
+
words = task.description.downcase.scan(/\b[a-z]{3,}\b/).first(3)
|
221
|
+
|
222
|
+
# Get the task type from the agent spec if available
|
223
|
+
task_type = task.agent_spec ? task.agent_spec.name.downcase.gsub(/\s+/, "_") : "agent"
|
224
|
+
|
225
|
+
# Generate a name with task type and words
|
226
|
+
if words.empty?
|
227
|
+
"#{task_type}_#{SecureRandom.hex(4)}"
|
228
|
+
else
|
229
|
+
"#{task_type}_#{words.join("_")}"
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
# Infer capabilities from a task description
|
234
|
+
# @param description [String] The task description
|
235
|
+
# @param requirements [Hash] The requirements hash to update
|
236
|
+
# @return [void]
|
237
|
+
def infer_capabilities_from_description(description, requirements)
|
238
|
+
# Extract capability names from description using simple pattern matching
|
239
|
+
# This is a basic implementation that should be enhanced with NLP or LLM-based analysis
|
240
|
+
|
241
|
+
# Example: Look for known capability keywords
|
242
|
+
known_capabilities = [
|
243
|
+
"document_analysis", "structure_extraction", "text_generation",
|
244
|
+
"web_search", "data_analysis", "code_generation", "uml_generation",
|
245
|
+
"dependency_analysis", "testing", "performance_analysis", "code_metrics"
|
246
|
+
]
|
247
|
+
|
248
|
+
known_capabilities.each do |capability|
|
249
|
+
if description.downcase.include?(capability.downcase)
|
250
|
+
requirements[capability] ||= {
|
251
|
+
importance: 0.5, # Default importance
|
252
|
+
version_constraint: nil # Any version
|
253
|
+
}
|
254
|
+
|
255
|
+
# Increase importance if mentioned multiple times
|
256
|
+
count = description.downcase.scan(capability.downcase).count
|
257
|
+
requirements[capability][:importance] += 0.1 * count if count > 1
|
258
|
+
end
|
259
|
+
end
|
260
|
+
|
261
|
+
# Special case for test tasks
|
262
|
+
if description.downcase.include?("code") && description.downcase.include?("generate")
|
263
|
+
requirements["code_generation"] ||= {
|
264
|
+
importance: 0.8, # High importance for code generation tasks
|
265
|
+
version_constraint: nil # Any version
|
266
|
+
}
|
267
|
+
end
|
268
|
+
|
269
|
+
# Add common capabilities for all tasks
|
270
|
+
requirements["text_generation"] ||= {
|
271
|
+
importance: 0.3, # Lower importance as it's a default capability
|
272
|
+
version_constraint: nil
|
273
|
+
}
|
274
|
+
end
|
275
|
+
|
276
|
+
# Infer capabilities from an agent specification
|
277
|
+
# @param agent_spec [AgentSpecification] The agent specification
|
278
|
+
# @param requirements [Hash] The requirements hash to update
|
279
|
+
# @return [void]
|
280
|
+
def infer_capabilities_from_agent_spec(agent_spec, requirements)
|
281
|
+
# Extract capabilities from agent name
|
282
|
+
infer_capabilities_from_description(agent_spec.name, requirements)
|
283
|
+
|
284
|
+
# Extract capabilities from agent description
|
285
|
+
if agent_spec.description
|
286
|
+
infer_capabilities_from_description(agent_spec.description, requirements)
|
287
|
+
end
|
288
|
+
|
289
|
+
# Extract capabilities from agent instructions
|
290
|
+
if agent_spec.instructions
|
291
|
+
infer_capabilities_from_description(agent_spec.instructions, requirements)
|
292
|
+
end
|
293
|
+
|
294
|
+
# If the agent spec mentions tools explicitly, prioritize those
|
295
|
+
if agent_spec.respond_to?(:tools) && agent_spec.tools
|
296
|
+
agent_spec.tools.each do |tool|
|
297
|
+
tool_name = tool.to_s
|
298
|
+
requirements[tool_name] ||= {
|
299
|
+
importance: 0.8, # High importance for explicitly mentioned tools
|
300
|
+
version_constraint: nil # Any version
|
301
|
+
}
|
302
|
+
end
|
303
|
+
end
|
304
|
+
|
305
|
+
# If the agent spec mentions capabilities explicitly, prioritize those
|
306
|
+
if agent_spec.respond_to?(:capabilities) && agent_spec.capabilities
|
307
|
+
agent_spec.capabilities.each do |capability|
|
308
|
+
capability_name = capability.to_s
|
309
|
+
requirements[capability_name] ||= {
|
310
|
+
importance: 0.9, # Very high importance for explicitly mentioned capabilities
|
311
|
+
version_constraint: nil # Any version
|
312
|
+
}
|
313
|
+
end
|
314
|
+
end
|
315
|
+
end
|
316
|
+
|
317
|
+
# Infer capabilities from task input
|
318
|
+
# @param input [Hash] The task input
|
319
|
+
# @param requirements [Hash] The requirements hash to update
|
320
|
+
# @return [void]
|
321
|
+
def infer_capabilities_from_input(input, requirements)
|
322
|
+
# Check if input contains capability requirements directly
|
323
|
+
if input[:capabilities] || input["capabilities"]
|
324
|
+
capabilities = input[:capabilities] || input["capabilities"]
|
325
|
+
|
326
|
+
if capabilities.is_a?(Array)
|
327
|
+
capabilities.each do |capability|
|
328
|
+
if capability.is_a?(String)
|
329
|
+
requirements[capability] ||= {
|
330
|
+
importance: 0.9, # Very high importance for explicitly requested capabilities
|
331
|
+
version_constraint: nil # Any version
|
332
|
+
}
|
333
|
+
elsif capability.is_a?(Hash)
|
334
|
+
name = capability[:name] || capability["name"]
|
335
|
+
version = capability[:version] || capability["version"]
|
336
|
+
importance = capability[:importance] || capability["importance"] || 0.9
|
337
|
+
|
338
|
+
if name
|
339
|
+
requirements[name] ||= {
|
340
|
+
importance: importance,
|
341
|
+
version_constraint: version
|
342
|
+
}
|
343
|
+
end
|
344
|
+
end
|
345
|
+
end
|
346
|
+
end
|
347
|
+
end
|
348
|
+
|
349
|
+
# Analyze input keys for capability hints
|
350
|
+
input.each do |key, value|
|
351
|
+
key_str = key.to_s
|
352
|
+
|
353
|
+
# Check if key suggests a capability
|
354
|
+
known_capability_indicators = [
|
355
|
+
"analyze", "generate", "search", "extract", "compute",
|
356
|
+
"test", "verify", "optimize", "format", "translate"
|
357
|
+
]
|
358
|
+
|
359
|
+
known_capability_indicators.each do |indicator|
|
360
|
+
if key_str.include?(indicator)
|
361
|
+
capability_name = "#{indicator}_#{key_str.sub(indicator, "")}"
|
362
|
+
requirements[capability_name] ||= {
|
363
|
+
importance: 0.6, # Medium importance for inferred capabilities
|
364
|
+
version_constraint: nil # Any version
|
365
|
+
}
|
366
|
+
end
|
367
|
+
end
|
368
|
+
end
|
369
|
+
end
|
370
|
+
end
|
371
|
+
|
372
|
+
# Base class for agent composition strategies
|
373
|
+
class AgentCompositionStrategy
|
374
|
+
# Select capabilities based on requirements
|
375
|
+
# @param requirements [Hash] The capability requirements
|
376
|
+
# @param registry [AgentCapabilityRegistry] The capability registry
|
377
|
+
# @return [Array<Hash>] The selected capabilities
|
378
|
+
def select_capabilities(requirements, registry)
|
379
|
+
raise NotImplementedError, "Subclasses must implement select_capabilities"
|
380
|
+
end
|
381
|
+
end
|
382
|
+
|
383
|
+
# Default strategy for agent composition
|
384
|
+
class DefaultCompositionStrategy < AgentCompositionStrategy
|
385
|
+
# Select capabilities based on requirements
|
386
|
+
# @param requirements [Hash] The capability requirements
|
387
|
+
# @param registry [AgentCapabilityRegistry] The capability registry
|
388
|
+
# @return [Array<Hash>] The selected capabilities
|
389
|
+
def select_capabilities(requirements, registry)
|
390
|
+
selected = []
|
391
|
+
|
392
|
+
# Sort requirements by importance (highest first)
|
393
|
+
sorted_requirements = requirements.sort_by { |_, info| -info[:importance] }
|
394
|
+
|
395
|
+
# Select capabilities for each requirement
|
396
|
+
sorted_requirements.each do |name, info|
|
397
|
+
# Find the capability in the registry
|
398
|
+
capability = registry.get(name, info[:version_constraint])
|
399
|
+
|
400
|
+
# Skip if not found
|
401
|
+
next unless capability
|
402
|
+
|
403
|
+
# Add to selected capabilities
|
404
|
+
selected << {
|
405
|
+
name: capability.name,
|
406
|
+
version: capability.version
|
407
|
+
}
|
408
|
+
|
409
|
+
# Add dependencies
|
410
|
+
capability.dependencies.each do |dep|
|
411
|
+
# Check if we already selected this dependency
|
412
|
+
next if selected.any? { |sel| sel[:name] == dep[:name] }
|
413
|
+
|
414
|
+
# Find the dependency in the registry
|
415
|
+
dep_capability = registry.get(dep[:name], dep[:version])
|
416
|
+
|
417
|
+
# Skip if not found
|
418
|
+
next unless dep_capability
|
419
|
+
|
420
|
+
# Add to selected capabilities
|
421
|
+
selected << {
|
422
|
+
name: dep_capability.name,
|
423
|
+
version: dep_capability.version
|
424
|
+
}
|
425
|
+
end
|
426
|
+
end
|
427
|
+
|
428
|
+
# If no capabilities were selected, add a default text generation capability
|
429
|
+
if selected.empty?
|
430
|
+
default_capability = registry.get("text_generation")
|
431
|
+
if default_capability
|
432
|
+
selected << {
|
433
|
+
name: default_capability.name,
|
434
|
+
version: default_capability.version
|
435
|
+
}
|
436
|
+
end
|
437
|
+
end
|
438
|
+
|
439
|
+
selected
|
440
|
+
end
|
441
|
+
end
|
442
|
+
end
|