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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 491a6d82dbce4b508dc8d83cda6d067b1ad13db47c0099e682beb900bf7964de
4
- data.tar.gz: 1b488811d68e2d2233621111cf053df6e1b55fd15d7a686502b8eded4a4a04e7
3
+ metadata.gz: 753d56bfc73e0b268d107ae37338f3efa465fc1fe559813228aa3498e6544d9e
4
+ data.tar.gz: 21ac2d0a614a3044f392675dd988dce82c4ffcc5cb2c5192ae9a8f32416bab1d
5
5
  SHA512:
6
- metadata.gz: 50c6da2263939742f55d9f8e324e5ffc78ab266c053b5831918520cb2c1cbb0377e7980b9a979984446260abfec1f2fe05e9b27e72ba9276594b49106ae0d071
7
- data.tar.gz: dad24a674070a0dc75c0c9b7eb3c04fd70140b521a12ff8058c86b16856150d828cf08934bd9506ab8af9df4f4065e6ebf615e626cd75ed94a73133e756944eb
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
- - 📝 **Two prompt rendering strategies** — Choose between clean **Markdown** for simple agents, or **MarkdownHeavy** with structured output parsing, execution checklists, and typed artifact support for complex agentic systems.
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 MarkdownHeavy) to build prompts for an LLM. It manages role, objective, instructions, constraints, and a conversation history.
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::MarkdownHeavy`) are not part of the public API themselves — they are included automatically by the agent's `strategy:` parameter.
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
- Two strategies are included, mixed into the agent at initialization via `singleton_class.include strategy`:
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 & MarkdownHeavy strategies
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
- sections << <<~EO_SECTION
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
@@ -2,5 +2,5 @@ module ComposableAgents
2
2
  # @!group Public API
3
3
 
4
4
  # Gem version
5
- VERSION = '3.6.0'
5
+ VERSION = '3.7.0'
6
6
  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.6.0
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