mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-10 11:19:50 +08:00
160 lines
4.9 KiB
Rust
160 lines
4.9 KiB
Rust
use async_trait::async_trait;
|
|||
|
|
|
||
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||
|
|
pub struct AiCandidateMaterializationOutcome<Attempt> {
|
||
|
|
pub attempts: Vec<Attempt>,
|
||
|
|
pub candidate_count: usize,
|
||
|
|
}
|
||
|
|
|
||
|
|
#[async_trait]
|
||
|
|
pub trait AiCandidateMaterializationPort: Send + Sync {
|
||
|
|
type Candidate: Send;
|
||
|
|
type Eligible: Send + Sync;
|
||
|
|
type Skipped: Send;
|
||
|
|
type Attempt: Send;
|
||
|
|
type Error: Send;
|
||
|
|
|
||
|
|
async fn resolve_and_rank_candidates(
|
||
|
|
&self,
|
||
|
|
candidates: Vec<Self::Candidate>,
|
||
|
|
) -> Result<(Vec<Self::Eligible>, Vec<Self::Skipped>), Self::Error>;
|
||
|
|
|
||
|
|
fn decorate_skipped_candidate(&self, skipped: Self::Skipped) -> Self::Skipped {
|
||
|
|
skipped
|
||
|
|
}
|
||
|
|
|
||
|
|
fn remember_first_candidate_affinity(&self, candidates: &[Self::Eligible]);
|
||
|
|
|
||
|
|
async fn persist_available_candidates(
|
||
|
|
&self,
|
||
|
|
candidates: Vec<Self::Eligible>,
|
||
|
|
) -> Result<Vec<Self::Attempt>, Self::Error>;
|
||
|
|
|
||
|
|
async fn persist_skipped_candidates(
|
||
|
|
&self,
|
||
|
|
starting_candidate_index: u32,
|
||
|
|
skipped_candidates: Vec<Self::Skipped>,
|
||
|
|
) -> Result<(), Self::Error>;
|
||
|
|
}
|
||
|
|
|
||
|
|
pub async fn run_ai_candidate_materialization<Port>(
|
||
|
|
port: &Port,
|
||
|
|
candidates: Vec<Port::Candidate>,
|
||
|
|
preselection_skipped_candidates: Vec<Port::Skipped>,
|
||
|
|
) -> Result<AiCandidateMaterializationOutcome<Port::Attempt>, Port::Error>
|
||
|
|
where
|
||
|
|
Port: AiCandidateMaterializationPort,
|
||
|
|
{
|
||
|
|
let (candidates, skipped_candidates) = port.resolve_and_rank_candidates(candidates).await?;
|
||
|
|
let skipped_candidates = preselection_skipped_candidates
|
||
|
|
.into_iter()
|
||
|
|
.chain(skipped_candidates)
|
||
|
|
.map(|candidate| port.decorate_skipped_candidate(candidate))
|
||
|
|
.collect::<Vec<_>>();
|
||
|
|
let candidate_count = candidates.len() + skipped_candidates.len();
|
||
|
|
|
||
|
|
port.remember_first_candidate_affinity(&candidates);
|
||
|
|
let available_candidate_count = candidates.len() as u32;
|
||
|
|
let attempts = port.persist_available_candidates(candidates).await?;
|
||
|
|
port.persist_skipped_candidates(available_candidate_count, skipped_candidates)
|
||
|
|
.await?;
|
||
|
|
|
||
|
|
Ok(AiCandidateMaterializationOutcome {
|
||
|
|
attempts,
|
||
|
|
candidate_count,
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
#[cfg(test)]
|
||
|
|
mod tests {
|
||
|
|
use super::*;
|
||
|
|
use std::sync::Mutex;
|
||
|
|
|
||
|
|
#[derive(Default)]
|
||
|
|
struct TestPort {
|
||
|
|
calls: Mutex<Vec<String>>,
|
||
|
|
}
|
||
|
|
|
||
|
|
#[async_trait]
|
||
|
|
impl AiCandidateMaterializationPort for TestPort {
|
||
|
|
type Candidate = &'static str;
|
||
|
|
type Eligible = &'static str;
|
||
|
|
type Skipped = &'static str;
|
||
|
|
type Attempt = &'static str;
|
||
|
|
type Error = std::convert::Infallible;
|
||
|
|
|
||
|
|
async fn resolve_and_rank_candidates(
|
||
|
|
&self,
|
||
|
|
candidates: Vec<Self::Candidate>,
|
||
|
|
) -> Result<(Vec<Self::Eligible>, Vec<Self::Skipped>), Self::Error> {
|
||
|
|
self.calls
|
||
|
|
.lock()
|
||
|
|
.unwrap()
|
||
|
|
.push(format!("resolve:{}", candidates.join(",")));
|
||
|
|
Ok((vec!["eligible-a", "eligible-b"], vec!["resolved-skip"]))
|
||
|
|
}
|
||
|
|
|
||
|
|
fn decorate_skipped_candidate(&self, skipped: Self::Skipped) -> Self::Skipped {
|
||
|
|
self.calls
|
||
|
|
.lock()
|
||
|
|
.unwrap()
|
||
|
|
.push(format!("decorate:{skipped}"));
|
||
|
|
skipped
|
||
|
|
}
|
||
|
|
|
||
|
|
fn remember_first_candidate_affinity(&self, candidates: &[Self::Eligible]) {
|
||
|
|
self.calls
|
||
|
|
.lock()
|
||
|
|
.unwrap()
|
||
|
|
.push(format!("affinity:{}", candidates.join(",")));
|
||
|
|
}
|
||
|
|
|
||
|
|
async fn persist_available_candidates(
|
||
|
|
&self,
|
||
|
|
candidates: Vec<Self::Eligible>,
|
||
|
|
) -> Result<Vec<Self::Attempt>, Self::Error> {
|
||
|
|
self.calls
|
||
|
|
.lock()
|
||
|
|
.unwrap()
|
||
|
|
.push(format!("available:{}", candidates.join(",")));
|
||
|
|
Ok(vec!["attempt-a", "attempt-b"])
|
||
|
|
}
|
||
|
|
|
||
|
|
async fn persist_skipped_candidates(
|
||
|
|
&self,
|
||
|
|
starting_candidate_index: u32,
|
||
|
|
skipped_candidates: Vec<Self::Skipped>,
|
||
|
|
) -> Result<(), Self::Error> {
|
||
|
|
self.calls.lock().unwrap().push(format!(
|
||
|
|
"skipped:{starting_candidate_index}:{}",
|
||
|
|
skipped_candidates.join(",")
|
||
|
|
));
|
||
|
|
Ok(())
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
#[tokio::test]
|
||
|
|
async fn materialization_runs_in_serving_order_and_counts_all_candidates() {
|
||
|
|
let port = TestPort::default();
|
||
|
|
|
||
|
|
let outcome =
|
||
|
|
run_ai_candidate_materialization(&port, vec!["candidate-a"], vec!["pre-skip"])
|
||
|
|
.await
|
||
|
|
.unwrap();
|
||
|
|
|
||
|
|
assert_eq!(outcome.attempts, ["attempt-a", "attempt-b"]);
|
||
|
|
assert_eq!(outcome.candidate_count, 4);
|
||
|
|
assert_eq!(
|
||
|
|
port.calls.lock().unwrap().as_slice(),
|
||
|
|
[
|
||
|
|
"resolve:candidate-a",
|
||
|
|
"decorate:pre-skip",
|
||
|
|
"decorate:resolved-skip",
|
||
|
|
"affinity:eligible-a,eligible-b",
|
||
|
|
"available:eligible-a,eligible-b",
|
||
|
|
"skipped:2:pre-skip,resolved-skip",
|
||
|
|
]
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|