use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use serde::{Deserialize, Serialize};
use terraphim_rolegraph::RoleGraph;
use crate::{Goal, GoalAlignmentResult, GoalId};
pub struct KnowledgeGraphGoalAnalyzer {
role_graph: Arc<RoleGraph>,
automata_config: AutomataConfig,
analysis_cache: Arc<tokio::sync::RwLock<HashMap<String, AnalysisResult>>>,
similarity_thresholds: SimilarityThresholds,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AutomataConfig {
pub min_confidence: f64,
pub max_paragraphs: usize,
pub context_window: usize,
pub language_models: Vec<String>,
}
impl Default for AutomataConfig {
fn default() -> Self {
Self {
min_confidence: 0.75,
max_paragraphs: 15,
context_window: 1024,
language_models: vec!["default".to_string()],
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SimilarityThresholds {
pub concept_similarity: f64,
pub domain_similarity: f64,
pub relationship_similarity: f64,
pub conflict_threshold: f64,
}
impl Default for SimilarityThresholds {
fn default() -> Self {
Self {
concept_similarity: 0.8,
domain_similarity: 0.75,
relationship_similarity: 0.7,
conflict_threshold: 0.6,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AnalysisResult {
pub analysis_hash: String,
pub concepts: Vec<String>,
pub connectivity: ConnectivityResult,
pub semantic_analysis: SemanticAnalysis,
pub cached_at: chrono::DateTime<chrono::Utc>,
pub expires_at: chrono::DateTime<chrono::Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConnectivityResult {
pub all_connected: bool,
pub paths: Vec<Vec<String>>,
pub disconnected: Vec<String>,
pub strength_score: f64,
pub suggested_connections: Vec<(String, String, f64)>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SemanticAnalysis {
pub primary_domains: Vec<String>,
pub secondary_domains: Vec<String>,
pub key_concepts: Vec<String>,
pub relationships: Vec<SemanticRelationship>,
pub complexity_score: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SemanticRelationship {
pub source: String,
pub target: String,
pub relationship_type: String,
pub strength: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GoalAlignmentAnalysis {
pub goals: Vec<Goal>,
pub analysis_type: AnalysisType,
pub context: HashMap<String, serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum AnalysisType {
HierarchyConsistency,
ConflictDetection,
ConnectivityValidation,
PropagationAnalysis,
Comprehensive,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GoalAlignmentAnalysisResult {
pub goal_analyses: HashMap<GoalId, GoalAnalysisResult>,
pub overall_alignment_score: f64,
pub conflicts: Vec<GoalConflict>,
pub connectivity_issues: Vec<ConnectivityIssue>,
pub recommendations: Vec<AlignmentRecommendation>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GoalAnalysisResult {
pub goal_id: GoalId,
pub semantic_analysis: SemanticAnalysis,
pub connectivity: ConnectivityResult,
pub alignment_scores: HashMap<GoalId, f64>,
pub potential_conflicts: Vec<GoalId>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GoalConflict {
pub goal1: GoalId,
pub goal2: GoalId,
pub conflict_type: ConflictType,
pub severity: f64,
pub description: String,
pub suggested_resolutions: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub enum ConflictType {
Resource,
Temporal,
Semantic,
Priority,
Dependency,
Constraint,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConnectivityIssue {
pub goal_id: GoalId,
pub issue_type: ConnectivityIssueType,
pub description: String,
pub suggested_fixes: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ConnectivityIssueType {
DisconnectedConcepts,
WeakConnections,
MissingRelationships,
CircularDependencies,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AlignmentRecommendation {
pub recommendation_type: RecommendationType,
pub target_goals: Vec<GoalId>,
pub description: String,
pub expected_impact: f64,
pub priority: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum RecommendationType {
ModifyDescription,
AddConstraints,
AdjustPriorities,
RestructureHierarchy,
AddDependencies,
MergeGoals,
SplitGoals,
}
impl KnowledgeGraphGoalAnalyzer {
pub fn new(
role_graph: Arc<RoleGraph>,
automata_config: AutomataConfig,
similarity_thresholds: SimilarityThresholds,
) -> Self {
Self {
role_graph,
automata_config,
analysis_cache: Arc::new(tokio::sync::RwLock::new(HashMap::new())),
similarity_thresholds,
}
}
pub async fn analyze_goal_alignment(
&self,
analysis: GoalAlignmentAnalysis,
) -> GoalAlignmentResult<GoalAlignmentAnalysisResult> {
let mut goal_analyses = HashMap::new();
let mut conflicts = Vec::new();
let mut connectivity_issues = Vec::new();
for goal in &analysis.goals {
let goal_analysis = self.analyze_individual_goal(goal, &analysis.goals).await?;
for other_goal in &analysis.goals {
if goal.goal_id != other_goal.goal_id
&& let Some(conflict) = self.detect_goal_conflict(goal, other_goal).await?
{
conflicts.push(conflict);
}
}
if let Some(issue) = self.check_goal_connectivity(goal).await? {
connectivity_issues.push(issue);
}
goal_analyses.insert(goal.goal_id.clone(), goal_analysis);
}
let overall_alignment_score = self.calculate_overall_alignment_score(&goal_analyses);
let recommendations = self
.generate_alignment_recommendations(&analysis.goals, &conflicts, &connectivity_issues)
.await?;
Ok(GoalAlignmentAnalysisResult {
goal_analyses,
overall_alignment_score,
conflicts,
connectivity_issues,
recommendations,
})
}
async fn analyze_individual_goal(
&self,
goal: &Goal,
all_goals: &[Goal],
) -> GoalAlignmentResult<GoalAnalysisResult> {
let concepts = self.extract_goal_concepts(goal).await?;
let semantic_analysis = self.perform_semantic_analysis(goal, &concepts).await?;
let connectivity = self.analyze_goal_connectivity(goal, &concepts).await?;
let mut alignment_scores = HashMap::new();
let mut potential_conflicts = Vec::new();
for other_goal in all_goals {
if goal.goal_id != other_goal.goal_id {
let alignment_score = self
.calculate_goal_alignment_score(goal, other_goal)
.await?;
alignment_scores.insert(other_goal.goal_id.clone(), alignment_score);
if alignment_score < self.similarity_thresholds.conflict_threshold {
potential_conflicts.push(other_goal.goal_id.clone());
}
}
}
Ok(GoalAnalysisResult {
goal_id: goal.goal_id.clone(),
semantic_analysis,
connectivity,
alignment_scores,
potential_conflicts,
})
}
async fn extract_goal_concepts(&self, goal: &Goal) -> GoalAlignmentResult<Vec<String>> {
let cache_key = format!("concepts_{}", goal.goal_id);
{
let cache = self.analysis_cache.read().await;
if let Some(cached_result) = cache.get(&cache_key)
&& cached_result.expires_at > chrono::Utc::now()
{
return Ok(cached_result.concepts.clone());
}
}
let text = format!(
"{} {}",
goal.description,
goal.knowledge_context.keywords.join(" ")
);
let scan_end = text
.char_indices()
.nth(self.automata_config.context_window)
.map(|(idx, _)| idx)
.unwrap_or(text.len());
let text = &text[..scan_end];
let mut concepts = HashSet::new();
let words: Vec<&str> = text.split_whitespace().collect();
for word in words {
if word.len() > 3 && !word.chars().all(|c| c.is_ascii_punctuation()) {
concepts.insert(word.to_lowercase());
}
}
concepts.extend(
goal.knowledge_context
.concepts
.iter()
.map(|c| c.to_lowercase()),
);
concepts.extend(
goal.knowledge_context
.domains
.iter()
.map(|d| d.to_lowercase()),
);
let concept_list: Vec<String> = concepts.into_iter().collect();
{
let mut cache = self.analysis_cache.write().await;
cache.insert(
cache_key,
AnalysisResult {
analysis_hash: format!("concepts_{}", goal.goal_id),
concepts: concept_list.clone(),
connectivity: ConnectivityResult {
all_connected: true,
paths: Vec::new(),
disconnected: Vec::new(),
strength_score: 1.0,
suggested_connections: Vec::new(),
},
semantic_analysis: SemanticAnalysis {
primary_domains: Vec::new(),
secondary_domains: Vec::new(),
key_concepts: concept_list.clone(),
relationships: Vec::new(),
complexity_score: 0.5,
},
cached_at: chrono::Utc::now(),
expires_at: chrono::Utc::now() + chrono::Duration::hours(2),
},
);
}
Ok(concept_list)
}
async fn perform_semantic_analysis(
&self,
goal: &Goal,
concepts: &[String],
) -> GoalAlignmentResult<SemanticAnalysis> {
let primary_domains = goal.knowledge_context.domains.clone();
let secondary_domains = Vec::new();
let key_concepts = concepts
.iter()
.take(10) .cloned()
.collect();
let relationships = self.identify_semantic_relationships(concepts).await?;
let complexity_score =
(concepts.len() as f64 * 0.1 + relationships.len() as f64 * 0.2).clamp(0.0, 1.0);
Ok(SemanticAnalysis {
primary_domains,
secondary_domains,
key_concepts,
relationships,
complexity_score,
})
}
async fn identify_semantic_relationships(
&self,
concepts: &[String],
) -> GoalAlignmentResult<Vec<SemanticRelationship>> {
let mut relationships = Vec::new();
for (i, concept1) in concepts.iter().enumerate() {
for concept2 in concepts.iter().skip(i + 1) {
let strength = self.calculate_concept_similarity(concept1, concept2);
if strength > self.similarity_thresholds.relationship_similarity {
relationships.push(SemanticRelationship {
source: concept1.clone(),
target: concept2.clone(),
relationship_type: "related_to".to_string(),
strength,
});
}
}
}
Ok(relationships)
}
fn calculate_concept_similarity(&self, concept1: &str, concept2: &str) -> f64 {
let c1_lower = concept1.to_lowercase();
let c2_lower = concept2.to_lowercase();
if c1_lower == c2_lower {
return 1.0;
}
if c1_lower.contains(&c2_lower) || c2_lower.contains(&c1_lower) {
return 0.8;
}
let c1_words: HashSet<&str> = c1_lower.split_whitespace().collect();
let c2_words: HashSet<&str> = c2_lower.split_whitespace().collect();
let intersection = c1_words.intersection(&c2_words).count();
let union = c1_words.union(&c2_words).count();
if union > 0 {
intersection as f64 / union as f64
} else {
0.0
}
}
async fn analyze_goal_connectivity(
&self,
_goal: &Goal,
concepts: &[String],
) -> GoalAlignmentResult<ConnectivityResult> {
if concepts.is_empty() {
return Ok(ConnectivityResult {
all_connected: true,
paths: Vec::new(),
disconnected: Vec::new(),
strength_score: 1.0,
suggested_connections: Vec::new(),
});
}
let all_connected = self
.role_graph
.is_all_terms_connected_by_path(&concepts.join(" "));
let connectivity_result = ConnectivityResult {
all_connected,
paths: if all_connected {
vec![concepts.to_vec()]
} else {
Vec::new()
},
disconnected: if all_connected {
Vec::new()
} else {
concepts.to_vec()
},
strength_score: if all_connected { 1.0 } else { 0.5 },
suggested_connections: Vec::new(),
};
Ok(connectivity_result)
}
async fn calculate_goal_alignment_score(
&self,
goal1: &Goal,
goal2: &Goal,
) -> GoalAlignmentResult<f64> {
let concepts1: HashSet<String> = goal1.knowledge_context.concepts.iter().cloned().collect();
let concepts2: HashSet<String> = goal2.knowledge_context.concepts.iter().cloned().collect();
let intersection = concepts1.intersection(&concepts2).count();
let union = concepts1.union(&concepts2).count();
let concept_similarity = if union > 0 {
intersection as f64 / union as f64
} else {
0.0
};
let domains1: HashSet<String> = goal1.knowledge_context.domains.iter().cloned().collect();
let domains2: HashSet<String> = goal2.knowledge_context.domains.iter().cloned().collect();
let domain_intersection = domains1.intersection(&domains2).count();
let domain_union = domains1.union(&domains2).count();
let domain_similarity = if domain_union > 0 {
domain_intersection as f64 / domain_union as f64
} else {
0.0
};
let roles1: HashSet<String> = goal1.assigned_roles.iter().cloned().collect();
let roles2: HashSet<String> = goal2.assigned_roles.iter().cloned().collect();
let role_intersection = roles1.intersection(&roles2).count();
let role_union = roles1.union(&roles2).count();
let role_similarity = if role_union > 0 {
role_intersection as f64 / role_union as f64
} else {
0.0
};
let alignment_score =
concept_similarity * 0.4 + domain_similarity * 0.4 + role_similarity * 0.2;
Ok(alignment_score)
}
async fn detect_goal_conflict(
&self,
goal1: &Goal,
goal2: &Goal,
) -> GoalAlignmentResult<Option<GoalConflict>> {
if let Some(conflict) = self.check_resource_conflict(goal1, goal2).await? {
return Ok(Some(conflict));
}
if let Some(conflict) = self.check_temporal_conflict(goal1, goal2).await? {
return Ok(Some(conflict));
}
if let Some(conflict) = self.check_semantic_conflict(goal1, goal2).await? {
return Ok(Some(conflict));
}
Ok(None)
}
async fn check_resource_conflict(
&self,
goal1: &Goal,
goal2: &Goal,
) -> GoalAlignmentResult<Option<GoalConflict>> {
let agents1: HashSet<_> = goal1.assigned_agents.iter().collect();
let agents2: HashSet<_> = goal2.assigned_agents.iter().collect();
let overlapping_agents = agents1.intersection(&agents2).count();
if overlapping_agents > 0 {
let severity = overlapping_agents as f64 / agents1.len().max(agents2.len()) as f64;
return Ok(Some(GoalConflict {
goal1: goal1.goal_id.clone(),
goal2: goal2.goal_id.clone(),
conflict_type: ConflictType::Resource,
severity,
description: format!(
"Goals share {} agents, which may cause resource contention",
overlapping_agents
),
suggested_resolutions: vec![
"Prioritize one goal over the other".to_string(),
"Assign different agents to each goal".to_string(),
"Schedule goals sequentially".to_string(),
],
}));
}
Ok(None)
}
async fn check_temporal_conflict(
&self,
goal1: &Goal,
goal2: &Goal,
) -> GoalAlignmentResult<Option<GoalConflict>> {
if goal1.priority == goal2.priority
&& goal1.status == crate::GoalStatus::Active
&& goal2.status == crate::GoalStatus::Active
{
return Ok(Some(GoalConflict {
goal1: goal1.goal_id.clone(),
goal2: goal2.goal_id.clone(),
conflict_type: ConflictType::Priority,
severity: 0.5,
description: "Goals have same priority and are both active".to_string(),
suggested_resolutions: vec![
"Adjust goal priorities".to_string(),
"Sequence goal execution".to_string(),
],
}));
}
Ok(None)
}
async fn check_semantic_conflict(
&self,
goal1: &Goal,
goal2: &Goal,
) -> GoalAlignmentResult<Option<GoalConflict>> {
let alignment_score = self.calculate_goal_alignment_score(goal1, goal2).await?;
if alignment_score < self.similarity_thresholds.conflict_threshold {
return Ok(Some(GoalConflict {
goal1: goal1.goal_id.clone(),
goal2: goal2.goal_id.clone(),
conflict_type: ConflictType::Semantic,
severity: 1.0 - alignment_score,
description: "Goals have low semantic alignment, indicating potential conflict"
.to_string(),
suggested_resolutions: vec![
"Review goal descriptions for contradictions".to_string(),
"Clarify goal scope and boundaries".to_string(),
"Consider merging or restructuring goals".to_string(),
],
}));
}
Ok(None)
}
async fn check_goal_connectivity(
&self,
goal: &Goal,
) -> GoalAlignmentResult<Option<ConnectivityIssue>> {
let concepts = self.extract_goal_concepts(goal).await?;
let connectivity = self.analyze_goal_connectivity(goal, &concepts).await?;
if !connectivity.all_connected {
return Ok(Some(ConnectivityIssue {
goal_id: goal.goal_id.clone(),
issue_type: ConnectivityIssueType::DisconnectedConcepts,
description: format!(
"Goal has {} disconnected concepts: {}",
connectivity.disconnected.len(),
connectivity.disconnected.join(", ")
),
suggested_fixes: vec![
"Add bridging concepts to connect disconnected elements".to_string(),
"Refine goal description to improve concept connectivity".to_string(),
"Split goal into smaller, more focused sub-goals".to_string(),
],
}));
}
Ok(None)
}
fn calculate_overall_alignment_score(
&self,
goal_analyses: &HashMap<GoalId, GoalAnalysisResult>,
) -> f64 {
if goal_analyses.is_empty() {
return 1.0;
}
let total_score: f64 = goal_analyses
.values()
.map(|analysis| {
let avg_alignment: f64 = if analysis.alignment_scores.is_empty() {
1.0
} else {
analysis.alignment_scores.values().sum::<f64>()
/ analysis.alignment_scores.len() as f64
};
let connectivity_score = analysis.connectivity.strength_score;
(avg_alignment + connectivity_score) / 2.0
})
.sum();
total_score / goal_analyses.len() as f64
}
async fn generate_alignment_recommendations(
&self,
_goals: &[Goal],
conflicts: &[GoalConflict],
connectivity_issues: &[ConnectivityIssue],
) -> GoalAlignmentResult<Vec<AlignmentRecommendation>> {
let mut recommendations = Vec::new();
for conflict in conflicts {
match conflict.conflict_type {
ConflictType::Resource => {
recommendations.push(AlignmentRecommendation {
recommendation_type: RecommendationType::AdjustPriorities,
target_goals: vec![conflict.goal1.clone(), conflict.goal2.clone()],
description: "Adjust goal priorities to resolve resource conflicts"
.to_string(),
expected_impact: conflict.severity,
priority: (conflict.severity * 10.0) as u32,
});
}
ConflictType::Semantic => {
recommendations.push(AlignmentRecommendation {
recommendation_type: RecommendationType::ModifyDescription,
target_goals: vec![conflict.goal1.clone(), conflict.goal2.clone()],
description: "Clarify goal descriptions to resolve semantic conflicts"
.to_string(),
expected_impact: conflict.severity,
priority: (conflict.severity * 8.0) as u32,
});
}
_ => {}
}
}
for issue in connectivity_issues {
if let ConnectivityIssueType::DisconnectedConcepts = issue.issue_type {
recommendations.push(AlignmentRecommendation {
recommendation_type: RecommendationType::ModifyDescription,
target_goals: vec![issue.goal_id.clone()],
description: "Improve concept connectivity in goal description".to_string(),
expected_impact: 0.7,
priority: 5,
});
}
}
recommendations.sort_by_key(|r| std::cmp::Reverse(r.priority));
Ok(recommendations)
}
pub async fn cleanup_cache(&self) {
let mut cache = self.analysis_cache.write().await;
let now = chrono::Utc::now();
cache.retain(|_, result| result.expires_at > now);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{Goal, GoalLevel};
use terraphim_types::{RoleName, Thesaurus};
#[tokio::test]
#[ignore]
async fn test_knowledge_graph_analyzer_creation() {
let role_name = RoleName::new("test_role");
let thesaurus = Thesaurus::new("test_thesaurus".to_string());
let role_graph = Arc::new(RoleGraph::new(role_name, thesaurus).await.unwrap());
let automata_config = AutomataConfig::default();
let similarity_thresholds = SimilarityThresholds::default();
let analyzer =
KnowledgeGraphGoalAnalyzer::new(role_graph, automata_config, similarity_thresholds);
assert_eq!(analyzer.similarity_thresholds.concept_similarity, 0.8);
}
#[tokio::test]
#[ignore]
async fn test_concept_similarity() {
let role_name = RoleName::new("test_role");
let thesaurus = Thesaurus::new("test_thesaurus".to_string());
let role_graph = Arc::new(RoleGraph::new(role_name, thesaurus).await.unwrap());
let analyzer = KnowledgeGraphGoalAnalyzer::new(
role_graph,
AutomataConfig::default(),
SimilarityThresholds::default(),
);
assert_eq!(
analyzer.calculate_concept_similarity("planning", "planning"),
1.0
);
assert!(analyzer.calculate_concept_similarity("planning", "plan") > 0.0);
assert_eq!(
analyzer.calculate_concept_similarity("planning", "execution"),
0.0
);
}
#[tokio::test]
#[ignore]
async fn test_goal_alignment_score() {
let role_name = RoleName::new("test_role");
let thesaurus = Thesaurus::new("test_thesaurus".to_string());
let role_graph = Arc::new(RoleGraph::new(role_name, thesaurus).await.unwrap());
let analyzer = KnowledgeGraphGoalAnalyzer::new(
role_graph,
AutomataConfig::default(),
SimilarityThresholds::default(),
);
let mut goal1 = Goal::new(
"goal1".to_string(),
GoalLevel::Local,
"Planning task".to_string(),
1,
);
goal1.knowledge_context.concepts = vec!["planning".to_string(), "task".to_string()];
goal1.knowledge_context.domains = vec!["project_management".to_string()];
let mut goal2 = Goal::new(
"goal2".to_string(),
GoalLevel::Local,
"Execution task".to_string(),
1,
);
goal2.knowledge_context.concepts = vec!["execution".to_string(), "task".to_string()];
goal2.knowledge_context.domains = vec!["project_management".to_string()];
let alignment_score = analyzer
.calculate_goal_alignment_score(&goal1, &goal2)
.await
.unwrap();
assert!(alignment_score > 0.0);
assert!(alignment_score < 1.0);
}
}