composable_agents 3.6.0 → 3.7.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 +18 -0
- data/README.md +7 -5
- data/lib/composable_agents/prompt_driven_agent.rb +1 -1
- data/lib/composable_agents/prompt_rendering_strategy/markdown_heavy.rb +73 -62
- data/lib/composable_agents/prompt_rendering_strategy/markdown_heavy_files.rb +266 -0
- data/lib/composable_agents/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 753d56bfc73e0b268d107ae37338f3efa465fc1fe559813228aa3498e6544d9e
|
|
4
|
+
data.tar.gz: 21ac2d0a614a3044f392675dd988dce82c4ffcc5cb2c5192ae9a8f32416bab1d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 39d7fb3e1d61adb4ed78ca652b1f1aa5fc9b689cea004305de6ba14a56646725762ffb1c72965a69a516c804c5f3231197b474ebdcc4119830f708c9c85135b0
|
|
7
|
+
data.tar.gz: b2a60316353effb352cd785e54faa5f0b3d07a01f3c46fdf447f0dfdb57dc990f495ed12fddaf70eeb800cccac4b05b71ac14543bf9057c17b32f90a313dd09a
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,21 @@
|
|
|
1
|
+
# [v3.7.0](https://github.com/Muriel-Salvan/composable_agents/compare/v3.6.1...v3.7.0) (2026-11-12 12:08:18)
|
|
2
|
+
|
|
3
|
+
## Global changes
|
|
4
|
+
### Patches
|
|
5
|
+
|
|
6
|
+
* [feat(MarkdownHeavyFiles): implement MarkdownHeavyFiles prompt rendering strategy](https://github.com/Muriel-Salvan/composable_agents/commit/f6d2e057a64a98c79e5d2566893bdd92bc16185e)
|
|
7
|
+
|
|
8
|
+
## Changes for MarkdownHeavyFiles
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
* [feat(MarkdownHeavyFiles): implement MarkdownHeavyFiles prompt rendering strategy](https://github.com/Muriel-Salvan/composable_agents/commit/f6d2e057a64a98c79e5d2566893bdd92bc16185e)
|
|
12
|
+
|
|
13
|
+
# [v3.6.1](https://github.com/Muriel-Salvan/composable_agents/compare/v3.6.0...v3.6.1) (2026-11-07 16:11:50)
|
|
14
|
+
|
|
15
|
+
### Patches
|
|
16
|
+
|
|
17
|
+
* [test: add logger coverage and improve SimpleCov configuration](https://github.com/Muriel-Salvan/composable_agents/commit/68804370a2d54917eb3792ec4932db3fc4941b4a)
|
|
18
|
+
|
|
1
19
|
# [v3.6.0](https://github.com/Muriel-Salvan/composable_agents/compare/v3.5.0...v3.6.0) (2026-11-07 11:35:50)
|
|
2
20
|
|
|
3
21
|
## Global changes
|
data/README.md
CHANGED
|
@@ -203,7 +203,7 @@ itinerary_agent = ComposableAgents::Cline::Agent.new(
|
|
|
203
203
|
- 🧠 **Three agent types** — Create LLM-powered agents via [`PromptDrivenAgent`](lib/composable_agents/prompt_driven_agent.rb), wrap plain Ruby logic with [`RubyAgent`](lib/composable_agents/ruby_agent.rb), or compose complex multi-step workflows using the [`Resumable`](lib/composable_agents/mixins/resumable.rb) mixin.
|
|
204
204
|
- 🔄 **Composable pipelines** — Pass output artifacts from one agent directly as input to another, forming reusable, chainable workflows.
|
|
205
205
|
- 🎯 **Multiple LLM backends** — Plug into different AI providers via the [`ai-agents`](https://github.com/nicbarker/ai-agents) gem (OpenRouter) or [`cline-rb`](https://github.com/Muriel-Salvan/cline-rb) (Claude, GPT, and many more).
|
|
206
|
-
- 📝 **
|
|
206
|
+
- 📝 **Three prompt rendering strategies** — Choose between clean **Markdown** for simple agents, **MarkdownHeavy** with structured output parsing, execution checklists, and typed artifact support for complex agentic systems, or **MarkdownHeavyFiles** which persists input and output artifacts as files on disk.
|
|
207
207
|
- 📦 **Typed artifact contracts** — Define and validate input/output schemas with descriptions, optional flags, and types (`:text`, `:markdown`, `:json`). The framework raises clear `MissingInputArtifactError`, `MissingOutputArtifactError`, or `ArtifactTypeError` on violations.
|
|
208
208
|
- 💾 **Resumable execution** — Persist step-by-step state to disk (via the `Resumable` mixin). Interrupted runs can be resumed seamlessly — previously completed steps are skipped, saving time and API costs.
|
|
209
209
|
- 🔁 **Conditional retries** — Wrap an agent's `#run` with the `Retriable` mixin to retry on transient failures. Register condition procs via `#retry_on` that receive the agent and any raised exception (or `nil`) and return a policy hash (optional `:wait_secs`, optional `:modifier`) to wait-and-retry, or `nil` to skip. Unhandled exceptions are re-raised when no policy applies.
|
|
@@ -292,7 +292,7 @@ The project is a Ruby library (not a CLI) — users install the gem and use its
|
|
|
292
292
|
|
|
293
293
|
#### `ComposableAgents::PromptDrivenAgent < Agent`
|
|
294
294
|
|
|
295
|
-
- **Description:** An agent that uses a **prompt rendering strategy** (Markdown or
|
|
295
|
+
- **Description:** An agent that uses a **prompt rendering strategy** (Markdown, MarkdownHeavy, or MarkdownHeavyFiles) to build prompts for an LLM. It manages role, objective, instructions, constraints, and a conversation history.
|
|
296
296
|
- **Public methods:**
|
|
297
297
|
- `#role` / `#role=` — Agent's role description.
|
|
298
298
|
- `#objective` / `#objective=` — Agent's objective.
|
|
@@ -568,7 +568,7 @@ When a proc returns a policy, the agent waits `wait_secs` seconds, applies the `
|
|
|
568
568
|
### Additional notes
|
|
569
569
|
|
|
570
570
|
- **No executables / CLI** — This gem is a library only; there are no scripts in `bin/`.
|
|
571
|
-
- **Prompt rendering strategies** (`PromptRenderingStrategy::Markdown` and `PromptRenderingStrategy::
|
|
571
|
+
- **Prompt rendering strategies** (`PromptRenderingStrategy::Markdown`, `PromptRenderingStrategy::MarkdownHeavy`, and `PromptRenderingStrategy::MarkdownHeavyFiles`) are not part of the public API themselves — they are included automatically by the agent's `strategy:` parameter.
|
|
572
572
|
- **The `AiAgentUserInteraction` mixin** (`ComposableAgents::Mixins::AiAgentUserInteraction`) is an internal bridge that combines `UserInteraction` with `AiAgents::Agent`; it is used by prepending it to an `AiAgents::Agent` subclass.
|
|
573
573
|
- See the [examples/](https://github.com/Muriel-Salvan/composable_agents/tree/main/examples) directory for complete, runnable scripts demonstrating all the above APIs.
|
|
574
574
|
|
|
@@ -594,6 +594,7 @@ When a proc returns a policy, the agent waits `wait_secs` seconds, applies the `
|
|
|
594
594
|
- [`Logger`](https://github.com/Muriel-Salvan/composable_agents/blob/main/lib/composable_agents/logger.rb) — Standard logger class
|
|
595
595
|
- [`PromptRenderingStrategy::Markdown`](https://github.com/Muriel-Salvan/composable_agents/blob/main/lib/composable_agents/prompt_rendering_strategy/markdown.rb) — Markdown prompt strategy
|
|
596
596
|
- [`PromptRenderingStrategy::MarkdownHeavy`](https://github.com/Muriel-Salvan/composable_agents/blob/main/lib/composable_agents/prompt_rendering_strategy/markdown_heavy.rb) — Heavy Markdown prompt strategy
|
|
597
|
+
- [`PromptRenderingStrategy::MarkdownHeavyFiles`](https://github.com/Muriel-Salvan/composable_agents/blob/main/lib/composable_agents/prompt_rendering_strategy/markdown_heavy_files.rb) — Heavy Markdown prompt strategy persisting artifacts as files
|
|
597
598
|
- [`Utils::Markdown`](https://github.com/Muriel-Salvan/composable_agents/blob/main/lib/composable_agents/utils/markdown.rb) — Markdown header alignment utilities
|
|
598
599
|
|
|
599
600
|
## How it works
|
|
@@ -674,10 +675,11 @@ flowchart TD
|
|
|
674
675
|
|
|
675
676
|
### Prompt rendering strategies 📝
|
|
676
677
|
|
|
677
|
-
|
|
678
|
+
Three strategies are included, mixed into the agent at initialization via `singleton_class.include strategy`:
|
|
678
679
|
|
|
679
680
|
- **`PromptRenderingStrategy::Markdown`** — Clean, minimal Markdown. Simple instructions and artifact references.
|
|
680
681
|
- **`PromptRenderingStrategy::MarkdownHeavy`** (default for Cline) — Elaborate prompts with execution checklists, structured artifact definition sections, and **JSON-based output parsing**. Agents format their artifacts as JSON blocks tagged with `output_artifact=NAME`, which the strategy parses back into the output hash. Includes type-aware parsing (`:text`, `:markdown`, `:json`).
|
|
682
|
+
- **`PromptRenderingStrategy::MarkdownHeavyFiles`** — Same heavy Markdown behavior as `MarkdownHeavy`, but artifacts are persisted as files on disk instead of being in-lined or returned as JSON. Input artifacts are written to files that the assistant is asked to open, and output artifacts are expected as files that are read back from disk. Accepts an `artifacts_directory` constructor parameter (defaults to `#{composable_agents_dir}/artifacts`).
|
|
681
683
|
|
|
682
684
|
### LLM backends: ai-agents vs cline-rb 🎯
|
|
683
685
|
|
|
@@ -764,7 +766,7 @@ npm install node-pty
|
|
|
764
766
|
│ ├── ai_agents/ # ai-agents backend
|
|
765
767
|
│ ├── cline/ # cline-rb backend
|
|
766
768
|
│ ├── mixins/ # ArtifactContract, Logger, Resumable, Retriable, RetriableArtifacts, RetriableCline, UserInteraction
|
|
767
|
-
│ ├── prompt_rendering_strategy/ # Markdown &
|
|
769
|
+
│ ├── prompt_rendering_strategy/ # Markdown, MarkdownHeavy & MarkdownHeavyFiles strategies
|
|
768
770
|
│ └── utils/ # Markdown utilities
|
|
769
771
|
├── spec/ # Test suite
|
|
770
772
|
│ ├── scenarios/ # RSpec test cases
|
|
@@ -52,8 +52,8 @@ module ComposableAgents
|
|
|
52
52
|
strategy: PromptRenderingStrategy::Markdown,
|
|
53
53
|
**kwargs
|
|
54
54
|
)
|
|
55
|
-
super(*args, **kwargs)
|
|
56
55
|
singleton_class.include strategy
|
|
56
|
+
super(*args, **kwargs)
|
|
57
57
|
@role = role
|
|
58
58
|
@objective = objective
|
|
59
59
|
@system_instructions = system_instructions
|
|
@@ -59,68 +59,8 @@ module ComposableAgents
|
|
|
59
59
|
sections = [super]
|
|
60
60
|
# Don't document the user instructions themselves
|
|
61
61
|
input_contracts = normalized_input_artifacts_contracts.except(:user_instructions)
|
|
62
|
-
unless input_contracts.empty?
|
|
63
|
-
|
|
64
|
-
# Input artifacts' concept and usage
|
|
65
|
-
|
|
66
|
-
- Artifacts are documents that you can get as input.
|
|
67
|
-
- Each artifact is identified by a name, like `#{artifact_ref(:plan)}`.
|
|
68
|
-
- You must consider all artifacts given in the "Input artifacts" section of the user prompt.
|
|
69
|
-
- All artifacts presented in the "Input artifacts" section of the user prompt provide their content in an in-line JSON document.
|
|
70
|
-
#{
|
|
71
|
-
input_contracts.map do |artifact_name, artifact_contract|
|
|
72
|
-
art_ref = artifact_ref(artifact_name)
|
|
73
|
-
[
|
|
74
|
-
"- The content of input artifact `#{art_ref}` describes this: #{artifact_contract[:description]}"
|
|
75
|
-
] + (
|
|
76
|
-
if artifact_contract[:optional]
|
|
77
|
-
["- The input artifact `#{art_ref}` is optional and may not be given to you."]
|
|
78
|
-
else
|
|
79
|
-
["- The input artifact `#{art_ref}` is expected to be in the user prompt."]
|
|
80
|
-
end
|
|
81
|
-
) + [
|
|
82
|
-
"- The input artifact `#{art_ref}` artifact content is embedded directly in the user prompt as in-line JSON. It is NOT a file. Do NOT try to open it as a file."
|
|
83
|
-
]
|
|
84
|
-
end.compact.flatten(1).join("\n")
|
|
85
|
-
}
|
|
86
|
-
EO_SECTION
|
|
87
|
-
end
|
|
88
|
-
unless normalized_output_artifacts_contracts.empty?
|
|
89
|
-
sections << <<~EO_SECTION
|
|
90
|
-
# Output artifacts' concept and usage
|
|
91
|
-
|
|
92
|
-
- The user will ask you to provide some artifacts as output.
|
|
93
|
-
- You must always return the required artifact as a JSON document in your response, with its name in the JSON header, like this:
|
|
94
|
-
```json output_artifact=#{artifact_ref(:name)}
|
|
95
|
-
{artifact_content}
|
|
96
|
-
```
|
|
97
|
-
- Do not create files for output artifacts: always give them inside embedded JSON in your last response.
|
|
98
|
-
- Always return output artifacts that the user is asking you to provide.
|
|
99
|
-
- You can return several output artifacts in your responses if needed.
|
|
100
|
-
|
|
101
|
-
Following sections enumerate all expected output artifacts.
|
|
102
|
-
|
|
103
|
-
#{
|
|
104
|
-
normalized_output_artifacts_contracts.map do |artifact_name, artifact_contract|
|
|
105
|
-
art_ref = artifact_ref(artifact_name)
|
|
106
|
-
(
|
|
107
|
-
[
|
|
108
|
-
"## Output artifact `#{art_ref}`",
|
|
109
|
-
'',
|
|
110
|
-
"- The content of output artifact `#{art_ref}` should describe this: #{artifact_contract[:description]}"
|
|
111
|
-
] + (
|
|
112
|
-
artifact_contract[:type] ? ["- The output artifact `#{art_ref}` content format should be #{artifact_contract[:type]}"] : []
|
|
113
|
-
) + [
|
|
114
|
-
<<~EO_ITEM.strip
|
|
115
|
-
- The output artifact `#{art_ref}` should be given in a block like this:
|
|
116
|
-
#{example_json_block(artifact_name)}
|
|
117
|
-
EO_ITEM
|
|
118
|
-
]
|
|
119
|
-
).join("\n")
|
|
120
|
-
end.join("\n\n")
|
|
121
|
-
}
|
|
122
|
-
EO_SECTION
|
|
123
|
-
end
|
|
62
|
+
sections << render_input_artifacts_system_prompt_section(input_contracts) unless input_contracts.empty?
|
|
63
|
+
sections << render_output_artifacts_system_prompt_section unless normalized_output_artifacts_contracts.empty?
|
|
124
64
|
sections.map(&:strip).join("\n\n")
|
|
125
65
|
end
|
|
126
66
|
|
|
@@ -288,6 +228,77 @@ module ComposableAgents
|
|
|
288
228
|
|
|
289
229
|
private
|
|
290
230
|
|
|
231
|
+
# Render the input artifacts concept section of the system prompt.
|
|
232
|
+
#
|
|
233
|
+
# @param input_contracts [Hash{Symbol => Hash{Symbol => Object}}] The normalized input artifacts contracts
|
|
234
|
+
# @return [String] The rendered input artifacts' concept and usage section
|
|
235
|
+
def render_input_artifacts_system_prompt_section(input_contracts)
|
|
236
|
+
<<~EO_SECTION
|
|
237
|
+
# Input artifacts' concept and usage
|
|
238
|
+
|
|
239
|
+
- Artifacts are documents that you can get as input.
|
|
240
|
+
- Each artifact is identified by a name, like `#{artifact_ref(:plan)}`.
|
|
241
|
+
- You must consider all artifacts given in the "Input artifacts" section of the user prompt.
|
|
242
|
+
- All artifacts presented in the "Input artifacts" section of the user prompt provide their content in an in-line JSON document.
|
|
243
|
+
#{
|
|
244
|
+
input_contracts.map do |artifact_name, artifact_contract|
|
|
245
|
+
art_ref = artifact_ref(artifact_name)
|
|
246
|
+
[
|
|
247
|
+
"- The content of input artifact `#{art_ref}` describes this: #{artifact_contract[:description]}"
|
|
248
|
+
] + (
|
|
249
|
+
if artifact_contract[:optional]
|
|
250
|
+
["- The input artifact `#{art_ref}` is optional and may not be given to you."]
|
|
251
|
+
else
|
|
252
|
+
["- The input artifact `#{art_ref}` is expected to be in the user prompt."]
|
|
253
|
+
end
|
|
254
|
+
) + [
|
|
255
|
+
"- The input artifact `#{art_ref}` artifact content is embedded directly in the user prompt as in-line JSON. It is NOT a file. Do NOT try to open it as a file."
|
|
256
|
+
]
|
|
257
|
+
end.compact.flatten(1).join("\n")
|
|
258
|
+
}
|
|
259
|
+
EO_SECTION
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
# Render the output artifacts concept section of the system prompt.
|
|
263
|
+
#
|
|
264
|
+
# @return [String] The rendered output artifacts' concept and usage section
|
|
265
|
+
def render_output_artifacts_system_prompt_section
|
|
266
|
+
<<~EO_SECTION
|
|
267
|
+
# Output artifacts' concept and usage
|
|
268
|
+
|
|
269
|
+
- The user will ask you to provide some artifacts as output.
|
|
270
|
+
- You must always return the required artifact as a JSON document in your response, with its name in the JSON header, like this:
|
|
271
|
+
```json output_artifact=#{artifact_ref(:name)}
|
|
272
|
+
{artifact_content}
|
|
273
|
+
```
|
|
274
|
+
- Do not create files for output artifacts: always give them inside embedded JSON in your last response.
|
|
275
|
+
- Always return output artifacts that the user is asking you to provide.
|
|
276
|
+
- You can return several output artifacts in your responses if needed.
|
|
277
|
+
|
|
278
|
+
Following sections enumerate all expected output artifacts.
|
|
279
|
+
|
|
280
|
+
#{
|
|
281
|
+
normalized_output_artifacts_contracts.map do |artifact_name, artifact_contract|
|
|
282
|
+
art_ref = artifact_ref(artifact_name)
|
|
283
|
+
(
|
|
284
|
+
[
|
|
285
|
+
"## Output artifact `#{art_ref}`",
|
|
286
|
+
'',
|
|
287
|
+
"- The content of output artifact `#{art_ref}` should describe this: #{artifact_contract[:description]}"
|
|
288
|
+
] + (
|
|
289
|
+
artifact_contract[:type] ? ["- The output artifact `#{art_ref}` content format should be #{artifact_contract[:type]}"] : []
|
|
290
|
+
) + [
|
|
291
|
+
<<~EO_ITEM.strip
|
|
292
|
+
- The output artifact `#{art_ref}` should be given in a block like this:
|
|
293
|
+
#{example_json_block(artifact_name)}
|
|
294
|
+
EO_ITEM
|
|
295
|
+
]
|
|
296
|
+
).join("\n")
|
|
297
|
+
end.join("\n\n")
|
|
298
|
+
}
|
|
299
|
+
EO_SECTION
|
|
300
|
+
end
|
|
301
|
+
|
|
291
302
|
# Provide a Markdown example of a JSON block for a given output artifact
|
|
292
303
|
#
|
|
293
304
|
# @param artifact_name [Symbol] The output artifact name
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
require 'fileutils'
|
|
3
|
+
|
|
4
|
+
module ComposableAgents
|
|
5
|
+
module PromptRenderingStrategy
|
|
6
|
+
# Render prompt as Markdown documents with a lot of emphasis for complex agentic systems,
|
|
7
|
+
# where input and output artifacts are persisted as files on disk instead of being in-lined
|
|
8
|
+
# as JSON in the prompts and responses.
|
|
9
|
+
# This prompt strategy needs to be used conjointly with the ArtifactContract mixin.
|
|
10
|
+
# It inherits from the {MarkdownHeavy} strategy and only differs in the way artifacts are handled:
|
|
11
|
+
# - Input artifacts are given to the assistant as absolute paths to files persisted on disk.
|
|
12
|
+
# - Output artifacts are expected to be written by the assistant to files, at absolute paths
|
|
13
|
+
# that are then read back by {#parse_output_artifacts}.
|
|
14
|
+
# This mixin also adds the following methods to be used:
|
|
15
|
+
# - `#parse_output_artifacts(text)` Parses a text that comes from the agent to retrieve output artifacts from files.
|
|
16
|
+
# - `#artifact_ref(artifact_name) -> String` Returns the absolute path of the artifact file as seen by the assistant.
|
|
17
|
+
# This can be used to refer to the artifact names properly in the user or system instructions.
|
|
18
|
+
# The constructor accepts the following extra parameter:
|
|
19
|
+
# - `artifacts_directory` [String, nil] Directory where artifacts are persisted, or nil to default to
|
|
20
|
+
# `#{composable_agents_dir}/artifacts`. Input and output artifacts are persisted in the
|
|
21
|
+
# `input` and `output` sub-directories respectively.
|
|
22
|
+
module MarkdownHeavyFiles
|
|
23
|
+
# @!group Internal
|
|
24
|
+
|
|
25
|
+
include MarkdownHeavy
|
|
26
|
+
|
|
27
|
+
# Constructor
|
|
28
|
+
#
|
|
29
|
+
# @param artifacts_directory [String, nil] Directory where artifacts are persisted, or nil to default to
|
|
30
|
+
# `#{composable_agents_dir}/artifacts`
|
|
31
|
+
def initialize(*args, artifacts_directory: nil, **kwargs)
|
|
32
|
+
super(*args, **kwargs)
|
|
33
|
+
artifacts_directory ||= "#{@composable_agents_dir}/artifacts"
|
|
34
|
+
@input_artifacts_directory = File.join(artifacts_directory, 'input')
|
|
35
|
+
@output_artifacts_directory = File.join(artifacts_directory, 'output')
|
|
36
|
+
FileUtils.mkdir_p(@input_artifacts_directory)
|
|
37
|
+
FileUtils.mkdir_p(@output_artifacts_directory)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Render the user prompt
|
|
41
|
+
# The following instance variables are accessible to render the prompt:
|
|
42
|
+
# - `@role`
|
|
43
|
+
# - `@objective`
|
|
44
|
+
# - `@constraints`
|
|
45
|
+
#
|
|
46
|
+
# @param rendered_instructions [String, nil] The rendered instructions, or nil if none
|
|
47
|
+
# @param input_artifacts [Hash{Symbol => Object}] The input artifacts content for which we render this prompt, per artifact name
|
|
48
|
+
# @return [String] The rendered user prompt
|
|
49
|
+
def render_user_prompt(rendered_instructions, input_artifacts:)
|
|
50
|
+
sections = []
|
|
51
|
+
unless input_artifacts.empty?
|
|
52
|
+
sections << <<~EO_SECTION.strip
|
|
53
|
+
# Input artifacts
|
|
54
|
+
|
|
55
|
+
The following input artifacts are persisted as files on disk. Open and read each file whenever you need the corresponding input artifact content:
|
|
56
|
+
|
|
57
|
+
#{
|
|
58
|
+
input_artifacts.map do |artifact_name, artifact_content|
|
|
59
|
+
art_path = persist_input_artifact(artifact_name, artifact_content)
|
|
60
|
+
<<~EO_ARTIFACT_SECTION.strip
|
|
61
|
+
## `#{art_path}`
|
|
62
|
+
|
|
63
|
+
- Open and read the file `#{art_path}` whenever you need the corresponding input artifact content.
|
|
64
|
+
EO_ARTIFACT_SECTION
|
|
65
|
+
end.join("\n\n")
|
|
66
|
+
}
|
|
67
|
+
EO_SECTION
|
|
68
|
+
end
|
|
69
|
+
sections << <<~EO_SECTION if rendered_instructions && !rendered_instructions.empty?
|
|
70
|
+
# User instructions
|
|
71
|
+
|
|
72
|
+
#{Utils::Markdown.align_markdown_headers(rendered_instructions, level: 2)}
|
|
73
|
+
EO_SECTION
|
|
74
|
+
sections.map(&:strip).join("\n\n")
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Get the artifact reference communicated to the assistant, as the absolute path of its file.
|
|
78
|
+
#
|
|
79
|
+
# @param artifact_name [Symbol] The artifact name
|
|
80
|
+
# @return [String] The absolute path of the artifact file used for the assistant
|
|
81
|
+
def artifact_ref(artifact_name)
|
|
82
|
+
if normalized_input_artifacts_contracts.key?(artifact_name)
|
|
83
|
+
input_artifact_path(artifact_name)
|
|
84
|
+
elsif normalized_output_artifacts_contracts.key?(artifact_name)
|
|
85
|
+
output_artifact_path(artifact_name)
|
|
86
|
+
else
|
|
87
|
+
raise "Unknown artifact: #{artifact_name}"
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Get user instructions for missing output artifacts
|
|
92
|
+
#
|
|
93
|
+
# @param missing_output_artifacts [Hash{Symbol => Object}] The missing output artifacts information, per artifact name
|
|
94
|
+
# Information can contain the following attributes:
|
|
95
|
+
# - description [String] The artifact's description.
|
|
96
|
+
# - error [String, nil] An error message related to this missing artifact.
|
|
97
|
+
# @return [Object] The user instructions (see Instructions#initialize)
|
|
98
|
+
def missing_output_user_instructions(missing_output_artifacts)
|
|
99
|
+
logger.debug '[Artifact] - Asking assistant for missing output artifacts ' \
|
|
100
|
+
"`#{missing_output_artifacts.keys.join(', ')}` to be written as files in its next answer."
|
|
101
|
+
<<~EO_PROMPT
|
|
102
|
+
The following output artifacts are missing from your previous responses:
|
|
103
|
+
#{
|
|
104
|
+
missing_output_artifacts.map do |artifact_name, missing_info|
|
|
105
|
+
["- `#{output_artifact_path(artifact_name)}`: #{missing_info[:description]}"] +
|
|
106
|
+
(missing_info[:error] ? [" An error occurred while reading this artifact from your previous responses: #{missing_info[:error]}"] : [])
|
|
107
|
+
end.flatten(1).join("\n")
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
You must write each one of them to its expected file path (the absolute paths given above) in your next response (MANDATORY).
|
|
111
|
+
EO_PROMPT
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Parse some text to find output artifacts in the files they are persisted to.
|
|
115
|
+
#
|
|
116
|
+
# @param text [String] The text to be parsed
|
|
117
|
+
def parse_output_artifacts(text)
|
|
118
|
+
logger.debug "[Artifact] - Looking for output artifacts persisted as files (ignoring the assistant text response): #{text}"
|
|
119
|
+
normalized_output_artifacts_contracts.each_key do |artifact_name|
|
|
120
|
+
artifact_path = output_artifact_path(artifact_name)
|
|
121
|
+
next unless File.exist?(artifact_path)
|
|
122
|
+
|
|
123
|
+
artifact_type = normalized_output_artifacts_contracts.dig(artifact_name, :type)
|
|
124
|
+
artifact_content =
|
|
125
|
+
begin
|
|
126
|
+
read_output_artifact(artifact_path, artifact_type)
|
|
127
|
+
rescue JSON::ParserError => e
|
|
128
|
+
report_error_for_output_artifact(artifact_name, e.to_s)
|
|
129
|
+
nil
|
|
130
|
+
end
|
|
131
|
+
save_output_artifact(artifact_name, artifact_content) if artifact_content
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
private
|
|
136
|
+
|
|
137
|
+
# Render the input artifacts concept section of the system prompt.
|
|
138
|
+
#
|
|
139
|
+
# @param input_contracts [Hash{Symbol => Hash{Symbol => Object}}] The normalized input artifacts contracts
|
|
140
|
+
# @return [String] The rendered input artifacts' concept and usage section
|
|
141
|
+
def render_input_artifacts_system_prompt_section(input_contracts)
|
|
142
|
+
<<~EO_SECTION
|
|
143
|
+
# Input artifacts' concept and usage
|
|
144
|
+
|
|
145
|
+
- Artifacts are documents that you can get as input.
|
|
146
|
+
- Each input artifact is persisted as a file on disk, identified by its absolute path, like `#{input_artifact_path(input_contracts.keys.first)}`.
|
|
147
|
+
- You must consider all artifacts given in the "Input artifacts" section of the user prompt.
|
|
148
|
+
- The "Input artifacts" section of the user prompt gives you the absolute path of each input artifact file.
|
|
149
|
+
- Open and read the file at the given absolute path whenever you need the corresponding input artifact content.
|
|
150
|
+
- The file extension indicates the artifact content type: `.json` for JSON, `.md` for Markdown, `.txt` for raw text.
|
|
151
|
+
#{
|
|
152
|
+
input_contracts.map do |artifact_name, artifact_contract|
|
|
153
|
+
art_ref = input_artifact_path(artifact_name)
|
|
154
|
+
[
|
|
155
|
+
"- The content of input artifact `#{art_ref}` describes this: #{artifact_contract[:description]}"
|
|
156
|
+
] + (
|
|
157
|
+
if artifact_contract[:optional]
|
|
158
|
+
["- The input artifact `#{art_ref}` is optional and may not be given to you."]
|
|
159
|
+
else
|
|
160
|
+
["- The input artifact `#{art_ref}` is expected in the user prompt."]
|
|
161
|
+
end
|
|
162
|
+
)
|
|
163
|
+
end.flatten(1).join("\n")
|
|
164
|
+
}
|
|
165
|
+
EO_SECTION
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Render the output artifacts concept section of the system prompt.
|
|
169
|
+
#
|
|
170
|
+
# @return [String] The rendered output artifacts' concept and usage section
|
|
171
|
+
def render_output_artifacts_system_prompt_section
|
|
172
|
+
<<~EO_SECTION
|
|
173
|
+
# Output artifacts' concept and usage
|
|
174
|
+
|
|
175
|
+
- The user will ask you to provide some artifacts as output.
|
|
176
|
+
- Each output artifact must be written to a file on disk, at the exact absolute path given in this system prompt.
|
|
177
|
+
- Do NOT return output artifacts inline in your response: always write each one of them to its expected file path.
|
|
178
|
+
- Always return output artifacts that the user is asking you to provide.
|
|
179
|
+
- You can return several output artifacts in your responses if needed.
|
|
180
|
+
|
|
181
|
+
Following sections enumerate all expected output artifacts.
|
|
182
|
+
|
|
183
|
+
#{
|
|
184
|
+
normalized_output_artifacts_contracts.map do |artifact_name, artifact_contract|
|
|
185
|
+
art_ref = output_artifact_path(artifact_name)
|
|
186
|
+
(
|
|
187
|
+
[
|
|
188
|
+
"## Output artifact `#{art_ref}`",
|
|
189
|
+
'',
|
|
190
|
+
"- The content of output artifact `#{art_ref}` should describe this: #{artifact_contract[:description]}"
|
|
191
|
+
] + (
|
|
192
|
+
artifact_contract[:type] ? ["- The output artifact `#{art_ref}` content format should be #{artifact_contract[:type]}"] : []
|
|
193
|
+
) + [
|
|
194
|
+
"- Write the output artifact `#{art_ref}` to this exact absolute path. The file extension indicates the expected content type."
|
|
195
|
+
]
|
|
196
|
+
).join("\n")
|
|
197
|
+
end.join("\n\n")
|
|
198
|
+
}
|
|
199
|
+
EO_SECTION
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# Get the file extension to use for a given artifact type.
|
|
203
|
+
#
|
|
204
|
+
# @param artifact_type [Symbol, nil] The artifact type
|
|
205
|
+
# @return [String] The corresponding file extension
|
|
206
|
+
def artifact_extension(artifact_type)
|
|
207
|
+
case artifact_type
|
|
208
|
+
when :text then '.txt'
|
|
209
|
+
when :markdown then '.md'
|
|
210
|
+
when :json, nil then '.json'
|
|
211
|
+
else raise "Unknown artifact type: #{artifact_type}"
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# Get the absolute path of an input artifact file.
|
|
216
|
+
#
|
|
217
|
+
# @param artifact_name [Symbol] The input artifact name
|
|
218
|
+
# @return [String] The absolute path of the input artifact file
|
|
219
|
+
def input_artifact_path(artifact_name)
|
|
220
|
+
File.join(
|
|
221
|
+
@input_artifacts_directory,
|
|
222
|
+
"#{artifact_name}#{artifact_extension(normalized_input_artifacts_contracts.dig(artifact_name, :type))}"
|
|
223
|
+
)
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# Get the absolute path of an output artifact file.
|
|
227
|
+
#
|
|
228
|
+
# @param artifact_name [Symbol] The output artifact name
|
|
229
|
+
# @return [String] The absolute path of the output artifact file
|
|
230
|
+
def output_artifact_path(artifact_name)
|
|
231
|
+
File.join(
|
|
232
|
+
@output_artifacts_directory,
|
|
233
|
+
"#{artifact_name}#{artifact_extension(normalized_output_artifacts_contracts.dig(artifact_name, :type))}"
|
|
234
|
+
)
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# Persist an input artifact to its file.
|
|
238
|
+
#
|
|
239
|
+
# @param artifact_name [Symbol] The input artifact name
|
|
240
|
+
# @param artifact_content [Object] The input artifact content
|
|
241
|
+
# @return [String] The absolute path where the artifact was persisted
|
|
242
|
+
def persist_input_artifact(artifact_name, artifact_content)
|
|
243
|
+
artifact_path = input_artifact_path(artifact_name)
|
|
244
|
+
artifact_type = normalized_input_artifacts_contracts.dig(artifact_name, :type)
|
|
245
|
+
File.write(
|
|
246
|
+
artifact_path,
|
|
247
|
+
%i[text markdown].include?(artifact_type) ? artifact_content : JSON.pretty_generate(artifact_content)
|
|
248
|
+
)
|
|
249
|
+
artifact_path
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
# Read an output artifact from its file.
|
|
253
|
+
#
|
|
254
|
+
# @param artifact_path [String] The absolute path of the output artifact file
|
|
255
|
+
# @param artifact_type [Symbol, nil] The output artifact type
|
|
256
|
+
# @return [Object] The output artifact content
|
|
257
|
+
def read_output_artifact(artifact_path, artifact_type)
|
|
258
|
+
if %i[text markdown].include?(artifact_type)
|
|
259
|
+
File.read(artifact_path)
|
|
260
|
+
else
|
|
261
|
+
JSON.parse(File.read(artifact_path))
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
end
|
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: 3.
|
|
4
|
+
version: 3.7.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Muriel Salvan
|
|
@@ -98,6 +98,7 @@ files:
|
|
|
98
98
|
- lib/composable_agents/prompt_driven_agent.rb
|
|
99
99
|
- lib/composable_agents/prompt_rendering_strategy/markdown.rb
|
|
100
100
|
- lib/composable_agents/prompt_rendering_strategy/markdown_heavy.rb
|
|
101
|
+
- lib/composable_agents/prompt_rendering_strategy/markdown_heavy_files.rb
|
|
101
102
|
- lib/composable_agents/ruby_agent.rb
|
|
102
103
|
- lib/composable_agents/run_info.rb
|
|
103
104
|
- lib/composable_agents/utils/markdown.rb
|