1use crate::llm::actions::{
4 client_trait::{Client, ClientActionResult},
5 protocol_trait::Protocol,
6 ActionDefinition, Parameter,
7};
8use crate::protocol::EventType;
9use crate::state::app_state::AppState;
10use anyhow::{Context, Result};
11use serde_json::json;
12use std::sync::LazyLock;
13
14pub static NTP_CLIENT_CONNECTED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
16 EventType::new(
17 "ntp_connected",
18 "NTP client ready to query time servers",
19 json!({"type": "placeholder", "event_id": "ntp_connected"}),
20 )
21 .with_parameters(vec![Parameter {
22 name: "remote_addr".to_string(),
23 type_hint: "string".to_string(),
24 description: "NTP server address".to_string(),
25 required: true,
26 }])
27});
28
29pub static NTP_CLIENT_RESPONSE_RECEIVED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
31 EventType::new(
32 "ntp_response_received",
33 "NTP time response received from server",
34 json!({"type": "analyze_response"}),
35 )
36 .with_parameters(vec![
37 Parameter {
38 name: "origin_timestamp".to_string(),
39 type_hint: "number".to_string(),
40 description: "Origin timestamp (Unix epoch)".to_string(),
41 required: true,
42 },
43 Parameter {
44 name: "receive_timestamp".to_string(),
45 type_hint: "number".to_string(),
46 description: "Server receive timestamp (Unix epoch)".to_string(),
47 required: true,
48 },
49 Parameter {
50 name: "transmit_timestamp".to_string(),
51 type_hint: "number".to_string(),
52 description: "Server transmit timestamp (Unix epoch)".to_string(),
53 required: true,
54 },
55 Parameter {
56 name: "stratum".to_string(),
57 type_hint: "number".to_string(),
58 description: "Server stratum level (0-15)".to_string(),
59 required: true,
60 },
61 Parameter {
62 name: "precision".to_string(),
63 type_hint: "number".to_string(),
64 description: "Server precision (log2 seconds)".to_string(),
65 required: true,
66 },
67 ])
68});
69
70pub struct NtpClientProtocol;
72
73impl NtpClientProtocol {
74 pub fn new() -> Self {
75 Self
76 }
77}
78
79impl Default for NtpClientProtocol {
80 fn default() -> Self {
81 Self::new()
82 }
83}
84
85impl Protocol for NtpClientProtocol {
87 fn get_async_actions(&self, _state: &AppState) -> Vec<ActionDefinition> {
88 vec![
89 ActionDefinition {
90 name: "query_time".to_string(),
91 description: "Query NTP server for current time".to_string(),
92 parameters: vec![],
93 example: json!({
94 "type": "query_time"
95 }),
96 log_template: None,
97 },
98 ActionDefinition {
99 name: "disconnect".to_string(),
100 description: "Close NTP client".to_string(),
101 parameters: vec![],
102 example: json!({
103 "type": "disconnect"
104 }),
105 log_template: None,
106 },
107 ]
108 }
109 fn get_sync_actions(&self) -> Vec<ActionDefinition> {
110 vec![ActionDefinition {
111 name: "analyze_response".to_string(),
112 description: "Analyze NTP response (no action needed, just for LLM understanding)"
113 .to_string(),
114 parameters: vec![],
115 example: json!({
116 "type": "analyze_response"
117 }),
118 log_template: None,
119 }]
120 }
121 fn protocol_name(&self) -> &'static str {
122 "NTP"
123 }
124 fn get_event_types(&self) -> Vec<EventType> {
125 vec![
126 EventType::new(
127 "ntp_connected",
128 "Triggered when NTP client is ready",
129 json!({"type": "placeholder", "event_id": "ntp_connected"}),
130 ),
131 EventType::new(
132 "ntp_response_received",
133 "Triggered when NTP response is received",
134 json!({"type": "placeholder", "event_id": "ntp_response_received"}),
135 ),
136 ]
137 }
138 fn stack_name(&self) -> &'static str {
139 "ETH>IP>UDP>NTP"
140 }
141 fn keywords(&self) -> Vec<&'static str> {
142 vec!["ntp", "ntp client", "time sync", "network time"]
143 }
144 fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
145 use crate::protocol::metadata::{DevelopmentState, ProtocolMetadataV2};
146
147 ProtocolMetadataV2::builder()
148 .state(DevelopmentState::Experimental)
149 .implementation("UDP-based NTP client with manual packet construction")
150 .llm_control("Query time servers and interpret timestamps, stratum, precision")
151 .e2e_testing("Public NTP servers (pool.ntp.org)")
152 .build()
153 }
154 fn description(&self) -> &'static str {
155 "NTP client for querying network time servers"
156 }
157 fn example_prompt(&self) -> &'static str {
158 "Query time.google.com:123 and show the time offset"
159 }
160 fn group_name(&self) -> &'static str {
161 "Network Infrastructure"
162 }
163
164 fn get_startup_examples(&self) -> crate::llm::actions::StartupExamples {
165 use crate::llm::actions::StartupExamples;
166 use serde_json::json;
167
168 StartupExamples::new(
169 json!({
171 "type": "open_client",
172 "remote_addr": "time.google.com:123",
173 "base_stack": "ntp",
174 "instruction": "Query the time server and report the current time offset and stratum level"
175 }),
176 json!({
178 "type": "open_client",
179 "remote_addr": "pool.ntp.org:123",
180 "base_stack": "ntp",
181 "event_handlers": [{
182 "event_pattern": "ntp_response_received",
183 "handler": {
184 "type": "script",
185 "language": "python",
186 "code": "<ntp_client_handler>"
187 }
188 }]
189 }),
190 json!({
192 "type": "open_client",
193 "remote_addr": "time.google.com:123",
194 "base_stack": "ntp",
195 "event_handlers": [
196 {
197 "event_pattern": "ntp_connected",
198 "handler": {
199 "type": "static",
200 "actions": [{
201 "type": "query_time"
202 }]
203 }
204 },
205 {
206 "event_pattern": "ntp_response_received",
207 "handler": {
208 "type": "static",
209 "actions": [{
210 "type": "disconnect"
211 }]
212 }
213 }
214 ]
215 }),
216 )
217 }
218}
219
220impl Client for NtpClientProtocol {
222 fn connect(
223 &self,
224 ctx: crate::protocol::ConnectContext,
225 ) -> std::pin::Pin<
226 Box<dyn std::future::Future<Output = anyhow::Result<std::net::SocketAddr>> + Send>,
227 > {
228 Box::pin(async move {
229 use crate::client::ntp::NtpClient;
230 NtpClient::connect_with_llm_actions(
231 ctx.remote_addr,
232 ctx.llm_client,
233 ctx.state,
234 ctx.status_tx,
235 ctx.client_id,
236 )
237 .await
238 })
239 }
240 fn execute_action(&self, action: serde_json::Value) -> Result<ClientActionResult> {
241 let action_type = action
242 .get("type")
243 .and_then(|v| v.as_str())
244 .context("Missing 'type' field in action")?;
245
246 match action_type {
247 "query_time" => Ok(ClientActionResult::Custom {
248 name: "ntp_query".to_string(),
249 data: json!({}),
250 }),
251 "disconnect" => Ok(ClientActionResult::Disconnect),
252 "analyze_response" => Ok(ClientActionResult::WaitForMore),
253 _ => Err(anyhow::anyhow!(
254 "Unknown NTP client action: {}",
255 action_type
256 )),
257 }
258 }
259}