use argonauts_core::names::ModelRef;
use argonauts_core::settings::Settings;
use argonauts_core::settings::crew::CrewRole;
use argonauts_core::steps::intake::InstructionKind;
use argonauts_crew::roles::{candidates, display_name};
use crate::controller::instructions::Applied;
const SHOWN: [CrewRole; 4] = [
CrewRole::Orchestrator,
CrewRole::Worker,
CrewRole::Reviewer,
CrewRole::Judge,
];
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct CrewLine {
pub role: CrewRole,
pub who: String,
pub from: String,
pub asked: Option<Vec<ModelRef>>,
}
pub(crate) fn understood(
settings: &Settings,
applied: &Applied,
run: Option<&[String]>,
) -> Option<Vec<CrewLine>> {
let followed = |kind: InstructionKind| {
applied
.instructions
.iter()
.any(|instruction| instruction.kind == kind && instruction.followed())
};
if !CREW_KINDS.into_iter().any(followed) {
return None;
}
let words = with_request(settings, &request_settings(applied))?;
let asked = match run {
Some(pairs) => with_request(settings, pairs)?,
None => words.clone(),
};
let models = |settings: &Settings, role: CrewRole| candidates(settings, role.fine_role());
let by_words = |role: CrewRole| {
models(settings, role) != models(&asked, role)
&& models(&words, role) == models(&asked, role)
};
if !SHOWN.into_iter().any(by_words) {
return None;
}
let main = [settings.crew.main.clone()];
let given_to_main = |role: CrewRole| {
models(&asked, role) == main
&& (role == CrewRole::Orchestrator || models(settings, role) != main)
};
let others = applied
.instructions
.iter()
.find(|instruction| instruction.kind == InstructionKind::Others && instruction.followed());
let lines = SHOWN
.into_iter()
.map(|role| {
let follows = follows(&asked, role);
let set_by_words = by_words(role) && follows.is_none();
let from = if set_by_words {
match others.filter(|_| reached_by_others(role, &followed)) {
Some(others) => format!("as you asked: \"{}\"", phrase(&others.says)),
None => "as you asked".to_owned(),
}
} else if let Some(followed) = follows {
format!("follows the {}", followed.plural())
} else if given_to_main(role) {
"your main session".to_owned()
} else {
"your settings".to_owned()
};
CrewLine {
role,
who: names(&models(&asked, role)),
from,
asked: set_by_words.then(|| models(&asked, role)),
}
})
.collect();
Some(lines)
}
pub(crate) fn text(lines: &[CrewLine]) -> String {
let role_width = lines
.iter()
.map(|line| line.role.plural().len())
.max()
.unwrap_or(0);
let who_width = lines
.iter()
.map(|line| line.who.chars().count())
.max()
.unwrap_or(0);
let mut text = "Crew for this request".to_owned();
for line in lines {
text.push_str(&format!(
"\n{:role_width$} {:who_width$} {}",
line.role.plural(),
line.who,
line.from
));
}
text
}
pub(crate) fn to_keep(settings: &Settings, lines: &[CrewLine]) -> Vec<(String, String)> {
lines
.iter()
.filter_map(|line| {
let asked = line.asked.as_ref()?;
let fine = line.role.fine_role();
let own = settings
.roles
.get(&fine)
.is_some_and(|own| !own.models.is_empty());
let key = if own {
format!("roles.{}.models", fine.as_str())
} else {
format!("crew.{}", line.role.as_str())
};
Some((key, toml_list(asked)))
})
.collect()
}
const CREW_KINDS: [InstructionKind; 8] = [
InstructionKind::MainAgent,
InstructionKind::Orchestrate,
InstructionKind::Build,
InstructionKind::Review,
InstructionKind::Judge,
InstructionKind::Crew,
InstructionKind::Others,
InstructionKind::Exclude,
];
fn reached_by_others(role: CrewRole, followed: &dyn Fn(InstructionKind) -> bool) -> bool {
let own = match role {
CrewRole::Orchestrator => InstructionKind::Orchestrate,
CrewRole::Reviewer => InstructionKind::Review,
CrewRole::Judge => InstructionKind::Judge,
CrewRole::Main | CrewRole::Worker => return false,
};
!followed(own)
}
fn phrase(says: &str) -> String {
let words: Vec<&str> = says.split_whitespace().collect();
let other = words
.iter()
.position(|word| word.to_lowercase().starts_with("other"));
let end = match other {
Some(at) if words[at].to_lowercase().starts_with("others") => at,
Some(at) => (at + 1).min(words.len() - 1),
None => return says.trim().to_owned(),
};
words[..=end]
.join(" ")
.trim_end_matches([',', '.', ';', ':'])
.to_owned()
}
fn follows(settings: &Settings, role: CrewRole) -> Option<CrewRole> {
settings.crew.followed(role).filter(|_| {
settings
.roles
.get(&role.fine_role())
.is_none_or(|own| own.models.is_empty())
})
}
fn names(models: &[ModelRef]) -> String {
match models {
[] => "nobody".to_owned(),
models => models
.iter()
.map(display_name)
.collect::<Vec<_>>()
.join(", "),
}
}
fn toml_list(models: &[ModelRef]) -> String {
let items: Vec<String> = models
.iter()
.map(|model| toml::Value::String(model.to_string()).to_string())
.collect();
format!("[{}]", items.join(", "))
}
fn request_settings(applied: &Applied) -> Vec<String> {
let mut pairs: Vec<String> = applied
.settings
.iter()
.map(|(key, value)| format!("{key}={value}"))
.collect();
if let Some(main_agent) = &applied.main_agent {
let main_agent = toml::Value::String(main_agent.to_string());
pairs.push(format!("crew.main={main_agent}"));
}
pairs
}
pub(super) fn with_request(settings: &Settings, pairs: &[String]) -> Option<Settings> {
if pairs.is_empty() {
return Some(settings.clone());
}
let mut table = toml::Value::try_from(settings).ok()?;
for pair in pairs {
let (key, value) = pair.split_once('=')?;
let value: toml::Value = toml::from_str::<toml::Table>(&format!("v = {}", value.trim()))
.ok()?
.remove("v")?;
let mut at = &mut table;
let keys: Vec<&str> = key.trim().split('.').collect();
let (last, path) = keys.split_last()?;
for key in path {
let toml::Value::Table(inner) = at else {
return None;
};
at = inner
.entry((*key).to_owned())
.or_insert_with(|| toml::Value::Table(toml::Table::new()));
}
let toml::Value::Table(inner) = at else {
return None;
};
inner.insert((*last).to_owned(), value);
}
table.try_into().ok()
}
#[cfg(test)]
mod tests;