1use converge_pack::ContextKey;
7use converge_provider_api::{CostClass, LatencyClass};
8use serde::{Deserialize, Serialize};
9
10pub trait SuggestorProfile {
12 fn role(&self) -> SuggestorRole;
13 fn output_keys(&self) -> &[ContextKey];
14 fn cost_hint(&self) -> CostClass;
15 fn latency_hint(&self) -> LatencyClass;
16 fn capabilities(&self) -> &[SuggestorCapability];
17 fn confidence_range(&self) -> (f32, f32);
18}
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
22#[serde(rename_all = "snake_case")]
23pub enum SuggestorRole {
24 Analysis,
25 Planning,
26 Evaluation,
27 Constraint,
28 Signal,
29 Synthesis,
30 Meta,
31}
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
35#[serde(rename_all = "snake_case")]
36pub enum SuggestorCapability {
37 LlmReasoning,
38 KnowledgeRetrieval,
39 Analytics,
40 Optimization,
41 PolicyEnforcement,
42 HumanInTheLoop,
43 ExperienceLearning,
44}
45
46#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct ProfileSnapshot {
49 pub name: String,
50 pub role: SuggestorRole,
51 pub output_keys: Vec<ContextKey>,
52 pub cost_hint: CostClass,
53 pub latency_hint: LatencyClass,
54 pub capabilities: Vec<SuggestorCapability>,
55 pub confidence_min: f32,
56 pub confidence_max: f32,
57}
58
59impl ProfileSnapshot {
60 #[must_use]
61 pub fn from_profile(name: impl Into<String>, profile: &dyn SuggestorProfile) -> Self {
62 let (min, max) = profile.confidence_range();
63 Self {
64 name: name.into(),
65 role: profile.role(),
66 output_keys: profile.output_keys().to_vec(),
67 cost_hint: profile.cost_hint(),
68 latency_hint: profile.latency_hint(),
69 capabilities: profile.capabilities().to_vec(),
70 confidence_min: min,
71 confidence_max: max,
72 }
73 }
74}
75
76#[derive(Debug, Clone, Serialize, Deserialize)]
78pub struct FormationRequest {
79 pub id: String,
81 pub required_roles: Vec<SuggestorRole>,
83 pub required_capabilities: Vec<SuggestorCapability>,
85}
86
87#[derive(Debug, Clone, Serialize, Deserialize)]
89pub struct FormationPlan {
90 pub request_id: String,
92 pub assignments: Vec<RoleAssignment>,
94 pub unmatched_roles: Vec<SuggestorRole>,
96 pub coverage_ratio: f64,
98}
99
100#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
102pub struct RoleAssignment {
103 pub role: SuggestorRole,
104 pub suggestor: String,
105}
106
107#[cfg(test)]
108mod tests {
109 use super::*;
110
111 struct AnalysisSuggestor;
112
113 impl SuggestorProfile for AnalysisSuggestor {
114 fn role(&self) -> SuggestorRole {
115 SuggestorRole::Analysis
116 }
117
118 fn output_keys(&self) -> &[ContextKey] {
119 &[ContextKey::Hypotheses]
120 }
121
122 fn cost_hint(&self) -> CostClass {
123 CostClass::Medium
124 }
125
126 fn latency_hint(&self) -> LatencyClass {
127 LatencyClass::Interactive
128 }
129
130 fn capabilities(&self) -> &[SuggestorCapability] {
131 &[SuggestorCapability::LlmReasoning]
132 }
133
134 fn confidence_range(&self) -> (f32, f32) {
135 (0.5, 0.95)
136 }
137 }
138
139 #[test]
140 fn profile_snapshot_captures_all_fields() {
141 let suggestor = AnalysisSuggestor;
142 let snap = ProfileSnapshot::from_profile("analysis-1", &suggestor);
143
144 assert_eq!(snap.name, "analysis-1");
145 assert_eq!(snap.role, SuggestorRole::Analysis);
146 assert_eq!(snap.output_keys, vec![ContextKey::Hypotheses]);
147 assert_eq!(snap.confidence_min, 0.5);
148 assert_eq!(snap.confidence_max, 0.95);
149 assert_eq!(snap.capabilities, vec![SuggestorCapability::LlmReasoning]);
150 }
151
152 #[test]
153 fn profile_snapshot_serde_roundtrip() {
154 let suggestor = AnalysisSuggestor;
155 let snap = ProfileSnapshot::from_profile("analysis-1", &suggestor);
156 let json = serde_json::to_string(&snap).unwrap();
157 let back: ProfileSnapshot = serde_json::from_str(&json).unwrap();
158
159 assert_eq!(back.name, snap.name);
160 assert_eq!(back.role, snap.role);
161 assert_eq!(back.confidence_min, snap.confidence_min);
162 }
163
164 #[test]
165 fn formation_request_and_plan_roundtrip() {
166 let request = FormationRequest {
167 id: "req-1".to_string(),
168 required_roles: vec![SuggestorRole::Analysis, SuggestorRole::Planning],
169 required_capabilities: vec![SuggestorCapability::Analytics],
170 };
171 let plan = FormationPlan {
172 request_id: request.id.clone(),
173 assignments: vec![RoleAssignment {
174 role: SuggestorRole::Analysis,
175 suggestor: "analysis-1".to_string(),
176 }],
177 unmatched_roles: vec![SuggestorRole::Planning],
178 coverage_ratio: 0.5,
179 };
180
181 let request_back: FormationRequest =
182 serde_json::from_str(&serde_json::to_string(&request).unwrap()).unwrap();
183 let plan_back: FormationPlan =
184 serde_json::from_str(&serde_json::to_string(&plan).unwrap()).unwrap();
185
186 assert_eq!(request_back.required_roles, request.required_roles);
187 assert_eq!(plan_back.assignments, plan.assignments);
188 assert_eq!(plan_back.unmatched_roles, plan.unmatched_roles);
189 }
190}