use crate::context::{Context, ContextKey, Fact};
use crate::types::IntentId;
use std::time::Duration;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum IntentKind {
GrowthStrategy,
Scheduling,
ResourceOptimization,
RiskAssessment,
ContentGeneration,
Custom,
}
impl IntentKind {
#[must_use]
pub fn name(&self) -> &'static str {
match self {
Self::GrowthStrategy => "growth_strategy",
Self::Scheduling => "scheduling",
Self::ResourceOptimization => "resource_optimization",
Self::RiskAssessment => "risk_assessment",
Self::ContentGeneration => "content_generation",
Self::Custom => "custom",
}
}
#[must_use]
pub fn suggested_context_keys(&self) -> &'static [ContextKey] {
match self {
Self::GrowthStrategy => &[
ContextKey::Seeds,
ContextKey::Signals,
ContextKey::Competitors,
ContextKey::Strategies,
ContextKey::Evaluations,
],
Self::Scheduling | Self::ContentGeneration => &[
ContextKey::Seeds,
ContextKey::Constraints,
ContextKey::Strategies,
],
Self::ResourceOptimization => &[
ContextKey::Seeds,
ContextKey::Constraints,
ContextKey::Strategies,
ContextKey::Evaluations,
],
Self::RiskAssessment => &[
ContextKey::Seeds,
ContextKey::Signals,
ContextKey::Hypotheses,
ContextKey::Evaluations,
],
Self::Custom => &[ContextKey::Seeds],
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Objective {
IncreaseDemand,
MinimizeTime,
MaximizeFeasibility,
MinimizeCost,
MaximizeCoverage,
Balance(Vec<Objective>),
Custom(String),
}
impl Objective {
#[must_use]
pub fn name(&self) -> String {
match self {
Self::IncreaseDemand => "increase_demand".into(),
Self::MinimizeTime => "minimize_time".into(),
Self::MaximizeFeasibility => "maximize_feasibility".into(),
Self::MinimizeCost => "minimize_cost".into(),
Self::MaximizeCoverage => "maximize_coverage".into(),
Self::Balance(_) => "balanced".into(),
Self::Custom(name) => name.clone(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum ScopeConstraint {
Market(String),
Geography(String),
Product(String),
TimeWindow {
start: Option<String>,
end: Option<String>,
description: String,
},
CustomerSegment(String),
Custom { key: String, value: String },
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Scope {
constraints: Vec<ScopeConstraint>,
}
impl Scope {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_constraint(mut self, constraint: ScopeConstraint) -> Self {
self.constraints.push(constraint);
self
}
#[must_use]
pub fn constraints(&self) -> &[ScopeConstraint] {
&self.constraints
}
#[must_use]
pub fn is_defined(&self) -> bool {
!self.constraints.is_empty()
}
#[must_use]
pub fn allows(&self, _fact: &Fact) -> bool {
true
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ConstraintSeverity {
Hard,
Soft,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct IntentConstraint {
pub key: String,
pub value: String,
pub severity: ConstraintSeverity,
}
impl IntentConstraint {
#[must_use]
pub fn hard(key: impl Into<String>, value: impl Into<String>) -> Self {
Self {
key: key.into(),
value: value.into(),
severity: ConstraintSeverity::Hard,
}
}
#[must_use]
pub fn soft(key: impl Into<String>, value: impl Into<String>) -> Self {
Self {
key: key.into(),
value: value.into(),
severity: ConstraintSeverity::Soft,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum SuccessCriterion {
AtLeastOneViableStrategy,
ValidScheduleFound,
AllTasksAllocated,
MinimumStrategies(usize),
AllEvaluationsPositive,
Custom(String),
}
impl SuccessCriterion {
#[must_use]
pub fn is_satisfied(&self, ctx: &Context) -> bool {
match self {
Self::AtLeastOneViableStrategy => {
let strategies = ctx.get(ContextKey::Strategies);
let evaluations = ctx.get(ContextKey::Evaluations);
if strategies.is_empty() {
return false;
}
if evaluations.is_empty() {
return true;
}
evaluations.iter().any(|e| {
e.content.to_lowercase().contains("viable")
|| e.content.to_lowercase().contains("positive")
|| e.content.to_lowercase().contains("recommended")
})
}
Self::ValidScheduleFound => ctx.has(ContextKey::Strategies),
Self::AllTasksAllocated => {
!ctx.get(ContextKey::Constraints)
.iter()
.any(|c| c.content.to_lowercase().contains("unallocated"))
}
Self::MinimumStrategies(n) => ctx.get(ContextKey::Strategies).len() >= *n,
Self::AllEvaluationsPositive => {
let evaluations = ctx.get(ContextKey::Evaluations);
!evaluations.is_empty()
&& evaluations.iter().all(|e| {
!e.content.to_lowercase().contains("negative")
&& !e.content.to_lowercase().contains("rejected")
})
}
Self::Custom(_) => {
true
}
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct SuccessCriteria {
required: Vec<SuccessCriterion>,
optional: Vec<SuccessCriterion>,
}
impl SuccessCriteria {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn require(mut self, criterion: SuccessCriterion) -> Self {
self.required.push(criterion);
self
}
#[must_use]
pub fn prefer(mut self, criterion: SuccessCriterion) -> Self {
self.optional.push(criterion);
self
}
#[must_use]
pub fn is_satisfied(&self, ctx: &Context) -> bool {
self.required.iter().all(|c| c.is_satisfied(ctx))
}
#[must_use]
pub fn unsatisfied(&self, ctx: &Context) -> Vec<&SuccessCriterion> {
self.required
.iter()
.filter(|c| !c.is_satisfied(ctx))
.collect()
}
#[must_use]
pub fn unsatisfied_optional(&self, ctx: &Context) -> Vec<&SuccessCriterion> {
self.optional
.iter()
.filter(|c| !c.is_satisfied(ctx))
.collect()
}
#[must_use]
pub fn is_explicit(&self) -> bool {
!self.required.is_empty()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Budgets {
pub max_cycles: u32,
pub max_agents_per_cycle: Option<u32>,
pub max_facts: u32,
pub time_limit: Option<Duration>,
pub max_tokens: Option<u64>,
}
impl Default for Budgets {
fn default() -> Self {
Self {
max_cycles: 100,
max_agents_per_cycle: None,
max_facts: 10_000,
time_limit: None,
max_tokens: None,
}
}
}
impl Budgets {
#[must_use]
pub fn with_max_cycles(mut self, max: u32) -> Self {
self.max_cycles = max;
self
}
#[must_use]
pub fn with_max_facts(mut self, max: u32) -> Self {
self.max_facts = max;
self
}
#[must_use]
pub fn with_time_limit(mut self, limit: Duration) -> Self {
self.time_limit = Some(limit);
self
}
#[must_use]
pub fn with_max_tokens(mut self, max: u64) -> Self {
self.max_tokens = Some(max);
self
}
#[must_use]
pub fn to_engine_budget(&self) -> crate::engine::Budget {
crate::engine::Budget {
max_cycles: self.max_cycles,
max_facts: self.max_facts,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct IntentValidationError {
pub field: String,
pub reason: String,
}
impl std::fmt::Display for IntentValidationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "invalid {}: {}", self.field, self.reason)
}
}
impl std::error::Error for IntentValidationError {}
#[derive(Debug, Clone)]
pub struct RootIntent {
pub id: IntentId,
pub kind: IntentKind,
pub objective: Option<Objective>,
pub scope: Scope,
pub constraints: Vec<IntentConstraint>,
pub success_criteria: SuccessCriteria,
pub budgets: Budgets,
}
impl RootIntent {
#[allow(deprecated)]
#[must_use]
pub fn new(kind: IntentKind) -> Self {
Self {
id: IntentId::generate(),
kind,
objective: None,
scope: Scope::new(),
constraints: Vec::new(),
success_criteria: SuccessCriteria::new(),
budgets: Budgets::default(),
}
}
#[must_use]
pub fn with_id(mut self, id: IntentId) -> Self {
self.id = id;
self
}
#[must_use]
pub fn with_objective(mut self, objective: Objective) -> Self {
self.objective = Some(objective);
self
}
#[must_use]
pub fn with_scope(mut self, scope: Scope) -> Self {
self.scope = scope;
self
}
#[must_use]
pub fn with_constraint(mut self, constraint: IntentConstraint) -> Self {
self.constraints.push(constraint);
self
}
#[must_use]
pub fn with_success_criterion(mut self, criterion: SuccessCriterion) -> Self {
self.success_criteria = self.success_criteria.require(criterion);
self
}
#[must_use]
pub fn with_success_criteria(mut self, criteria: SuccessCriteria) -> Self {
self.success_criteria = criteria;
self
}
#[must_use]
pub fn with_budgets(mut self, budgets: Budgets) -> Self {
self.budgets = budgets;
self
}
pub fn validate(&self) -> Result<(), IntentValidationError> {
if !self.scope.is_defined() {
return Err(IntentValidationError {
field: "scope".into(),
reason: "scope must have at least one constraint".into(),
});
}
if !self.success_criteria.is_explicit() {
return Err(IntentValidationError {
field: "success_criteria".into(),
reason: "at least one success criterion must be defined".into(),
});
}
Ok(())
}
#[must_use]
pub fn to_seed_facts(&self) -> Vec<Fact> {
let mut facts = Vec::new();
facts.push(crate::context::new_fact(
ContextKey::Seeds,
format!("intent:{}", self.id),
format!(
"kind={} objective={}",
self.kind.name(),
self.objective
.as_ref()
.map_or("unspecified".to_string(), Objective::name)
),
));
for (i, constraint) in self.scope.constraints().iter().enumerate() {
let content = match constraint {
ScopeConstraint::Market(m) => format!("market={m}"),
ScopeConstraint::Geography(g) => format!("geography={g}"),
ScopeConstraint::Product(p) => format!("product={p}"),
ScopeConstraint::TimeWindow { description, .. } => {
format!("timewindow={description}")
}
ScopeConstraint::CustomerSegment(s) => format!("segment={s}"),
ScopeConstraint::Custom { key, value } => format!("{key}={value}"),
};
facts.push(crate::context::new_fact(
ContextKey::Seeds,
format!("scope:{}:{i}", self.id),
content,
));
}
for constraint in &self.constraints {
if constraint.severity == ConstraintSeverity::Hard {
facts.push(crate::context::new_fact(
ContextKey::Constraints,
format!("constraint:{}:{}", self.id, constraint.key),
format!("{}={}", constraint.key, constraint.value),
));
}
}
facts
}
#[must_use]
pub fn hard_constraints(&self) -> Vec<&IntentConstraint> {
self.constraints
.iter()
.filter(|c| c.severity == ConstraintSeverity::Hard)
.collect()
}
#[must_use]
pub fn soft_constraints(&self) -> Vec<&IntentConstraint> {
self.constraints
.iter()
.filter(|c| c.severity == ConstraintSeverity::Soft)
.collect()
}
#[must_use]
pub fn is_successful(&self, ctx: &Context) -> bool {
self.success_criteria.is_satisfied(ctx)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[allow(deprecated)]
fn intent_id_generates_unique_ids() {
let id1 = IntentId::generate();
let id2 = IntentId::generate();
assert_ne!(id1, id2);
}
#[test]
fn intent_kind_has_names() {
assert_eq!(IntentKind::GrowthStrategy.name(), "growth_strategy");
assert_eq!(IntentKind::Scheduling.name(), "scheduling");
}
#[test]
fn intent_kind_suggests_context_keys() {
let keys = IntentKind::GrowthStrategy.suggested_context_keys();
assert!(keys.contains(&ContextKey::Strategies));
assert!(keys.contains(&ContextKey::Competitors));
}
#[test]
fn scope_tracks_constraints() {
let scope = Scope::new()
.with_constraint(ScopeConstraint::Market("Nordic B2B".into()))
.with_constraint(ScopeConstraint::Geography("EMEA".into()));
assert!(scope.is_defined());
assert_eq!(scope.constraints().len(), 2);
}
#[test]
fn intent_constraint_severities() {
let hard = IntentConstraint::hard("budget", "1M");
let soft = IntentConstraint::soft("brand", "friendly");
assert_eq!(hard.severity, ConstraintSeverity::Hard);
assert_eq!(soft.severity, ConstraintSeverity::Soft);
}
#[test]
fn success_criteria_checks_satisfaction() {
let mut ctx = Context::new();
ctx.add_fact(crate::context::new_fact(
ContextKey::Strategies,
"strat-1",
"growth strategy",
))
.unwrap();
ctx.add_fact(crate::context::new_fact(
ContextKey::Evaluations,
"eval-1",
"viable and recommended",
))
.unwrap();
let criteria = SuccessCriteria::new().require(SuccessCriterion::AtLeastOneViableStrategy);
assert!(criteria.is_satisfied(&ctx));
}
#[test]
fn success_criteria_reports_unsatisfied() {
let ctx = Context::new();
let criteria = SuccessCriteria::new().require(SuccessCriterion::MinimumStrategies(2));
assert!(!criteria.is_satisfied(&ctx));
assert_eq!(criteria.unsatisfied(&ctx).len(), 1);
}
#[test]
fn root_intent_validates_scope() {
let intent = RootIntent::new(IntentKind::GrowthStrategy)
.with_success_criterion(SuccessCriterion::AtLeastOneViableStrategy);
let result = intent.validate();
assert!(result.is_err());
assert_eq!(result.unwrap_err().field, "scope");
}
#[test]
fn root_intent_validates_success_criteria() {
let intent = RootIntent::new(IntentKind::GrowthStrategy)
.with_scope(Scope::new().with_constraint(ScopeConstraint::Market("B2B".into())));
let result = intent.validate();
assert!(result.is_err());
assert_eq!(result.unwrap_err().field, "success_criteria");
}
#[test]
fn root_intent_passes_validation() {
let intent = RootIntent::new(IntentKind::GrowthStrategy)
.with_objective(Objective::IncreaseDemand)
.with_scope(Scope::new().with_constraint(ScopeConstraint::Market("B2B".into())))
.with_success_criterion(SuccessCriterion::AtLeastOneViableStrategy);
assert!(intent.validate().is_ok());
}
#[test]
fn root_intent_generates_seed_facts() {
let intent = RootIntent::new(IntentKind::GrowthStrategy)
.with_id(IntentId::new("test-intent"))
.with_objective(Objective::IncreaseDemand)
.with_scope(
Scope::new()
.with_constraint(ScopeConstraint::Market("Nordic".into()))
.with_constraint(ScopeConstraint::Geography("EMEA".into())),
)
.with_constraint(IntentConstraint::hard("budget", "1M"));
let facts = intent.to_seed_facts();
assert_eq!(facts.len(), 4);
let intent_fact = facts.iter().find(|f| f.id.starts_with("intent:")).unwrap();
assert!(intent_fact.content.contains("growth_strategy"));
assert!(intent_fact.content.contains("increase_demand"));
let constraint_fact = facts
.iter()
.find(|f| f.key() == ContextKey::Constraints)
.unwrap();
assert!(constraint_fact.content.contains("budget=1M"));
}
#[test]
fn budgets_converts_to_engine_budget() {
let budgets = Budgets::default().with_max_cycles(50).with_max_facts(5000);
let engine_budget = budgets.to_engine_budget();
assert_eq!(engine_budget.max_cycles, 50);
assert_eq!(engine_budget.max_facts, 5000);
}
#[test]
fn root_intent_checks_success() {
let intent = RootIntent::new(IntentKind::GrowthStrategy)
.with_success_criterion(SuccessCriterion::MinimumStrategies(1));
let mut ctx = Context::new();
assert!(!intent.is_successful(&ctx));
ctx.add_fact(crate::context::new_fact(
ContextKey::Strategies,
"s1",
"strategy",
))
.unwrap();
assert!(intent.is_successful(&ctx));
}
#[test]
fn hard_and_soft_constraints_filtered() {
let intent = RootIntent::new(IntentKind::GrowthStrategy)
.with_constraint(IntentConstraint::hard("budget", "1M"))
.with_constraint(IntentConstraint::soft("brand", "friendly"))
.with_constraint(IntentConstraint::hard("compliance", "GDPR"));
assert_eq!(intent.hard_constraints().len(), 2);
assert_eq!(intent.soft_constraints().len(), 1);
}
}