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
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "llm_client"
|
4
|
+
require_relative "llm_config"
|
5
|
+
|
6
|
+
module Agentic
|
7
|
+
# Default implementation of an agent provider for use in the CLI
|
8
|
+
# This provider creates agents based on agent specs in tasks
|
9
|
+
class DefaultAgentProvider
|
10
|
+
# Initialize with optional LLM configuration
|
11
|
+
# @param llm_config [LlmConfig, nil] Configuration for the LLM client
|
12
|
+
def initialize(llm_config = nil)
|
13
|
+
@llm_config = llm_config || LlmConfig.new
|
14
|
+
end
|
15
|
+
|
16
|
+
# Creates and returns an agent for a task
|
17
|
+
# @param task [Task] The task that needs an agent
|
18
|
+
# @return [Agent] The agent created for the task
|
19
|
+
def get_agent_for_task(task)
|
20
|
+
agent_spec = task.agent_spec
|
21
|
+
|
22
|
+
# Create LLM client for this agent
|
23
|
+
llm_client = LlmClient.new(@llm_config)
|
24
|
+
|
25
|
+
# Create a new agent using the factory methods
|
26
|
+
Agent.build do |a|
|
27
|
+
a.name = agent_spec.name
|
28
|
+
a.role = agent_spec.name # Use name as role for simplicity
|
29
|
+
a.purpose = agent_spec.description
|
30
|
+
a.instructions = agent_spec.instructions
|
31
|
+
a.llm_client = llm_client
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,184 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Agentic
|
4
|
+
module Errors
|
5
|
+
# Base class for all LLM-related errors
|
6
|
+
class LlmError < StandardError
|
7
|
+
# @return [Hash, nil] The raw response from the LLM API, if available
|
8
|
+
attr_reader :response
|
9
|
+
|
10
|
+
# @return [Hash, nil] Additional context about the error
|
11
|
+
attr_reader :context
|
12
|
+
|
13
|
+
# @param message [String] The error message
|
14
|
+
# @param response [Hash, nil] The raw response from the LLM API
|
15
|
+
# @param context [Hash, nil] Additional context about the error
|
16
|
+
def initialize(message, response: nil, context: nil)
|
17
|
+
super(message)
|
18
|
+
@response = response
|
19
|
+
@context = context || {}
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# Error raised when the LLM refuses to respond
|
24
|
+
class LlmRefusalError < LlmError
|
25
|
+
# @return [String] The refusal message from the LLM
|
26
|
+
attr_reader :refusal_message
|
27
|
+
|
28
|
+
# @return [Symbol] The category of refusal
|
29
|
+
attr_reader :refusal_category
|
30
|
+
|
31
|
+
# @param refusal_message [String] The refusal message from the LLM
|
32
|
+
# @param refusal_category [Symbol, nil] The category of refusal
|
33
|
+
# @param response [Hash, nil] The raw response from the LLM API
|
34
|
+
# @param context [Hash, nil] Additional context about the error
|
35
|
+
def initialize(refusal_message, refusal_category: nil, response: nil, context: nil)
|
36
|
+
super("LLM refused to respond: #{refusal_message}", response: response, context: context)
|
37
|
+
@refusal_message = refusal_message
|
38
|
+
@refusal_category = refusal_category || determine_refusal_category(refusal_message)
|
39
|
+
end
|
40
|
+
|
41
|
+
# Determines whether this refusal is retryable with modifications
|
42
|
+
# @return [Boolean] True if the refusal can be retried with modifications
|
43
|
+
def retryable_with_modifications?
|
44
|
+
[:unclear_instructions, :needs_clarification, :ambiguous_request, :format_error].include?(@refusal_category)
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
# Determines the category of refusal from the message
|
50
|
+
# @param message [String] The refusal message
|
51
|
+
# @return [Symbol] The category of refusal
|
52
|
+
def determine_refusal_category(message)
|
53
|
+
message = message.to_s.downcase
|
54
|
+
|
55
|
+
if message.include?("harmful") || message.include?("offensive") || message.include?("illegal")
|
56
|
+
:harmful_content
|
57
|
+
elsif message.include?("clarif") || message.include?("ambiguous")
|
58
|
+
:needs_clarification
|
59
|
+
elsif message.include?("format") || message.include?("structure")
|
60
|
+
:format_error
|
61
|
+
elsif message.include?("unclear") || message.include?("specific")
|
62
|
+
:unclear_instructions
|
63
|
+
elsif message.include?("capability") || message.include?("unable")
|
64
|
+
:capability_limitation
|
65
|
+
else
|
66
|
+
:general_refusal
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# Error raised when the LLM response cannot be parsed
|
72
|
+
class LlmParseError < LlmError
|
73
|
+
# @return [Exception] The original parsing exception
|
74
|
+
attr_reader :parse_exception
|
75
|
+
|
76
|
+
# @param message [String] The error message
|
77
|
+
# @param parse_exception [Exception] The original parsing exception
|
78
|
+
# @param response [Hash, nil] The raw response from the LLM API
|
79
|
+
# @param context [Hash, nil] Additional context about the error
|
80
|
+
def initialize(message, parse_exception: nil, response: nil, context: nil)
|
81
|
+
super(message, response: response, context: context)
|
82
|
+
@parse_exception = parse_exception
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
# Error raised when there's a connection or network issue
|
87
|
+
class LlmNetworkError < LlmError
|
88
|
+
# @return [Exception] The original network exception
|
89
|
+
attr_reader :network_exception
|
90
|
+
|
91
|
+
# @param message [String] The error message
|
92
|
+
# @param network_exception [Exception] The original network exception
|
93
|
+
# @param context [Hash, nil] Additional context about the error
|
94
|
+
def initialize(message, network_exception: nil, context: nil)
|
95
|
+
super(message, context: context)
|
96
|
+
@network_exception = network_exception
|
97
|
+
end
|
98
|
+
|
99
|
+
# @return [Boolean] Whether this error is retryable
|
100
|
+
def retryable?
|
101
|
+
true
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
# Error raised when the API returns a rate limit error
|
106
|
+
class LlmRateLimitError < LlmError
|
107
|
+
# @return [Integer, nil] The number of seconds to wait before retrying
|
108
|
+
attr_reader :retry_after
|
109
|
+
|
110
|
+
# @param message [String] The error message
|
111
|
+
# @param retry_after [Integer, nil] The number of seconds to wait before retrying
|
112
|
+
# @param response [Hash, nil] The raw response from the LLM API
|
113
|
+
# @param context [Hash, nil] Additional context about the error
|
114
|
+
def initialize(message, retry_after: nil, response: nil, context: nil)
|
115
|
+
super(message, response: response, context: context)
|
116
|
+
@retry_after = retry_after
|
117
|
+
end
|
118
|
+
|
119
|
+
# @return [Boolean] Whether this error is retryable
|
120
|
+
def retryable?
|
121
|
+
true
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
# Error raised when the API returns an authentication error
|
126
|
+
class LlmAuthenticationError < LlmError
|
127
|
+
# @param message [String] The error message
|
128
|
+
# @param response [Hash, nil] The raw response from the LLM API
|
129
|
+
# @param context [Hash, nil] Additional context about the error
|
130
|
+
def initialize(message, response: nil, context: nil)
|
131
|
+
super(message, response: response, context: context)
|
132
|
+
end
|
133
|
+
|
134
|
+
# @return [Boolean] Whether this error is retryable
|
135
|
+
def retryable?
|
136
|
+
false
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
# Error raised when the API returns a server error
|
141
|
+
class LlmServerError < LlmError
|
142
|
+
# @param message [String] The error message
|
143
|
+
# @param response [Hash, nil] The raw response from the LLM API
|
144
|
+
# @param context [Hash, nil] Additional context about the error
|
145
|
+
def initialize(message, response: nil, context: nil)
|
146
|
+
super(message, response: response, context: context)
|
147
|
+
end
|
148
|
+
|
149
|
+
# @return [Boolean] Whether this error is retryable
|
150
|
+
def retryable?
|
151
|
+
true
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
# Error raised when the request to the LLM times out
|
156
|
+
class LlmTimeoutError < LlmError
|
157
|
+
# @param message [String] The error message
|
158
|
+
# @param context [Hash, nil] Additional context about the error
|
159
|
+
def initialize(message, context: nil)
|
160
|
+
super(message, context: context)
|
161
|
+
end
|
162
|
+
|
163
|
+
# @return [Boolean] Whether this error is retryable
|
164
|
+
def retryable?
|
165
|
+
true
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
# Error raised when an invalid request is made to the LLM API
|
170
|
+
class LlmInvalidRequestError < LlmError
|
171
|
+
# @param message [String] The error message
|
172
|
+
# @param response [Hash, nil] The raw response from the LLM API
|
173
|
+
# @param context [Hash, nil] Additional context about the error
|
174
|
+
def initialize(message, response: nil, context: nil)
|
175
|
+
super(message, response: response, context: context)
|
176
|
+
end
|
177
|
+
|
178
|
+
# @return [Boolean] Whether this error is retryable
|
179
|
+
def retryable?
|
180
|
+
false
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Agentic
|
4
|
+
# Value object representing an execution plan with tasks and expected answer
|
5
|
+
#
|
6
|
+
# This class is part of the data-presentation separation pattern:
|
7
|
+
# 1. TaskPlanner generates the core plan data
|
8
|
+
# 2. ExecutionPlan serves as a structured value object to hold this data
|
9
|
+
# 3. The to_s method provides presentation capabilities when needed
|
10
|
+
#
|
11
|
+
# Using a value object instead of raw hashes provides:
|
12
|
+
# - Type safety
|
13
|
+
# - Domain-specific methods
|
14
|
+
# - Encapsulation of presentation logic
|
15
|
+
# - Clearer interfaces between components
|
16
|
+
class ExecutionPlan
|
17
|
+
# @return [Array<TaskDefinition>] The list of tasks to accomplish the goal
|
18
|
+
attr_reader :tasks
|
19
|
+
|
20
|
+
# @return [ExpectedAnswerFormat] The expected answer format
|
21
|
+
attr_reader :expected_answer
|
22
|
+
|
23
|
+
# @param tasks [Array<TaskDefinition>] The list of tasks to accomplish the goal
|
24
|
+
# @param expected_answer [ExpectedAnswerFormat] The expected answer format
|
25
|
+
def initialize(tasks, expected_answer)
|
26
|
+
@tasks = tasks
|
27
|
+
@expected_answer = expected_answer
|
28
|
+
end
|
29
|
+
|
30
|
+
# Returns a hash representation of the execution plan
|
31
|
+
# @return [Hash] The execution plan as a hash
|
32
|
+
def to_h
|
33
|
+
{
|
34
|
+
tasks: @tasks.map(&:to_h),
|
35
|
+
expected_answer: @expected_answer.to_h
|
36
|
+
}
|
37
|
+
end
|
38
|
+
|
39
|
+
# Returns a formatted string representation of the execution plan
|
40
|
+
# @return [String] The formatted execution plan
|
41
|
+
def to_s
|
42
|
+
plan = "Execution Plan:\n\n"
|
43
|
+
@tasks.each_with_index do |task, index|
|
44
|
+
plan += "#{index + 1}. #{task.description} (Agent: #{task.agent.name})\n"
|
45
|
+
end
|
46
|
+
plan += "\nExpected Answer:\n"
|
47
|
+
plan += "Format: #{@expected_answer.format}\n"
|
48
|
+
plan += "Sections: #{@expected_answer.sections.join(", ")}\n"
|
49
|
+
plan += "Length: #{@expected_answer.length}\n"
|
50
|
+
plan
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Agentic
|
4
|
+
# Value object representing the results of plan execution
|
5
|
+
class ExecutionResult
|
6
|
+
# @return [String] The ID of the plan
|
7
|
+
attr_reader :plan_id
|
8
|
+
|
9
|
+
# @return [Symbol] The status of the execution (:completed, :partial_failure, :failed)
|
10
|
+
attr_reader :status
|
11
|
+
|
12
|
+
# @return [Float] The total execution time in seconds
|
13
|
+
attr_reader :execution_time
|
14
|
+
|
15
|
+
# @return [Hash] The tasks that were executed, keyed by ID
|
16
|
+
attr_reader :tasks
|
17
|
+
|
18
|
+
# @return [Hash] The results of the tasks, keyed by task ID
|
19
|
+
attr_reader :results
|
20
|
+
|
21
|
+
# Initializes a new execution result
|
22
|
+
# @param plan_id [String] The ID of the plan
|
23
|
+
# @param status [Symbol] The status of the execution
|
24
|
+
# @param execution_time [Float] The total execution time in seconds
|
25
|
+
# @param tasks [Hash] The tasks that were executed, keyed by ID
|
26
|
+
# @param results [Hash] The results of the tasks, keyed by task ID
|
27
|
+
def initialize(plan_id:, status:, execution_time:, tasks:, results:)
|
28
|
+
@plan_id = plan_id
|
29
|
+
@status = status
|
30
|
+
@execution_time = execution_time
|
31
|
+
@tasks = tasks
|
32
|
+
@results = results
|
33
|
+
end
|
34
|
+
|
35
|
+
# Returns the result for a specific task
|
36
|
+
# @param task_id [String] The ID of the task
|
37
|
+
# @return [TaskResult, nil] The result of the task, or nil if not found
|
38
|
+
def task_result(task_id)
|
39
|
+
@results[task_id]
|
40
|
+
end
|
41
|
+
|
42
|
+
# Checks if the execution was fully successful
|
43
|
+
# @return [Boolean] True if all tasks succeeded
|
44
|
+
def successful?
|
45
|
+
@status == :completed
|
46
|
+
end
|
47
|
+
|
48
|
+
# Checks if the execution partially failed
|
49
|
+
# @return [Boolean] True if some tasks failed but the plan completed
|
50
|
+
def partial_failure?
|
51
|
+
@status == :partial_failure
|
52
|
+
end
|
53
|
+
|
54
|
+
# Checks if the execution completely failed
|
55
|
+
# @return [Boolean] True if the plan failed to complete
|
56
|
+
def failed?
|
57
|
+
@status == :failed
|
58
|
+
end
|
59
|
+
|
60
|
+
# Returns a hash representation of the execution result
|
61
|
+
# @return [Hash] The execution result as a hash
|
62
|
+
def to_h
|
63
|
+
{
|
64
|
+
plan_id: @plan_id,
|
65
|
+
status: @status,
|
66
|
+
execution_time: @execution_time,
|
67
|
+
tasks: @tasks.transform_values { |task| task.is_a?(Task) ? task.to_h : task },
|
68
|
+
results: @results.transform_values { |result| result.is_a?(TaskResult) ? result.to_h : result }
|
69
|
+
}
|
70
|
+
end
|
71
|
+
|
72
|
+
# Returns a summary of the execution result
|
73
|
+
# @return [Hash] A summary of the execution result
|
74
|
+
def summary
|
75
|
+
total_tasks = @tasks.size
|
76
|
+
successful_tasks = @results.count { |_, result| result.successful? }
|
77
|
+
failed_tasks = @results.count { |_, result| result.failed? }
|
78
|
+
|
79
|
+
{
|
80
|
+
plan_id: @plan_id,
|
81
|
+
status: @status,
|
82
|
+
execution_time: @execution_time,
|
83
|
+
task_counts: {
|
84
|
+
total: total_tasks,
|
85
|
+
successful: successful_tasks,
|
86
|
+
failed: failed_tasks
|
87
|
+
}
|
88
|
+
}
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Agentic
|
4
|
+
# Value object representing expected answer format
|
5
|
+
class ExpectedAnswerFormat
|
6
|
+
# @return [String] The format of the expected answer
|
7
|
+
attr_reader :format
|
8
|
+
|
9
|
+
# @return [Array<String>] The sections expected in the answer
|
10
|
+
attr_reader :sections
|
11
|
+
|
12
|
+
# @return [String] The expected length of the answer
|
13
|
+
attr_reader :length
|
14
|
+
|
15
|
+
# Initializes a new expected answer format
|
16
|
+
# @param format [String] The format of the expected answer
|
17
|
+
# @param sections [Array<String>] The sections expected in the answer
|
18
|
+
# @param length [String] The expected length of the answer
|
19
|
+
def initialize(format:, sections:, length:)
|
20
|
+
@format = format
|
21
|
+
@sections = sections
|
22
|
+
@length = length
|
23
|
+
end
|
24
|
+
|
25
|
+
# Returns a serializable representation of the expected answer format
|
26
|
+
# @return [Hash] The expected answer format as a hash
|
27
|
+
def to_h
|
28
|
+
{
|
29
|
+
"format" => @format,
|
30
|
+
"sections" => @sections,
|
31
|
+
"length" => @length
|
32
|
+
}
|
33
|
+
end
|
34
|
+
|
35
|
+
# Creates an ExpectedAnswerFormat from a hash
|
36
|
+
# @param hash [Hash] The hash representation
|
37
|
+
# @return [ExpectedAnswerFormat] A new expected answer format
|
38
|
+
def self.from_hash(hash)
|
39
|
+
new(
|
40
|
+
format: hash["format"],
|
41
|
+
sections: hash["sections"],
|
42
|
+
length: hash["length"]
|
43
|
+
)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Agentic
|
4
|
+
module Extension
|
5
|
+
# The DomainAdapter integrates domain-specific knowledge into the general agent framework.
|
6
|
+
# It provides mechanisms for adapting prompts, tasks, and verification strategies
|
7
|
+
# to specific domains like healthcare, finance, legal, etc.
|
8
|
+
class DomainAdapter
|
9
|
+
# Initialize a new DomainAdapter
|
10
|
+
#
|
11
|
+
# @param [String] domain The identifier for the domain (e.g., "healthcare", "finance")
|
12
|
+
# @param [Hash] options Configuration options
|
13
|
+
# @option options [Logger] :logger Custom logger instance
|
14
|
+
# @option options [Hash] :domain_config Domain-specific configuration
|
15
|
+
def initialize(domain, options = {})
|
16
|
+
@domain = domain
|
17
|
+
@logger = options[:logger] || Agentic.logger
|
18
|
+
@domain_config = options[:domain_config] || {}
|
19
|
+
@adapters = {}
|
20
|
+
@domain_knowledge = {}
|
21
|
+
|
22
|
+
initialize_default_adapters
|
23
|
+
end
|
24
|
+
|
25
|
+
# Register an adapter for a specific component
|
26
|
+
#
|
27
|
+
# @param [Symbol] component The component to adapt (e.g., :prompt, :task, :verification)
|
28
|
+
# @param [Proc] adapter A callable that performs the adaptation
|
29
|
+
# @return [Boolean] True if registration was successful
|
30
|
+
def register_adapter(component, adapter)
|
31
|
+
return false unless adapter.respond_to?(:call)
|
32
|
+
|
33
|
+
@adapters[component] = adapter
|
34
|
+
true
|
35
|
+
end
|
36
|
+
|
37
|
+
# Add domain-specific knowledge
|
38
|
+
#
|
39
|
+
# @param [Symbol] key The knowledge identifier
|
40
|
+
# @param [Object] knowledge The domain knowledge to store
|
41
|
+
def add_knowledge(key, knowledge)
|
42
|
+
@domain_knowledge[key] = knowledge
|
43
|
+
end
|
44
|
+
|
45
|
+
# Get domain-specific knowledge
|
46
|
+
#
|
47
|
+
# @param [Symbol] key The knowledge identifier
|
48
|
+
# @return [Object, nil] The stored knowledge or nil if not found
|
49
|
+
def get_knowledge(key)
|
50
|
+
@domain_knowledge[key]
|
51
|
+
end
|
52
|
+
|
53
|
+
# Apply domain-specific adaptation to a component
|
54
|
+
#
|
55
|
+
# @param [Symbol] component The component to adapt
|
56
|
+
# @param [Object] target The target to apply adaptation to
|
57
|
+
# @param [Hash] context Additional context for adaptation
|
58
|
+
# @return [Object] The adapted target
|
59
|
+
def adapt(component, target, context = {})
|
60
|
+
return target unless @adapters.key?(component)
|
61
|
+
|
62
|
+
adapter = @adapters[component]
|
63
|
+
context = context.merge(domain: @domain, domain_knowledge: @domain_knowledge)
|
64
|
+
|
65
|
+
begin
|
66
|
+
result = adapter.call(target, context)
|
67
|
+
@logger.debug("Applied #{@domain} domain adaptation to #{component}")
|
68
|
+
result
|
69
|
+
rescue => e
|
70
|
+
@logger.error("Failed to apply #{@domain} domain adaptation to #{component}: #{e.message}")
|
71
|
+
target # Return original if adaptation fails
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
# Check if the adapter supports a specific component
|
76
|
+
#
|
77
|
+
# @param [Symbol] component The component to check
|
78
|
+
# @return [Boolean] True if an adapter exists for the component
|
79
|
+
def supports?(component)
|
80
|
+
@adapters.key?(component)
|
81
|
+
end
|
82
|
+
|
83
|
+
# Get the domain identifier
|
84
|
+
#
|
85
|
+
# @return [String] The domain identifier
|
86
|
+
attr_reader :domain
|
87
|
+
|
88
|
+
# Get domain configuration
|
89
|
+
#
|
90
|
+
# @return [Hash] The domain configuration
|
91
|
+
def configuration
|
92
|
+
@domain_config
|
93
|
+
end
|
94
|
+
|
95
|
+
private
|
96
|
+
|
97
|
+
# Initialize default adapters for common components
|
98
|
+
def initialize_default_adapters
|
99
|
+
# Identity adapter (returns input unchanged) as fallback
|
100
|
+
identity_adapter = ->(target, _context) { target }
|
101
|
+
|
102
|
+
# Register default adapters for common components
|
103
|
+
register_adapter(:prompt, identity_adapter)
|
104
|
+
register_adapter(:task, identity_adapter)
|
105
|
+
register_adapter(:verification, identity_adapter)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
@@ -0,0 +1,163 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Agentic
|
4
|
+
module Extension
|
5
|
+
# The PluginManager coordinates third-party extension loading, registration,
|
6
|
+
# and lifecycle management. It provides a central registry for all extensions
|
7
|
+
# and ensures they conform to the extension contracts.
|
8
|
+
class PluginManager
|
9
|
+
# Initialize a new PluginManager
|
10
|
+
#
|
11
|
+
# @param [Hash] options Configuration options for the plugin manager
|
12
|
+
# @option options [Logger] :logger Custom logger instance
|
13
|
+
# @option options [Boolean] :auto_discovery Whether to automatically discover plugins
|
14
|
+
# @option options [Array<String>] :plugin_paths Additional paths to search for plugins
|
15
|
+
def initialize(options = {})
|
16
|
+
@logger = options[:logger] || Agentic.logger
|
17
|
+
@auto_discovery = options.fetch(:auto_discovery, true)
|
18
|
+
@plugin_paths = options[:plugin_paths] || []
|
19
|
+
@plugin_paths << default_plugin_path
|
20
|
+
@plugins = {}
|
21
|
+
@hooks = Hash.new { |h, k| h[k] = [] }
|
22
|
+
|
23
|
+
discover_plugins if @auto_discovery
|
24
|
+
end
|
25
|
+
|
26
|
+
# Register a plugin with the manager
|
27
|
+
#
|
28
|
+
# @param [String] name The unique name of the plugin
|
29
|
+
# @param [Object] plugin The plugin instance
|
30
|
+
# @param [Hash] metadata Additional information about the plugin
|
31
|
+
# @return [Boolean] True if registration was successful
|
32
|
+
def register(name, plugin, metadata = {})
|
33
|
+
if @plugins.key?(name)
|
34
|
+
@logger.warn("Plugin '#{name}' is already registered. Use force: true to override.")
|
35
|
+
return false
|
36
|
+
end
|
37
|
+
|
38
|
+
unless valid_plugin?(plugin)
|
39
|
+
@logger.error("Plugin '#{name}' does not conform to the plugin contract")
|
40
|
+
return false
|
41
|
+
end
|
42
|
+
|
43
|
+
@plugins[name] = {
|
44
|
+
instance: plugin,
|
45
|
+
metadata: metadata.merge(registered_at: Time.now),
|
46
|
+
enabled: true
|
47
|
+
}
|
48
|
+
|
49
|
+
@logger.info("Plugin '#{name}' registered successfully")
|
50
|
+
true
|
51
|
+
end
|
52
|
+
|
53
|
+
# Register a plugin with the manager, overriding any existing plugin with the same name
|
54
|
+
#
|
55
|
+
# @param [String] name The unique name of the plugin
|
56
|
+
# @param [Object] plugin The plugin instance
|
57
|
+
# @param [Hash] metadata Additional information about the plugin
|
58
|
+
# @return [Boolean] True if registration was successful
|
59
|
+
def register!(name, plugin, metadata = {})
|
60
|
+
@plugins.delete(name) if @plugins.key?(name)
|
61
|
+
register(name, plugin, metadata)
|
62
|
+
end
|
63
|
+
|
64
|
+
# Enable a registered plugin
|
65
|
+
#
|
66
|
+
# @param [String] name The name of the plugin to enable
|
67
|
+
# @return [Boolean] True if the plugin was enabled
|
68
|
+
def enable(name)
|
69
|
+
return false unless @plugins.key?(name)
|
70
|
+
|
71
|
+
@plugins[name][:enabled] = true
|
72
|
+
@logger.info("Plugin '#{name}' enabled")
|
73
|
+
true
|
74
|
+
end
|
75
|
+
|
76
|
+
# Disable a registered plugin
|
77
|
+
#
|
78
|
+
# @param [String] name The name of the plugin to disable
|
79
|
+
# @return [Boolean] True if the plugin was disabled
|
80
|
+
def disable(name)
|
81
|
+
return false unless @plugins.key?(name)
|
82
|
+
|
83
|
+
@plugins[name][:enabled] = false
|
84
|
+
@logger.info("Plugin '#{name}' disabled")
|
85
|
+
true
|
86
|
+
end
|
87
|
+
|
88
|
+
# Get a registered plugin by name
|
89
|
+
#
|
90
|
+
# @param [String] name The name of the plugin to retrieve
|
91
|
+
# @return [Object, nil] The plugin or nil if not found or disabled
|
92
|
+
def get(name)
|
93
|
+
return nil unless @plugins.key?(name) && @plugins[name][:enabled]
|
94
|
+
|
95
|
+
@plugins[name][:instance]
|
96
|
+
end
|
97
|
+
|
98
|
+
# List all registered plugins
|
99
|
+
#
|
100
|
+
# @param [Boolean] only_enabled Only return enabled plugins
|
101
|
+
# @return [Hash] A hash of all registered plugins and their metadata
|
102
|
+
def list(only_enabled: false)
|
103
|
+
if only_enabled
|
104
|
+
@plugins.select { |_, data| data[:enabled] }
|
105
|
+
else
|
106
|
+
@plugins
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
# Register a hook for plugin events
|
111
|
+
#
|
112
|
+
# @param [Symbol] event The event to hook into (:after_register, :before_enable, :after_enable, :before_disable, :after_disable)
|
113
|
+
# @yield [name, plugin] The callback to execute when the event occurs
|
114
|
+
# @yieldparam [String] name The name of the plugin
|
115
|
+
# @yieldparam [Object] plugin The plugin instance
|
116
|
+
# @return [Boolean] True if the hook was registered
|
117
|
+
def register_hook(event, &callback)
|
118
|
+
return false unless callback
|
119
|
+
|
120
|
+
@hooks[event] << callback
|
121
|
+
true
|
122
|
+
end
|
123
|
+
|
124
|
+
# Discover plugins in configured paths
|
125
|
+
#
|
126
|
+
# @return [Integer] The number of plugins discovered
|
127
|
+
def discover_plugins
|
128
|
+
return 0 unless @auto_discovery
|
129
|
+
|
130
|
+
discovered = 0
|
131
|
+
@plugin_paths.each do |path|
|
132
|
+
Dir.glob(File.join(path, "*.rb")).each do |file|
|
133
|
+
require file
|
134
|
+
discovered += 1
|
135
|
+
rescue => e
|
136
|
+
@logger.error("Failed to load plugin from #{file}: #{e.message}")
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
@logger.info("Discovered #{discovered} plugins")
|
141
|
+
discovered
|
142
|
+
end
|
143
|
+
|
144
|
+
private
|
145
|
+
|
146
|
+
# Get the default plugin path
|
147
|
+
#
|
148
|
+
# @return [String] The default path for plugins
|
149
|
+
def default_plugin_path
|
150
|
+
File.join(File.dirname(__FILE__), "../../../plugins")
|
151
|
+
end
|
152
|
+
|
153
|
+
# Check if a plugin conforms to the plugin contract
|
154
|
+
#
|
155
|
+
# @param [Object] plugin The plugin to validate
|
156
|
+
# @return [Boolean] True if the plugin is valid
|
157
|
+
def valid_plugin?(plugin)
|
158
|
+
# Check that the plugin implements both required methods
|
159
|
+
plugin.respond_to?(:initialize_plugin) && plugin.respond_to?(:call)
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|