1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
//! Agents module
//!
//! This module defines the `Agent` struct, which is responsible for interacting with
//! a Language Model (LLM) to generate recommendations or enhance existing content.
//!
//! The `Agent` uses a `Handbook` to manage guidelines, input files, and LLM configurations.
//! It supports actions such as recommending improvements or enhancing content
//! based on provided guidelines.
use crate::builder::build_model;
use crate::cli::Action;
use crate::handb::Handbook;
use crate::prompts::RECOMMENDATION_PROMPT_TEMPLATE;
use anyhow::{Result, anyhow};
use futures::future::join_all;
use llm::LLMProvider as ExternalLLMProvider;
use llm::chat::ChatMessage;
use log::{debug, error, info};
use std::env::var;
use std::fs;
use std::path::{Path, PathBuf};
/// Represents an agent capable of interacting with a Language Model.
///
/// The agent uses a `Handbook` to manage guidelines, input files, and LLM configurations.
pub struct Agent {
/// The 'Action' instance representing the type of action: Recommend, Enhance.
/// It determines what the agent is supposed to do.
action: Action,
/// The `Handbook` instance containing the configuration for the agent.
/// It includes guidelines, input files, and LLM configurations.
handbook: Handbook,
}
impl Agent {
/// Creates a new Agent instance.
///
/// # Arguments
///
/// * `action` - The action to be performed by the agent (e.g., Recommend, Enhance).
/// * `handbook` - The handbook containing guidelines, input files, and LLM configurations.
///
/// # Returns
///
/// A `Result` containing the new `Agent` instance, or an error if the creation fails.
///
/// # Errors
///
/// This function does not return a `Result` or throw errors, but the Agent
/// initialization may fail if the provided Handbook is invalid.
pub fn new(action: Action, handbook: Handbook) -> Agent {
debug!("Creating Agent.");
Self { action, handbook }
}
/// Builds the language model based on the configuration specified in the `Action` and `Handbook`.
///
/// This function constructs the LLM (Language Model) based on the provided configurations
/// (compatibility checked at cli.rs).
/// It supports different LLM providers (e.g., Google) and configures parameters
/// such as API keys, model names, max tokens, and temperature.
///
/// # Returns
///
/// A `Result` containing a boxed `ExternalLLMProvider` if the model is built successfully,
/// or an error if the model cannot be built due to configuration issues or API failures.
///
/// # Errors
///
/// This function may return an error if:
/// - The LLM_API_KEY environment variable is not set.
/// - The model fails to build due to invalid configurations.
fn build_model(&self) -> Result<Box<dyn ExternalLLMProvider>> {
debug!("Creating Model.");
match &self.action {
Action::Recommend {
num_recommendations,
model,
max_tokens,
temperature,
llm_provider,
..
} => {
let api_key = var("LLM_API_KEY").expect("LLM_API_KEY environment variable must be set for traiy to work with your llm backend.");
let model = model.to_string();
let max_tokens = max_tokens.unwrap_or(9999);
let temperature = temperature.unwrap_or(0.75);
let num_recomm = num_recommendations.unwrap_or(3);
let system_msg = format!("{} {}", RECOMMENDATION_PROMPT_TEMPLATE, num_recomm);
let llm = build_model(
&self.action,
api_key,
model,
max_tokens,
temperature,
system_msg,
llm_provider,
)?;
Ok(llm)
}
Action::Enhance { .. } => {
unimplemented!(" ==> In progress of implementation <== ")
}
}
}
/// Retrieves a response from the language model.
///
/// # Arguments
///
/// * `task` - The task or query to be sent to the language model.
///
/// # Returns
///
/// A `Result` containing an `Option<String>` with the LLM's response.
/// Returns `Ok(Some(String))` with the LLM's response if the request is successful.
/// Returns `Ok(None)` if the LLM does not provide a response.
/// Returns an `Err` if the request fails.
///
/// # Errors
///
/// This function may return an error if:
/// - The language model fails to build.
/// - The chat request to the language model fails.
async fn get_llm_response(&self, task: &str) -> Result<Option<String>> {
info!("Task: {:?}", &task);
// debug!("Asking model.");
let llm = self.build_model()?;
///////////////////
// TODO
// Diverge by action
///////////////////
// Construct the input messages for the LLM.
let mut input_messages = Vec::with_capacity(6);
input_messages.extend([
ChatMessage::user().content("GUIDELINE:").build(),
ChatMessage::user().content(task).build(),
ChatMessage::user().content("DOCUMENT:").build(),
ChatMessage::user()
.content(&self.handbook.input_csv)
.build(),
]);
if let Some(context) = &self.handbook.context_csv {
if !context.trim().is_empty() {
input_messages.insert(0, ChatMessage::user().content("CONTEXT:").build());
input_messages.insert(1, ChatMessage::user().content(context).build());
}
}
let response = llm.chat(&input_messages).await?;
let response = response.text();
Ok(response)
}
/// Writes the LLM's response to a file.
///
/// # Arguments
///
/// * `file_path` - The path to the file where the response will be written.
/// * `content` - The content to write to the file.
///
/// # Returns
///
/// A `Result` indicating success or an error if writing fails.
///
/// # Errors
///
/// This function may return an error if the file cannot be written to,
/// for example, due to insufficient permissions or the file path
/// not being found.
fn write_response_to_file(&self, file_path: &Path, content: &str) -> Result<()> {
debug!("Writing to output path.");
let file_str = file_path.to_string_lossy();
fs::write(file_path, content)
.map_err(|e| anyhow!("Error writing to file '{}': {}", file_str, e))
}
/// Executes the language model for each guideline provided.
///
/// This function iterates over each guideline, invokes the language model,
/// and collects the responses. It handles potential errors during the
/// execution of each task.
///
/// # Returns
///
/// A `Result` containing a `Vec<Option<String>>` with the LLM's responses for each guideline.
///
/// # Errors
///
/// This function may return an error if:
/// - An error occurs while retrieving the LLM's response for a task.
/// The error message will provide details about the specific task that failed.
pub async fn loop_guidelines(&self) -> Result<Option<String>> {
let guidelines = &self.handbook.guidelines_csv;
let future = self.get_llm_response(&guidelines);
let result = future.await?;
// let result = join_all(futures)
// .await
// .into_iter()
// .map(|x| {
// x.map_err(|e| {
// error!("An error happened while looping this task: {}", e);
// anyhow!("An error happened while looping this task: {}", e)
// })
// .ok()
// .flatten()
// })
// .collect();
// info!("Guidelines looped.");
// Ok(result)
Ok(result)
}
/// Extracts the file stem and original path from the input CSV file specified
/// in the action.
///
/// This function retrieves the path to the input CSV file from the `Action` enum
/// (either `Recommend` or `Enhance`) and extracts the file's stem (the filename
/// without the extension).
///
/// # Returns
///
/// A `Result` containing a tuple:
/// - The `String` of the file stem.
/// - The `PathBuf` of the original input file path.
///
/// # Errors
///
/// This function may return an error if:
/// - The file stem cannot be extracted from the path. This might occur if the
/// path is malformed or does not point to a valid file.
///
/// # Remarks
///
/// The function currently uses `unwrap()` to handle the case where the file stem cannot be extracted.
/// Consider adding more robust error handling to manage cases where the file stem is not present.
fn get_file_stem_and_original_path(&self) -> Result<(String, PathBuf)> {
let input_csv = match &self.action {
Action::Recommend { input_csv, .. } => input_csv,
Action::Enhance { input_csv, .. } => input_csv,
};
let original_path = PathBuf::from(input_csv);
let file_stem = original_path.file_stem().and_then(|s| s.to_str()).unwrap(); // Handle this error ? Maybe not as the file has already been checked
Ok((file_stem.to_string(), original_path))
}
/// Creates the output directory and determines the file stem for output files.
///
/// This function constructs the output directory path based on the input file's name
/// and appends "_traiy" to it, creating a unique directory for the output.
/// It extracts the file stem (name without extension) from the input file path,
/// which is used for naming output files within the newly created directory.
///
/// # Arguments
///
/// * `file_stem` - The stem of the input file name.
/// * `original_path` - The path to the original input file.
///
/// # Returns
///
/// A `Result` containing a tuple:
/// - The `String` of the file stem (name without extension).
/// - The `PathBuf` of the output directory where the results will be stored.
///
/// # Errors
///
/// This function may return an error if:
/// - The output directory cannot be created due to permission issues or other IO errors.
///
/// # Remarks
///
/// The output directory is created as a subdirectory of the original file's parent directory.
/// If the directory already exists, this function does nothing.
fn create_output_dir_and_file_stem(
&self,
file_stem: String,
original_path: PathBuf,
) -> Result<(String, PathBuf)> {
let output_dir_name = format!("{}_traiy", file_stem);
let parent_dir = original_path.parent().unwrap();
let output_dir_path = parent_dir.join(output_dir_name);
fs::create_dir_all(&output_dir_path).map_err(|e| {
anyhow!(
"Failed to create output directory '{}': {}",
output_dir_path.display(),
e
)
})?;
Ok((file_stem, output_dir_path))
}
/// Saves the results to individual files in the output directory.
///
/// This function iterates through the results, constructs a file name for each,
/// and writes the result content to the file.
///
/// # Arguments
///
/// * `results` - A vector of optional strings, where each string is the result of a task.
/// * `file_stem` - The base name for the output files.
/// * `output_dir_path` - The path to the directory where the files will be saved.
///
/// # Errors
///
/// This function may return an error if:
/// - Writing to a file fails.
fn save_results(
&self,
result: Option<String>,
file_stem: String,
output_dir_path: PathBuf,
) -> Result<()> {
// debug
if let Some(response_text) = result {
let new_file_name = format!("{}_recommendations.{}", file_stem, "md");
let output_path = output_dir_path.join(new_file_name);
self.write_response_to_file(&output_path, &response_text)?;
} else {
error!("LLM response was empty for current task");
}
Ok(())
}
/// Executes the recommendation process.
///
/// This function orchestrates the recommendation process by first looping through
/// the guidelines, then creating the output directory and file stem, and finally
/// saving the results to the output directory.
///
/// # Errors
///
/// This function may return an error if:
/// - Looping through the guidelines fails.
/// - Creating the output directory and file stem fails.
/// - Saving the results fails.
pub async fn recommend(&self) -> Result<()> {
debug!("Starting recommendations...");
let results = self.loop_guidelines().await?;
let (file_stem, original_path) = self.get_file_stem_and_original_path()?;
let (file_stem, output_dir_path) =
self.create_output_dir_and_file_stem(file_stem, original_path)?;
let _ = self.save_results(results, file_stem, output_dir_path);
Ok(())
}
// Enhance
// pub async fn enhance(&self) -> Result<()> {
// debug!("Consuming recommendations...");
// get input_file
// get (edited) recommendations
// call llm with both, exclusive prompt
// overwrite input file...?
// which wil serve as for the next recommendations
// and thus a loop
// Ok(())
// }
// pub fn enhance()
// verify folder of recommendations exist
// then prompt it to take those recommendations
// and implement them on a new final file
}