composable_agents 2.0.0 → 2.1.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/CHANGELOG.md +13 -0
- data/README.md +7 -10
- data/lib/composable_agents/agent.rb +16 -3
- data/lib/composable_agents/ai_agents/agent.rb +2 -2
- data/lib/composable_agents/ai_agents/tools/ask_user_tool.rb +5 -6
- data/lib/composable_agents/ai_agents/tools/base_tool.rb +22 -0
- data/lib/composable_agents/ai_agents/tools/create_artifact_tool.rb +5 -5
- data/lib/composable_agents/ai_agents/tools/get_artifact_tool.rb +6 -6
- data/lib/composable_agents/cline/agent.rb +12 -5
- data/lib/composable_agents/logger/tagged.rb +110 -0
- data/lib/composable_agents/logger.rb +48 -0
- data/lib/composable_agents/mixins/ai_agent_user_interaction.rb +1 -1
- data/lib/composable_agents/mixins/resumable.rb +3 -2
- data/lib/composable_agents/prompt_driven_agent.rb +5 -5
- data/lib/composable_agents/prompt_rendering_strategy/markdown_heavy.rb +2 -1
- data/lib/composable_agents/version.rb +1 -1
- metadata +4 -2
- data/lib/composable_agents/mixins/logger.rb +0 -47
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: af07644d98f8997653ead8dc4b2707fe3ff6338480373cbc6fa5c5cc9aa5d82a
|
|
4
|
+
data.tar.gz: 2824ad225970ce7f3af6ff6e3d64d4a4200638af65bda0263e592466e7793e8d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 154a31e282dccb4b9c2f02a45b4ec2322b6dd269e63f9e21d0f7f9d239c5a087307d62c905a23ed5ca7981cd6fce8bf4bd4005ba80165f251d0c5b5d36d57440
|
|
7
|
+
data.tar.gz: 4f0d0b0ad45ab9a72ab206fd41d6d420dc167f0b3a66b3fd485f76ebece746d47eff3a9d17e7d2d66570369cda52eb1036eb44b1a59459b8626c063cf62eaa69
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,16 @@
|
|
|
1
|
+
# [v2.1.0](https://github.com/Muriel-Salvan/composable_agents/compare/v2.0.0...v2.1.0) (2026-09-06 16:41:14)
|
|
2
|
+
|
|
3
|
+
## Global changes
|
|
4
|
+
### Patches
|
|
5
|
+
|
|
6
|
+
* [fix: return nil on JSON parse failure to skip artifact save](https://github.com/Muriel-Salvan/composable_agents/commit/2f70822a9142d97aa9d5fa9424a8f9c97f98208e)
|
|
7
|
+
* [[Feature(Agent)] Added configurable logger for all agents.](https://github.com/Muriel-Salvan/composable_agents/commit/2979d6de1ea73bb259147f3778d6a51481c79f22)
|
|
8
|
+
|
|
9
|
+
## Changes for Agent
|
|
10
|
+
### Features
|
|
11
|
+
|
|
12
|
+
* [[Feature(Agent)] Added configurable logger for all agents.](https://github.com/Muriel-Salvan/composable_agents/commit/2979d6de1ea73bb259147f3778d6a51481c79f22)
|
|
13
|
+
|
|
1
14
|
# [v2.0.0](https://github.com/Muriel-Salvan/composable_agents/compare/v1.1.2...v2.0.0) (2026-09-04 12:17:53)
|
|
2
15
|
|
|
3
16
|
## Global changes
|
data/README.md
CHANGED
|
@@ -49,7 +49,7 @@ Whether you're building a code review assistant, a document summarizer, or a mul
|
|
|
49
49
|
- [`ComposableAgents::Cline::Agent < PromptDrivenAgent`](#composableagentsclineagent--promptdrivenagent)
|
|
50
50
|
- [`ComposableAgents::Cline::MissingSkillError < RuntimeError`](#composableagentsclinemissingskillerror--runtimeerror)
|
|
51
51
|
- [Mixins](#mixins)
|
|
52
|
-
- [`ComposableAgents::
|
|
52
|
+
- [`ComposableAgents::Logger < ::Logger`](#composableagentslogger--logger)
|
|
53
53
|
- [`ComposableAgents::Mixins::Resumable`](#composableagentsmixinsresumable)
|
|
54
54
|
- [`ComposableAgents::Mixins::UserInteraction`](#composableagentsmixinsuserinteraction)
|
|
55
55
|
- [`ComposableAgents::Mixins::ArtifactContract`](#composableagentsmixinsartifactcontract)
|
|
@@ -365,18 +365,16 @@ The project is a Ruby library (not a CLI) — users install the gem and use its
|
|
|
365
365
|
|
|
366
366
|
Mixins are `prepend`-ed into an agent class to add specific capabilities.
|
|
367
367
|
|
|
368
|
-
#### `ComposableAgents::
|
|
368
|
+
#### `ComposableAgents::Logger < ::Logger`
|
|
369
369
|
|
|
370
|
-
- **Description:**
|
|
371
|
-
- **Public methods (class-level):**
|
|
372
|
-
- `self.debug?` — Returns `true` if debug mode is enabled (`ENV['COMPOSABLE_AGENTS_DEBUG'] == '1'`).
|
|
370
|
+
- **Description:** Standard Ruby `::Logger`-based logger for agents. Debug logging is enabled by setting `COMPOSABLE_AGENTS_DEBUG=1` in the environment (which sets the logger level to `DEBUG`), and log fields (like the agent full name) can be prefixed to messages via `#tagged`.
|
|
373
371
|
- **Usage example:**
|
|
374
372
|
```ruby
|
|
375
373
|
# Enable debug logs
|
|
376
374
|
ENV['COMPOSABLE_AGENTS_DEBUG'] = '1'
|
|
377
|
-
puts ComposableAgents::
|
|
375
|
+
puts ComposableAgents::Logger.instance.level == Logger::DEBUG # => true
|
|
378
376
|
```
|
|
379
|
-
- **Full documentation:**
|
|
377
|
+
- **Full documentation:** ComposableAgents::Logger on RubyDoc | [logger.rb on GitHub](https://github.com/Muriel-Salvan/composable_agents/blob/main/lib/composable_agents/logger.rb)
|
|
380
378
|
|
|
381
379
|
#### `ComposableAgents::Mixins::Resumable`
|
|
382
380
|
|
|
@@ -479,7 +477,7 @@ Mixins are `prepend`-ed into an agent class to add specific capabilities.
|
|
|
479
477
|
- [`Mixins::Resumable`](https://github.com/Muriel-Salvan/composable_agents/blob/main/lib/composable_agents/mixins/resumable.rb) — Resumable workflow mixin
|
|
480
478
|
- [`Mixins::ArtifactContract`](https://github.com/Muriel-Salvan/composable_agents/blob/main/lib/composable_agents/mixins/artifact_contract.rb) — Artifact validation mixin
|
|
481
479
|
- [`Mixins::UserInteraction`](https://github.com/Muriel-Salvan/composable_agents/blob/main/lib/composable_agents/mixins/user_interaction.rb) — User question-asking mixin
|
|
482
|
-
- [`
|
|
480
|
+
- [`Logger`](https://github.com/Muriel-Salvan/composable_agents/blob/main/lib/composable_agents/logger.rb) — Standard logger class
|
|
483
481
|
- [`PromptRenderingStrategy::Markdown`](https://github.com/Muriel-Salvan/composable_agents/blob/main/lib/composable_agents/prompt_rendering_strategy/markdown.rb) — Markdown prompt strategy
|
|
484
482
|
- [`PromptRenderingStrategy::MarkdownHeavy`](https://github.com/Muriel-Salvan/composable_agents/blob/main/lib/composable_agents/prompt_rendering_strategy/markdown_heavy.rb) — Heavy Markdown prompt strategy
|
|
485
483
|
- [`Utils::Markdown`](https://github.com/Muriel-Salvan/composable_agents/blob/main/lib/composable_agents/utils/markdown.rb) — Markdown header alignment utilities
|
|
@@ -524,7 +522,7 @@ classDiagram
|
|
|
524
522
|
|
|
525
523
|
The framework provides a clean **4-class hierarchy**:
|
|
526
524
|
|
|
527
|
-
- **`Agent`** — Abstract base class. Defines the `run(**input_artifacts)` contract and includes the [`
|
|
525
|
+
- **`Agent`** — Abstract base class. Defines the `run(**input_artifacts)` contract and includes the [`Logger`](lib/composable_agents/logger.rb) for debug/info logging.
|
|
528
526
|
- **`RubyAgent`** — Wraps any Ruby `Proc` as an agent. No LLM involved: call `proc.call(input_artifacts)` and return a hash. Ideal for deterministic logic.
|
|
529
527
|
- **`PromptDrivenAgent`** — Base for LLM-powered agents. Holds a **role**, **objective**, **system_instructions**, and **constraints**. Renders them via a pluggable **prompt rendering strategy** and records every prompt/response in a `conversation` array.
|
|
530
528
|
- **`AiAgents::Agent` / `Cline::Agent`** — Concrete LLM backends: one wraps the [`ai-agents`](https://github.com/nicbarker/ai-agents) gem, the other wraps [`cline-rb`](https://github.com/Muriel-Salvan/cline-rb). Both implement `#prompt(user_prompt)` to send the rendered prompt to the LLM.
|
|
@@ -584,7 +582,6 @@ Mixins are **prepended** (using `prepend`) or **included** (using `include`) to
|
|
|
584
582
|
- **[`Mixins::ArtifactContract`](lib/composable_agents/mixins/artifact_contract.rb)** — Wraps `#run` to validate inputs before and outputs after execution against declared contracts. Raises `MissingInputArtifactError`, `MissingOutputArtifactError`, or `ArtifactTypeError` on violations.
|
|
585
583
|
- **[`Mixins::Resumable`](lib/composable_agents/mixins/resumable.rb)** — Overrides `#run` with a **step-based execution model**. Each `step` block is persisted to `.composable_agents/runs/{run_id}/` as JSON. On re-run, completed steps are skipped — only new steps execute. Supports nested steps and agent state serialization via `export_state`/`import_state`.
|
|
586
584
|
- **[`Mixins::UserInteraction`](lib/composable_agents/mixins/user_interaction.rb)** — Adds an `#ask(question)` method. By default prompts the terminal; override `#answer_to` for custom behavior.
|
|
587
|
-
- **[`Mixins::Logger`](lib/composable_agents/mixins/logger.rb)** — Provides `log_debug`/`log_info` methods. Debug output is toggled via the `COMPOSABLE_AGENTS_DEBUG=1` environment variable.
|
|
588
585
|
|
|
589
586
|
### Instruction system 📋
|
|
590
587
|
|
|
@@ -3,8 +3,6 @@ module ComposableAgents
|
|
|
3
3
|
# Agents may internally use LLMs, tools, or other agents.
|
|
4
4
|
# Agents are stateless: they take input artifacts and return output artifacts.
|
|
5
5
|
class Agent
|
|
6
|
-
include Mixins::Logger
|
|
7
|
-
|
|
8
6
|
# @!group Public API
|
|
9
7
|
|
|
10
8
|
# @return [String, nil] The agent name, if any.
|
|
@@ -17,12 +15,18 @@ module ComposableAgents
|
|
|
17
15
|
#
|
|
18
16
|
# @param name [String, nil] Agent name, or nil if none
|
|
19
17
|
# @param composable_agents_dir [String] Base directory where composable agents can store data
|
|
18
|
+
# @param logger [::Logger] Logger to be used by this agent. Any Ruby-like logger is accepted.
|
|
19
|
+
# Defaults to ComposableAgents::Logger's singleton instance.
|
|
20
|
+
# This agent's log fields (see #log_fields) are prefixed to logged messages when the logger supports it
|
|
21
|
+
# (see ComposableAgents::Logger#tagged), and are left as-is otherwise.
|
|
20
22
|
def initialize(
|
|
21
23
|
name: nil,
|
|
22
|
-
composable_agents_dir: '.composable_agents'
|
|
24
|
+
composable_agents_dir: '.composable_agents',
|
|
25
|
+
logger: ComposableAgents::Logger.instance
|
|
23
26
|
)
|
|
24
27
|
@name = name
|
|
25
28
|
@composable_agents_dir = composable_agents_dir
|
|
29
|
+
@provided_logger = logger
|
|
26
30
|
@runs_info = []
|
|
27
31
|
@current_run_info = nil
|
|
28
32
|
end
|
|
@@ -52,6 +56,15 @@ module ComposableAgents
|
|
|
52
56
|
# @return [RunInfo, nil] The current run info, or nil if no run has started yet
|
|
53
57
|
attr_reader :current_run_info
|
|
54
58
|
|
|
59
|
+
# Get the logger to be used by this agent, with this agent's log fields (see #log_fields) prefixed to messages.
|
|
60
|
+
# If the provided logger does not support fields tagging, it is used as-is (fields are ignored).
|
|
61
|
+
# Memoized.
|
|
62
|
+
#
|
|
63
|
+
# @return [::Logger] The logger to be used by this agent
|
|
64
|
+
def logger
|
|
65
|
+
@logger ||= @provided_logger.respond_to?(:tagged) ? @provided_logger.tagged(*log_fields) : @provided_logger
|
|
66
|
+
end
|
|
67
|
+
|
|
55
68
|
# Fields to be logged
|
|
56
69
|
#
|
|
57
70
|
# @return [Array<String>] Fields to log
|
|
@@ -94,8 +94,8 @@ module ComposableAgents
|
|
|
94
94
|
params: @params,
|
|
95
95
|
instructions: @system_prompt,
|
|
96
96
|
tools: [
|
|
97
|
-
Tools::CreateArtifactTool.new(self),
|
|
98
|
-
Tools::GetArtifactTool.new(@input_artifacts)
|
|
97
|
+
Tools::CreateArtifactTool.new(self, logger:),
|
|
98
|
+
Tools::GetArtifactTool.new(@input_artifacts, logger:)
|
|
99
99
|
] + agent_tools
|
|
100
100
|
)
|
|
101
101
|
] + @handoff_agents
|
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
module ComposableAgents
|
|
2
2
|
module AiAgents
|
|
3
|
-
# Collection of tools that can be used with ai-agents' agents
|
|
4
3
|
module Tools
|
|
5
4
|
# Tool that is used to ask the user for some input
|
|
6
|
-
class AskUserTool <
|
|
5
|
+
class AskUserTool < BaseTool
|
|
7
6
|
# @!group Internal
|
|
8
7
|
|
|
9
|
-
include Mixins::Logger
|
|
10
|
-
|
|
11
8
|
description <<~EO_DESCRIPTION
|
|
12
9
|
Ask the user a question and wait for their response.
|
|
13
10
|
If you need information from the user, call this tool instead of asking directly.
|
|
@@ -18,8 +15,10 @@ module ComposableAgents
|
|
|
18
15
|
# Constructor
|
|
19
16
|
#
|
|
20
17
|
# @param agent [Agent] The agent that is using this tool
|
|
21
|
-
|
|
22
|
-
|
|
18
|
+
# @param logger [::Logger] Logger to be used by this tool. Any Ruby-like logger is accepted.
|
|
19
|
+
# Defaults to ComposableAgents::Logger's singleton instance.
|
|
20
|
+
def initialize(agent, logger: ComposableAgents::Logger.instance)
|
|
21
|
+
super(logger:)
|
|
23
22
|
@agent = agent
|
|
24
23
|
end
|
|
25
24
|
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
module ComposableAgents
|
|
2
|
+
module AiAgents
|
|
3
|
+
# Collection of tools that can be used with ai-agents' agents
|
|
4
|
+
module Tools
|
|
5
|
+
# Base class for all tools used by agents.
|
|
6
|
+
# It provides a logger to all tools, that can be set to any Ruby-like logger
|
|
7
|
+
# and defaults to ComposableAgents::Logger's singleton instance.
|
|
8
|
+
class BaseTool < Agents::Tool
|
|
9
|
+
# @!group Internal
|
|
10
|
+
|
|
11
|
+
# Constructor
|
|
12
|
+
#
|
|
13
|
+
# @param logger [::Logger] Logger to be used by this tool. Any Ruby-like logger is accepted.
|
|
14
|
+
# Defaults to ComposableAgents::Logger's singleton instance.
|
|
15
|
+
def initialize(logger: ComposableAgents::Logger.instance)
|
|
16
|
+
super()
|
|
17
|
+
@logger = logger
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -2,11 +2,9 @@ module ComposableAgents
|
|
|
2
2
|
module AiAgents
|
|
3
3
|
module Tools
|
|
4
4
|
# Tool that is used to create a new artifact's content
|
|
5
|
-
class CreateArtifactTool <
|
|
5
|
+
class CreateArtifactTool < BaseTool
|
|
6
6
|
# @!group Internal
|
|
7
7
|
|
|
8
|
-
include Mixins::Logger
|
|
9
|
-
|
|
10
8
|
description <<~EO_DESCRIPTION
|
|
11
9
|
Create an output artifact.
|
|
12
10
|
Only create artifacts when the user is asking you to create an artifact named `{name}`.
|
|
@@ -18,8 +16,10 @@ module ComposableAgents
|
|
|
18
16
|
# Constructor
|
|
19
17
|
#
|
|
20
18
|
# @param agent [PromptDrivenAgent] The agent that will receive output artifacts
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
# @param logger [::Logger] Logger to be used by this tool. Any Ruby-like logger is accepted.
|
|
20
|
+
# Defaults to ComposableAgents::Logger's singleton instance.
|
|
21
|
+
def initialize(agent, logger: ComposableAgents::Logger.instance)
|
|
22
|
+
super(logger:)
|
|
23
23
|
@agent = agent
|
|
24
24
|
end
|
|
25
25
|
|
|
@@ -2,19 +2,19 @@ module ComposableAgents
|
|
|
2
2
|
module AiAgents
|
|
3
3
|
module Tools
|
|
4
4
|
# Tool that is used to read an artifact's content
|
|
5
|
-
class GetArtifactTool <
|
|
5
|
+
class GetArtifactTool < BaseTool
|
|
6
6
|
# @!group Internal
|
|
7
7
|
|
|
8
|
-
include Mixins::Logger
|
|
9
|
-
|
|
10
8
|
description 'Get an input artifact'
|
|
11
9
|
param :name, type: 'string', desc: 'Artifact name'
|
|
12
10
|
|
|
13
11
|
# Constructor
|
|
14
12
|
#
|
|
15
13
|
# @param artifacts [Hash{Symbol => Object}>] The artifacts store
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
# @param logger [::Logger] Logger to be used by this tool. Any Ruby-like logger is accepted.
|
|
15
|
+
# Defaults to ComposableAgents::Logger's singleton instance.
|
|
16
|
+
def initialize(artifacts, logger: ComposableAgents::Logger.instance)
|
|
17
|
+
super(logger:)
|
|
18
18
|
@artifacts = artifacts
|
|
19
19
|
end
|
|
20
20
|
|
|
@@ -27,7 +27,7 @@ module ComposableAgents
|
|
|
27
27
|
def perform(_tool_context, name:)
|
|
28
28
|
name_sym = name.to_sym
|
|
29
29
|
if @artifacts.key?(name_sym)
|
|
30
|
-
|
|
30
|
+
@logger.debug "Artifact `#{name}` read successfully."
|
|
31
31
|
@artifacts[name_sym].to_s.strip
|
|
32
32
|
else
|
|
33
33
|
"[ERROR] No artifact named #{name}, don't call this tool with the name #{name} anymore."
|
|
@@ -209,7 +209,7 @@ module ComposableAgents
|
|
|
209
209
|
# Setup the temporary Cline global config dir
|
|
210
210
|
agent_tmp_dir = "#{@composable_agents_dir}/tmp/#{Time.now.utc.strftime('%F-%H-%M-%S')}#{"-#{name.gsub(/[^\w.]/, '_')}" if name}"
|
|
211
211
|
::Cline.configure do |config|
|
|
212
|
-
config.debug =
|
|
212
|
+
config.debug = logger_debug?
|
|
213
213
|
config.temp_dir_root = "#{agent_tmp_dir}/cline-rb"
|
|
214
214
|
end
|
|
215
215
|
cline_config = ::Cline::Config.open("#{agent_tmp_dir}/cline_config", create: true)
|
|
@@ -219,7 +219,7 @@ module ComposableAgents
|
|
|
219
219
|
new_skill = cline_config.skills.new(skill_name)
|
|
220
220
|
new_skill.files.replace(::Cline::Config.global.skills[skill_name].files)
|
|
221
221
|
new_skill.enable
|
|
222
|
-
|
|
222
|
+
logger.debug "[Cline] - Enable global skill #{skill_name}"
|
|
223
223
|
new_skill.save
|
|
224
224
|
end
|
|
225
225
|
end
|
|
@@ -229,10 +229,10 @@ module ComposableAgents
|
|
|
229
229
|
next if skill.enabled? == selected_skill
|
|
230
230
|
|
|
231
231
|
if selected_skill
|
|
232
|
-
|
|
232
|
+
logger.debug "[Cline] - Enable project skill #{skill_name}"
|
|
233
233
|
skill.enable
|
|
234
234
|
else
|
|
235
|
-
|
|
235
|
+
logger.debug "[Cline] - Disable project skill #{skill_name}"
|
|
236
236
|
skill.disable
|
|
237
237
|
end
|
|
238
238
|
skill.save
|
|
@@ -261,10 +261,17 @@ module ComposableAgents
|
|
|
261
261
|
global_settings.auto_update_enabled = false
|
|
262
262
|
@configure_global&.call(global_settings)
|
|
263
263
|
global_settings.save
|
|
264
|
-
cline_config.cli(stdout_echo:
|
|
264
|
+
cline_config.cli(stdout_echo: logger_debug?, verbose: logger_debug?)
|
|
265
265
|
end
|
|
266
266
|
end
|
|
267
267
|
|
|
268
|
+
# Check if the agent's logger is set to a debug level.
|
|
269
|
+
#
|
|
270
|
+
# @return [Boolean] True if the logger level is DEBUG or lower
|
|
271
|
+
def logger_debug?
|
|
272
|
+
@provided_logger.respond_to?(:level) && @provided_logger.level <= ::Logger::DEBUG
|
|
273
|
+
end
|
|
274
|
+
|
|
268
275
|
# Find a skill among the current Cline environment (global and project) and recursively finds all its dependencies.
|
|
269
276
|
#
|
|
270
277
|
# @param skill [String] The skill name we are looking for
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
require 'logger'
|
|
2
|
+
|
|
3
|
+
module ComposableAgents
|
|
4
|
+
# Proxy around any Ruby-like logger that prefixes fields to the logged messages.
|
|
5
|
+
# This is a standard ::Logger that inherits all the severity methods (debug, info, warn, error, fatal, unknown),
|
|
6
|
+
# but overrides the add/log methods to delegate the actual logging to the wrapped logger,
|
|
7
|
+
# with the given fields prefixed to the message.
|
|
8
|
+
# Messages can be given as a String or a Proc returning the message (for lazy evaluation).
|
|
9
|
+
class Logger::Tagged < ::Logger
|
|
10
|
+
# Mapping of severity values to the corresponding severity methods on the wrapped logger
|
|
11
|
+
SEVERITY_METHODS = {
|
|
12
|
+
::Logger::DEBUG => :debug,
|
|
13
|
+
::Logger::INFO => :info,
|
|
14
|
+
::Logger::WARN => :warn,
|
|
15
|
+
::Logger::ERROR => :error,
|
|
16
|
+
::Logger::FATAL => :fatal,
|
|
17
|
+
::Logger::UNKNOWN => :unknown
|
|
18
|
+
}.freeze
|
|
19
|
+
|
|
20
|
+
# @!group Public API
|
|
21
|
+
|
|
22
|
+
# Constructor
|
|
23
|
+
#
|
|
24
|
+
# @param logger [Object] Any Ruby-like logger responding to the standard severity methods
|
|
25
|
+
# @param fields [Array<String>] Fields to be prefixed to each logged message
|
|
26
|
+
def initialize(logger, fields)
|
|
27
|
+
@logger = logger
|
|
28
|
+
@fields = fields
|
|
29
|
+
# This proxy does not log on its own: the wrapped logger does. Its level is kept in sync so that
|
|
30
|
+
# the inherited level predicates (debug?, info?...) reflect the wrapped logger's level.
|
|
31
|
+
super(nil, level: logger.respond_to?(:level) ? logger.level : ::Logger::DEBUG)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Log a message with the given severity, delegating to the wrapped logger with the fields prefixed.
|
|
35
|
+
# This is the implementation used by all severity methods, that all call #add in ::Logger.
|
|
36
|
+
#
|
|
37
|
+
# @param severity [Integer, nil] Severity of the message
|
|
38
|
+
# @param message [String, #call => String, nil] Message string, Proc returning the message for lazy evaluation,
|
|
39
|
+
# or nil if the message is given by the progname or a block
|
|
40
|
+
# @param progname [String, nil] Program name to be logged if the message is nil
|
|
41
|
+
# @yield The optional code returning the message to log
|
|
42
|
+
# @yieldreturn [String] The message to log
|
|
43
|
+
# @return [Boolean] True, as ::Logger#add does
|
|
44
|
+
def add(severity, message = nil, progname = nil, &)
|
|
45
|
+
log_message(severity, message, progname, &)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Log a message with the given severity.
|
|
49
|
+
# Defined because ::Logger defines #log as an alias of #add, and aliases are bound at definition time:
|
|
50
|
+
# without redefining it here, #log would not use this class' #add override.
|
|
51
|
+
#
|
|
52
|
+
# @param severity [Integer, nil] Severity of the message
|
|
53
|
+
# @param message [String, #call => String, nil] Message string, Proc returning the message for lazy evaluation,
|
|
54
|
+
# or nil if the message is given by the progname or a block
|
|
55
|
+
# @param progname [String, nil] Program name to be logged if the message is nil
|
|
56
|
+
# @yield The optional code returning the message to log
|
|
57
|
+
# @yieldreturn [String] The message to log
|
|
58
|
+
# @return [Boolean] True, as ::Logger#add does
|
|
59
|
+
def log(severity, message = nil, progname = nil, &)
|
|
60
|
+
add(severity, message, progname, &)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
# Delegate the message to the wrapped logger, with the fields prefixed.
|
|
66
|
+
# The message is evaluated lazily (only when the wrapped logger's severity level allows it),
|
|
67
|
+
# by passing a block to the wrapped logger's severity method.
|
|
68
|
+
#
|
|
69
|
+
# @param severity [Integer, nil] Severity of the message
|
|
70
|
+
# @param message [String, #call => String, nil] Message string, Proc returning the message for lazy evaluation,
|
|
71
|
+
# or nil if the message is given by the progname or a block
|
|
72
|
+
# @param progname [String, nil] Program name to be logged if the message is nil
|
|
73
|
+
# @yield The optional code returning the message to log
|
|
74
|
+
# @yieldreturn [String] The message to log
|
|
75
|
+
def log_message(severity, message, progname, &)
|
|
76
|
+
@logger.public_send(SEVERITY_METHODS.fetch(severity || ::Logger::UNKNOWN, :unknown)) do
|
|
77
|
+
resolved = resolve_message(message, progname, &)
|
|
78
|
+
prefix = fields_prefix
|
|
79
|
+
prefix.empty? ? resolved.to_s : "#{prefix} - #{resolved}"
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Get the fields to be prefixed to logged messages, formatted
|
|
84
|
+
#
|
|
85
|
+
# @return [String] The formatted fields prefix
|
|
86
|
+
def fields_prefix
|
|
87
|
+
@fields.map { |field| "[#{field}]" }.join(' ')
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Resolve the message to be logged.
|
|
91
|
+
# The block has priority over the message and progname parameters, and Procs are called to get their result.
|
|
92
|
+
#
|
|
93
|
+
# @param message [String, #call => String, nil] Message string or Proc returning the message
|
|
94
|
+
# @param progname [String, nil] Program name to be logged if the message is nil
|
|
95
|
+
# @yield The optional code returning the message to log
|
|
96
|
+
# @yieldreturn [String] The message to log
|
|
97
|
+
# @return [String] The message to be logged
|
|
98
|
+
def resolve_message(message, progname, &)
|
|
99
|
+
resolved =
|
|
100
|
+
if block_given?
|
|
101
|
+
yield
|
|
102
|
+
elsif message.nil?
|
|
103
|
+
progname
|
|
104
|
+
else
|
|
105
|
+
message
|
|
106
|
+
end
|
|
107
|
+
resolved.respond_to?(:call) ? resolved.call : resolved
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
require 'logger'
|
|
2
|
+
|
|
3
|
+
module ComposableAgents
|
|
4
|
+
# Logger to be used by agents.
|
|
5
|
+
# This is a standard Ruby Logger (inheriting from ::Logger) that formats messages with a UTC timestamp and severity.
|
|
6
|
+
# It also supports prefixing fields to messages (like the agent's full name) using the #tagged method,
|
|
7
|
+
# which returns a proxy compatible with any Ruby-like logger: fields are then prepended to the logged messages
|
|
8
|
+
# themselves, and silently ignored by loggers not supporting them.
|
|
9
|
+
class Logger < ::Logger
|
|
10
|
+
# @!group Public API
|
|
11
|
+
|
|
12
|
+
class << self
|
|
13
|
+
# Get the default singleton logger instance
|
|
14
|
+
#
|
|
15
|
+
# @return [Logger] The default logger instance
|
|
16
|
+
def instance
|
|
17
|
+
@instance ||= new
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Constructor.
|
|
22
|
+
# Unless overridden by the caller, the default level follows the COMPOSABLE_AGENTS_DEBUG environment variable
|
|
23
|
+
# (DEBUG when it is set to 1, INFO otherwise), and the default formatter outputs messages as
|
|
24
|
+
# `[UTC timestamp] [SEVERITY] message`.
|
|
25
|
+
#
|
|
26
|
+
# @param logdev [IO, String, #write, nil] Log device to be used, defaulting to standard output
|
|
27
|
+
# @param args [Array<Object>] Positional arguments to be given to ::Logger (shifting parameters...)
|
|
28
|
+
# @param kwargs [Hash{Symbol => Object}] Optional parameters to be given to ::Logger (level, formatter...)
|
|
29
|
+
def initialize(logdev = $stdout, *args, **kwargs)
|
|
30
|
+
super
|
|
31
|
+
self.level = ENV['COMPOSABLE_AGENTS_DEBUG'] == '1' ? DEBUG : INFO unless kwargs.key?(:level)
|
|
32
|
+
return if kwargs.key?(:formatter)
|
|
33
|
+
|
|
34
|
+
self.formatter = proc do |severity, _datetime, _progname, message|
|
|
35
|
+
"[#{Time.now.utc.strftime('%F %T')}] [#{severity}] #{message}\n"
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Get a logger proxy that prefixes the given fields to logged messages.
|
|
40
|
+
# The returned proxy is compatible with any Ruby-like logger.
|
|
41
|
+
#
|
|
42
|
+
# @param fields [Array<String>] Fields to be prefixed to each logged message
|
|
43
|
+
# @return [Tagged] The fields-prefixing logger proxy
|
|
44
|
+
def tagged(*fields)
|
|
45
|
+
Tagged.new(self, fields)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -2,6 +2,7 @@ require 'json'
|
|
|
2
2
|
require 'fileutils'
|
|
3
3
|
|
|
4
4
|
module ComposableAgents
|
|
5
|
+
# Collection of useful mixins that add functionality to agents prepending them.
|
|
5
6
|
module Mixins
|
|
6
7
|
# Mixin adding resumable step capabilities to agents.
|
|
7
8
|
# An agent prepending this mixin can use the following:
|
|
@@ -139,14 +140,14 @@ module ComposableAgents
|
|
|
139
140
|
if step_state == saved_input_state
|
|
140
141
|
@current_step_node[:status] = :cached
|
|
141
142
|
set_current_step_state(saved_output_state, agent:)
|
|
142
|
-
|
|
143
|
+
logger.debug "[Step #{step_full_name}] - Already executed - Got #{@artifacts.size} artifacts from persistence: #{@artifacts.keys.join(', ')}"
|
|
143
144
|
else
|
|
144
145
|
# Clone state before yielding because it will certainly be modified
|
|
145
146
|
input_step_state = clone_step_state(step_state)
|
|
146
147
|
yield
|
|
147
148
|
@current_step_node[:status] = :executed
|
|
148
149
|
store_step_states(step_full_name, input: input_step_state, output: current_step_state(agent:))
|
|
149
|
-
|
|
150
|
+
logger.debug "[Step #{step_full_name}] - Executed - Stored #{@artifacts.size} artifacts in persistence: #{@artifacts.keys.join(', ')}"
|
|
150
151
|
end
|
|
151
152
|
end
|
|
152
153
|
end
|
|
@@ -73,7 +73,7 @@ module ComposableAgents
|
|
|
73
73
|
@output_artifacts = {}
|
|
74
74
|
@output_artifacts_errors = {}
|
|
75
75
|
@system_prompt = render_system_prompt(render_instructions(@system_instructions))
|
|
76
|
-
|
|
76
|
+
logger.debug "System prompt: #{@system_prompt}"
|
|
77
77
|
converse(user_instructions, input_artifacts: @input_artifacts, author: 'User')
|
|
78
78
|
if respond_to?(:normalized_output_artifacts_contracts, true)
|
|
79
79
|
# We know which output artifacts we are expecting.
|
|
@@ -148,7 +148,7 @@ module ComposableAgents
|
|
|
148
148
|
def save_output_artifact(artifact_name, content)
|
|
149
149
|
@output_artifacts[artifact_name] = content
|
|
150
150
|
@output_artifacts_errors.delete(artifact_name)
|
|
151
|
-
|
|
151
|
+
logger.debug "[Artifact] - Received output artifact #{artifact_name}"
|
|
152
152
|
end
|
|
153
153
|
|
|
154
154
|
# Report an error on an output artifact.
|
|
@@ -162,7 +162,7 @@ module ComposableAgents
|
|
|
162
162
|
@output_artifacts.delete(artifact_name)
|
|
163
163
|
@output_artifacts_errors[artifact_name] = error
|
|
164
164
|
# TODO: Make this as a warning message
|
|
165
|
-
|
|
165
|
+
logger.debug "[Artifact] - Should have received content for output artifact `#{artifact_name}` " \
|
|
166
166
|
"but the following error occurred: #{error}"
|
|
167
167
|
end
|
|
168
168
|
|
|
@@ -191,10 +191,10 @@ module ComposableAgents
|
|
|
191
191
|
def converse(instructions, input_artifacts: {}, author: 'Orchestrator')
|
|
192
192
|
rendered_instructions = render_instructions(instructions)
|
|
193
193
|
rendered_user_prompt = render_user_prompt(rendered_instructions, input_artifacts:)
|
|
194
|
-
|
|
194
|
+
logger.debug "Rendered User prompt: #{rendered_user_prompt}"
|
|
195
195
|
track_message(message: rendered_instructions, author:)
|
|
196
196
|
response = prompt(rendered_user_prompt)
|
|
197
|
-
|
|
197
|
+
logger.debug "Raw Agent #{full_name} response: #{response}"
|
|
198
198
|
track_message(message: response, author: "Agent #{full_name}")
|
|
199
199
|
end
|
|
200
200
|
|
|
@@ -169,7 +169,8 @@ module ComposableAgents
|
|
|
169
169
|
# - error [String, nil] An error message related to this missing artifact.
|
|
170
170
|
# @return [Object] The user instructions (see Instructions#initialize)
|
|
171
171
|
def missing_output_user_instructions(missing_output_artifacts)
|
|
172
|
-
|
|
172
|
+
logger.debug '[Artifact] - Asking assistant for missing output artifacts ' \
|
|
173
|
+
"`#{missing_output_artifacts.keys.join(', ')}` to be returned in its next answer."
|
|
173
174
|
<<~EO_PROMPT
|
|
174
175
|
The following output artifacts are missing from your previous responses:
|
|
175
176
|
#{
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: composable_agents
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.
|
|
4
|
+
version: 2.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Muriel Salvan
|
|
@@ -77,13 +77,15 @@ files:
|
|
|
77
77
|
- lib/composable_agents/agent.rb
|
|
78
78
|
- lib/composable_agents/ai_agents/agent.rb
|
|
79
79
|
- lib/composable_agents/ai_agents/tools/ask_user_tool.rb
|
|
80
|
+
- lib/composable_agents/ai_agents/tools/base_tool.rb
|
|
80
81
|
- lib/composable_agents/ai_agents/tools/create_artifact_tool.rb
|
|
81
82
|
- lib/composable_agents/ai_agents/tools/get_artifact_tool.rb
|
|
82
83
|
- lib/composable_agents/cline/agent.rb
|
|
83
84
|
- lib/composable_agents/instructions.rb
|
|
85
|
+
- lib/composable_agents/logger.rb
|
|
86
|
+
- lib/composable_agents/logger/tagged.rb
|
|
84
87
|
- lib/composable_agents/mixins/ai_agent_user_interaction.rb
|
|
85
88
|
- lib/composable_agents/mixins/artifact_contract.rb
|
|
86
|
-
- lib/composable_agents/mixins/logger.rb
|
|
87
89
|
- lib/composable_agents/mixins/resumable.rb
|
|
88
90
|
- lib/composable_agents/mixins/user_interaction.rb
|
|
89
91
|
- lib/composable_agents/prompt_driven_agent.rb
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
module ComposableAgents
|
|
2
|
-
# Collection of useful mixins that add functionality to agents prepending them.
|
|
3
|
-
module Mixins
|
|
4
|
-
# Logging mixin for agents
|
|
5
|
-
module Logger
|
|
6
|
-
# @!group Public API
|
|
7
|
-
|
|
8
|
-
# Check if debug mode is enabled
|
|
9
|
-
#
|
|
10
|
-
# @return [Boolean] True if debug mode is enabled
|
|
11
|
-
def self.debug?
|
|
12
|
-
ENV['COMPOSABLE_AGENTS_DEBUG'] == '1'
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
private
|
|
16
|
-
|
|
17
|
-
# Log debug message only if debug mode is enabled
|
|
18
|
-
#
|
|
19
|
-
# @param message [String, Proc] Message string or Proc returning message for lazy evaluation
|
|
20
|
-
def log_debug(message)
|
|
21
|
-
return unless Logger.debug?
|
|
22
|
-
|
|
23
|
-
log(message, severity: :debug)
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
# Log info message
|
|
27
|
-
#
|
|
28
|
-
# @param message [String, #call => String] Message string or Proc returning message for lazy evaluation
|
|
29
|
-
def log_info(message)
|
|
30
|
-
log(message, severity: :info)
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
# Log a message with severity
|
|
34
|
-
#
|
|
35
|
-
# @param message [String, #call => String] Message string or Proc returning message for lazy evaluation
|
|
36
|
-
# @param severity [Symbol] Severity
|
|
37
|
-
def log(message, severity: :info)
|
|
38
|
-
fields = [
|
|
39
|
-
Time.now.utc.strftime('%F %T'),
|
|
40
|
-
severity.to_s.upcase
|
|
41
|
-
]
|
|
42
|
-
fields.concat(log_fields) if respond_to?(:log_fields, true)
|
|
43
|
-
puts "#{fields.map { |field| "[#{field}]" }.join(' ')} - #{message.is_a?(String) ? message : message.call}"
|
|
44
|
-
end
|
|
45
|
-
end
|
|
46
|
-
end
|
|
47
|
-
end
|