use crate::Result;
use crate::agent::agent_options::AgentOptions;
use crate::agent::agent_ref::AgentRef;
use crate::agent::{Agent, AgentInner, PartKind, PromptPart, get_prompt_part_kind, get_prompt_part_options_str};
use crate::support::md::InBlockState;
use crate::support::tomls::parse_toml;
use genai::ModelName;
use simple_fs::{SPath, read_to_string};
use std::path::Path;
use std::sync::Arc;
#[derive(Debug)]
pub struct AgentDoc {
spath: SPath,
raw_content: String,
}
#[derive(Debug)]
enum CaptureMode {
None,
BeforeAllSection,
BeforeAllCodeBlock,
OptionsSection,
OptionsTomlBlock,
DataSection,
DataCodeBlock,
PromptPart,
OutputSection,
OutputCodeBlock,
AfterAllSection,
AfterAllCodeBlock,
}
impl CaptureMode {
#[allow(unused)]
fn is_inside_actionable_block(&self) -> bool {
matches!(
self,
CaptureMode::OptionsTomlBlock
| CaptureMode::BeforeAllCodeBlock
| CaptureMode::DataCodeBlock
| CaptureMode::OutputCodeBlock
| CaptureMode::AfterAllCodeBlock
)
}
fn is_prompt_part(&self) -> bool {
matches!(self, CaptureMode::PromptPart)
}
}
impl AgentDoc {
pub fn from_file(path: impl AsRef<Path>) -> Result<Self> {
let spath = SPath::from_std_path(path.as_ref())?;
let raw_content = read_to_string(path)?;
Ok(Self { spath, raw_content })
}
pub fn into_agent(self, name: &str, agent_ref: AgentRef, options: AgentOptions) -> Result<Agent> {
let agent_inner = self.into_agent_inner(name, agent_ref, options)?;
let agent = Agent::new(agent_inner)?;
Ok(agent)
}
fn into_agent_inner(self, name: &str, agent_ref: AgentRef, agent_options: AgentOptions) -> Result<AgentInner> {
let mut capture_mode = CaptureMode::None;
let mut options_toml: Vec<&str> = Vec::new();
let mut before_all_script: Vec<&str> = Vec::new();
let mut data_script: Vec<&str> = Vec::new();
let mut output_script: Vec<&str> = Vec::new();
let mut after_all_script: Vec<&str> = Vec::new();
let mut prompt_parts: Vec<PromptPart> = Vec::new();
let mut current_part: Option<CurrentPromptPart> = None;
let mut block_state = InBlockState::Out;
for line in self.raw_content.lines() {
let old_block_state = block_state;
block_state = block_state.compute_new(line);
if block_state.is_out() && line.starts_with('#') && !line.starts_with("##") {
let header_lower = line[1..].trim().to_lowercase();
if header_lower == "options" {
capture_mode = CaptureMode::OptionsSection;
} else if header_lower == "before all" {
capture_mode = CaptureMode::BeforeAllSection;
} else if header_lower == "data" {
capture_mode = CaptureMode::DataSection;
} else if header_lower == "output" {
capture_mode = CaptureMode::OutputSection;
} else if header_lower == "after all" {
capture_mode = CaptureMode::AfterAllSection;
} else if let Some(part_kind) = get_prompt_part_kind(&header_lower) {
capture_mode = CaptureMode::PromptPart;
let part_options_str = get_prompt_part_options_str(&header_lower)?;
finalize_current_prompt_part(&mut current_part, &mut prompt_parts);
current_part = Some(CurrentPromptPart(part_kind, part_options_str, Vec::new()));
} else if capture_mode.is_prompt_part() && !block_state.is_out() {
if let Some(current_part) = &mut current_part {
current_part.2.push(line);
}
} else {
capture_mode = CaptureMode::None;
}
continue;
}
match capture_mode {
CaptureMode::None => {}
CaptureMode::PromptPart => {
if let Some(current_part) = &mut current_part {
current_part.2.push(line);
}
}
CaptureMode::OptionsSection => {
if (line.starts_with("```toml") || line.starts_with("````toml")) && old_block_state.is_out() {
capture_mode = CaptureMode::OptionsTomlBlock;
continue;
}
}
CaptureMode::OptionsTomlBlock => {
if line.starts_with("```") && block_state.is_out() && !old_block_state.is_out() {
capture_mode = CaptureMode::None;
continue;
} else {
push_line(&mut options_toml, line);
}
}
CaptureMode::BeforeAllSection => {
if (line.starts_with("```lua") || line.starts_with("````lua")) && old_block_state.is_out() {
capture_mode = CaptureMode::BeforeAllCodeBlock;
continue;
}
}
CaptureMode::BeforeAllCodeBlock => {
if line.starts_with("```") && block_state.is_out() && !old_block_state.is_out() {
capture_mode = CaptureMode::None;
continue;
} else {
push_line(&mut before_all_script, line);
}
}
CaptureMode::DataSection => {
if (line.starts_with("```lua") || line.starts_with("````lua")) && old_block_state.is_out() {
capture_mode = CaptureMode::DataCodeBlock;
continue;
}
}
CaptureMode::DataCodeBlock => {
if line.starts_with("```") && block_state.is_out() && !old_block_state.is_out() {
capture_mode = CaptureMode::None;
continue;
} else {
push_line(&mut data_script, line);
}
}
CaptureMode::OutputSection => {
if (line.starts_with("```lua") || line.starts_with("````lua")) && old_block_state.is_out() {
capture_mode = CaptureMode::OutputCodeBlock;
continue;
}
}
CaptureMode::OutputCodeBlock => {
if line.starts_with("```") && block_state.is_out() && !old_block_state.is_out() {
capture_mode = CaptureMode::None;
continue;
} else {
push_line(&mut output_script, line);
}
}
CaptureMode::AfterAllSection => {
if (line.starts_with("```lua") || line.starts_with("````lua")) && old_block_state.is_out() {
capture_mode = CaptureMode::AfterAllCodeBlock;
continue;
}
}
CaptureMode::AfterAllCodeBlock => {
if line.starts_with("```") && block_state.is_out() && !old_block_state.is_out() {
capture_mode = CaptureMode::None;
continue;
} else {
push_line(&mut after_all_script, line);
}
}
}
}
finalize_current_prompt_part(&mut current_part, &mut prompt_parts);
let options_toml = buffer_to_string(options_toml);
let agent_options_ov: Option<AgentOptions> = if let Some(options_toml) = options_toml {
Some(AgentOptions::from_options_value(parse_toml(&options_toml)?)?)
} else {
None
};
let agent_options = match agent_options_ov {
Some(agent_options_ov) => agent_options.merge(agent_options_ov)?,
None => agent_options,
};
let model_name = agent_options.model().map(ModelName::from);
let agent_inner = AgentInner {
agent_options: Arc::new(agent_options),
name: name.to_string(),
agent_ref,
file_name: self.spath.name().to_string(),
file_path: self.spath.as_str().to_string(),
model_name,
before_all_script: buffer_to_string(before_all_script),
data_script: buffer_to_string(data_script),
prompt_parts,
output_script: buffer_to_string(output_script),
after_all_script: buffer_to_string(after_all_script),
};
Ok(agent_inner)
}
}
#[cfg(test)]
impl AgentDoc {
pub fn from_content(spath: impl AsRef<Path>, content: impl Into<String>) -> Result<Self> {
let spath = SPath::from_std_path(spath.as_ref())?;
let raw_content = content.into();
Ok(Self { spath, raw_content })
}
}
struct CurrentPromptPart<'a>(PartKind, Option<String>, Vec<&'a str>);
fn finalize_current_prompt_part(current_part: &mut Option<CurrentPromptPart<'_>>, prompt_parts: &mut Vec<PromptPart>) {
if let Some(current_part) = current_part.take() {
let kind = current_part.0;
let options_str = current_part.1;
let mut content = current_part.2;
content.push("");
let content = content.join("\n");
let part = PromptPart {
kind,
options_str,
content,
};
prompt_parts.push(part);
}
}
fn push_line<'a, 'b, 'c: 'b>(content: &'a mut Vec<&'b str>, line: &'c str) {
content.push(line);
content.push("\n");
}
fn buffer_to_string(content: Vec<&str>) -> Option<String> {
if content.is_empty() {
None
} else {
Some(content.join(""))
}
}