aion_server/assistant/
descriptor.rs1use aion::Engine;
10use serde::Serialize;
11use serde_json::Value;
12
13use super::document::{
14 CONTINUE_END_FIELD, CONTINUE_MESSAGE_FIELD, CONTINUE_SIGNAL, EMBEDDED_ASSISTANT_FILENAME,
15 EmbeddedAssistant, OBJECTIVE_INPUT, REPO_PATH_INPUT, STATUS_QUERY,
16};
17
18#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
20#[serde(rename_all = "snake_case", tag = "state")]
21pub enum AssistantResidency {
22 Routed,
25 LoadedNotRouted {
28 routed_hash: Option<String>,
30 },
31 NotLoaded {
35 routed_hash: Option<String>,
37 },
38 Unknown {
40 reason: String,
42 },
43}
44
45#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
49pub struct AssistantSessionContract {
50 pub objective_input: &'static str,
52 pub repo_path_input: &'static str,
54 pub continue_signal: &'static str,
56 pub message_field: &'static str,
58 pub end_field: &'static str,
60 pub status_query: &'static str,
62}
63
64impl AssistantSessionContract {
65 #[must_use]
67 pub const fn current() -> Self {
68 Self {
69 objective_input: OBJECTIVE_INPUT,
70 repo_path_input: REPO_PATH_INPUT,
71 continue_signal: CONTINUE_SIGNAL,
72 message_field: CONTINUE_MESSAGE_FIELD,
73 end_field: CONTINUE_END_FIELD,
74 status_query: STATUS_QUERY,
75 }
76 }
77}
78
79#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
81pub struct AssistantSignal {
82 pub name: String,
84 pub input_schema: Value,
86}
87
88#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
90pub struct AssistantDescriptor {
91 pub workflow_type: String,
93 pub content_hash: String,
95 pub document_filename: &'static str,
97 pub document_bytes: usize,
100 pub input_schema: Value,
102 pub signals: Vec<AssistantSignal>,
104 pub queries: Vec<String>,
106 pub session: AssistantSessionContract,
108 pub residency: AssistantResidency,
110}
111
112#[must_use]
114pub fn describe(embedded: &EmbeddedAssistant, engine: &Engine) -> AssistantDescriptor {
115 AssistantDescriptor {
116 workflow_type: embedded.workflow_type().to_owned(),
117 content_hash: embedded.content_hash().to_string(),
118 document_filename: EMBEDDED_ASSISTANT_FILENAME,
119 document_bytes: embedded.source().len(),
120 input_schema: embedded.input_schema().clone(),
121 signals: embedded
122 .signals()
123 .iter()
124 .map(|signal| AssistantSignal {
125 name: signal.name.clone(),
126 input_schema: signal.input_schema.clone(),
127 })
128 .collect(),
129 queries: embedded.queries().to_vec(),
130 session: AssistantSessionContract::current(),
131 residency: residency(embedded, engine),
132 }
133}
134
135fn residency(embedded: &EmbeddedAssistant, engine: &Engine) -> AssistantResidency {
137 let versions = match engine.list_workflow_versions() {
138 Ok(versions) => versions,
139 Err(error) => {
140 return AssistantResidency::Unknown {
141 reason: format!("the engine catalog could not be read: {error}"),
142 };
143 }
144 };
145 let embedded_hash = embedded.content_hash().to_string();
146 let resident: Vec<_> = versions
147 .into_iter()
148 .filter(|version| version.workflow_type == embedded.workflow_type())
149 .collect();
150 let loaded = resident
151 .iter()
152 .any(|version| version.content_hash.to_string() == embedded_hash);
153 let routed_hash = resident
154 .iter()
155 .find(|version| version.route_active)
156 .map(|version| version.content_hash.to_string());
157 if !loaded {
158 return AssistantResidency::NotLoaded { routed_hash };
159 }
160 if routed_hash.as_deref() == Some(embedded_hash.as_str()) {
161 AssistantResidency::Routed
162 } else {
163 AssistantResidency::LoadedNotRouted { routed_hash }
164 }
165}