1use crate::llm::actions::{
4 client_trait::{Client, ClientActionResult},
5 protocol_trait::Protocol,
6 ActionDefinition, Parameter, ParameterDefinition,
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 XMLRPC_CLIENT_CONNECTED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
16 EventType::new(
17 "xmlrpc_connected",
18 "XML-RPC client initialized and ready to call methods",
19 json!({
20 "type": "call_xmlrpc_method",
21 "method_name": "system.listMethods",
22 "params": []
23 }),
24 )
25 .with_parameters(vec![Parameter {
26 name: "server_url".to_string(),
27 type_hint: "string".to_string(),
28 description: "XML-RPC server URL".to_string(),
29 required: true,
30 }])
31});
32
33pub static XMLRPC_CLIENT_RESPONSE_RECEIVED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
35 EventType::new(
36 "xmlrpc_response_received",
37 "XML-RPC response received from server",
38 json!({
39 "type": "call_xmlrpc_method",
40 "method_name": "system.listMethods",
41 "params": []
42 }),
43 )
44 .with_parameters(vec![
45 Parameter {
46 name: "method_name".to_string(),
47 type_hint: "string".to_string(),
48 description: "Name of the method that was called".to_string(),
49 required: true,
50 },
51 Parameter {
52 name: "result".to_string(),
53 type_hint: "any".to_string(),
54 description: "Method call result (can be string, number, array, struct, etc.)"
55 .to_string(),
56 required: false,
57 },
58 Parameter {
59 name: "fault".to_string(),
60 type_hint: "object".to_string(),
61 description: "Fault information if the call failed".to_string(),
62 required: false,
63 },
64 ])
65});
66
67pub struct XmlRpcClientProtocol;
69
70impl XmlRpcClientProtocol {
71 pub fn new() -> Self {
72 Self
73 }
74}
75
76impl Protocol for XmlRpcClientProtocol {
78 fn get_startup_parameters(&self) -> Vec<ParameterDefinition> {
79 vec![ParameterDefinition {
80 name: "timeout_secs".to_string(),
81 description: "Request timeout in seconds (default: 30)".to_string(),
82 type_hint: "number".to_string(),
83 required: false,
84 example: json!(30),
85 default: None,
86 }]
87 }
88 fn get_async_actions(&self, _state: &AppState) -> Vec<ActionDefinition> {
89 vec![
90 ActionDefinition {
91 name: "call_xmlrpc_method".to_string(),
92 description: "Call an XML-RPC method on the server".to_string(),
93 parameters: vec![
94 Parameter {
95 name: "method_name".to_string(),
96 type_hint: "string".to_string(),
97 description: "Name of the XML-RPC method to call".to_string(),
98 required: true,
99 },
100 Parameter {
101 name: "params".to_string(),
102 type_hint: "array".to_string(),
103 description: "Array of parameters for the method call (strings, numbers, bools, arrays, objects)".to_string(),
104 required: false,
105 },
106 ],
107 example: json!({
108 "type": "call_xmlrpc_method",
109 "method_name": "examples.getStateName",
110 "params": [41]
111 }),
112 log_template: None,
113 },
114 ActionDefinition {
115 name: "disconnect".to_string(),
116 description: "Disconnect from the XML-RPC server".to_string(),
117 parameters: vec![],
118 example: json!({
119 "type": "disconnect"
120 }),
121 log_template: None,
122 },
123 ]
124 }
125 fn get_sync_actions(&self) -> Vec<ActionDefinition> {
126 vec![ActionDefinition {
127 name: "call_xmlrpc_method".to_string(),
128 description: "Call another XML-RPC method in response to received data".to_string(),
129 parameters: vec![
130 Parameter {
131 name: "method_name".to_string(),
132 type_hint: "string".to_string(),
133 description: "Name of the XML-RPC method to call".to_string(),
134 required: true,
135 },
136 Parameter {
137 name: "params".to_string(),
138 type_hint: "array".to_string(),
139 description: "Array of parameters for the method call".to_string(),
140 required: false,
141 },
142 ],
143 example: json!({
144 "type": "call_xmlrpc_method",
145 "method_name": "system.listMethods",
146 "params": []
147 }),
148 log_template: None,
149 }]
150 }
151 fn protocol_name(&self) -> &'static str {
152 "XML-RPC"
153 }
154 fn get_event_types(&self) -> Vec<EventType> {
155 vec![
156 EventType::new(
157 "xmlrpc_connected",
158 "Triggered when XML-RPC client is initialized",
159 json!({"type": "placeholder", "event_id": "xmlrpc_connected"}),
160 ),
161 EventType::new(
162 "xmlrpc_response_received",
163 "Triggered when XML-RPC client receives a response",
164 json!({"type": "placeholder", "event_id": "xmlrpc_response_received"}),
165 ),
166 ]
167 }
168 fn stack_name(&self) -> &'static str {
169 "ETH>IP>TCP>HTTP>XML-RPC"
170 }
171 fn keywords(&self) -> Vec<&'static str> {
172 vec!["xmlrpc", "xml-rpc", "xml rpc", "rpc", "connect to xmlrpc"]
173 }
174 fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
175 use crate::protocol::metadata::{DevelopmentState, ProtocolMetadataV2};
176
177 ProtocolMetadataV2::builder()
178 .state(DevelopmentState::Experimental)
179 .implementation("xmlrpc crate for method calls")
180 .llm_control("Full control over method calls with structured parameters")
181 .e2e_testing("Public XML-RPC test servers or local implementation")
182 .build()
183 }
184 fn description(&self) -> &'static str {
185 "XML-RPC client for calling remote procedures"
186 }
187 fn example_prompt(&self) -> &'static str {
188 "Connect to http://example.com/xmlrpc and call system.listMethods"
189 }
190 fn group_name(&self) -> &'static str {
191 "RPC & API"
192 }
193 fn get_startup_examples(&self) -> crate::llm::actions::StartupExamples {
194 use crate::llm::actions::StartupExamples;
195 use serde_json::json;
196
197 StartupExamples::new(
198 json!({
200 "type": "open_client",
201 "remote_addr": "http://example.com/xmlrpc",
202 "base_stack": "xmlrpc",
203 "instruction": "Call system.listMethods to discover available methods"
204 }),
205 json!({
207 "type": "open_client",
208 "remote_addr": "http://example.com/xmlrpc",
209 "base_stack": "xmlrpc",
210 "event_handlers": [{
211 "event_pattern": "xmlrpc_response_received",
212 "handler": {
213 "type": "script",
214 "language": "python",
215 "code": "<xmlrpc_client_handler>"
216 }
217 }]
218 }),
219 json!({
221 "type": "open_client",
222 "remote_addr": "http://example.com/xmlrpc",
223 "base_stack": "xmlrpc",
224 "event_handlers": [
225 {
226 "event_pattern": "xmlrpc_connected",
227 "handler": {
228 "type": "static",
229 "actions": [{
230 "type": "call_xmlrpc_method",
231 "method_name": "system.listMethods",
232 "params": []
233 }]
234 }
235 },
236 {
237 "event_pattern": "xmlrpc_response_received",
238 "handler": {
239 "type": "static",
240 "actions": [{
241 "type": "disconnect"
242 }]
243 }
244 }
245 ]
246 }),
247 )
248 }
249}
250
251impl Client for XmlRpcClientProtocol {
253 fn connect(
254 &self,
255 ctx: crate::protocol::ConnectContext,
256 ) -> std::pin::Pin<
257 Box<dyn std::future::Future<Output = anyhow::Result<std::net::SocketAddr>> + Send>,
258 > {
259 Box::pin(async move {
260 use crate::client::xmlrpc::XmlRpcClient;
261 XmlRpcClient::connect_with_llm_actions(
262 ctx.remote_addr,
263 ctx.llm_client,
264 ctx.state,
265 ctx.status_tx,
266 ctx.client_id,
267 ctx.startup_params,
268 )
269 .await
270 })
271 }
272 fn execute_action(&self, action: serde_json::Value) -> Result<ClientActionResult> {
273 let action_type = action
274 .get("type")
275 .and_then(|v| v.as_str())
276 .context("Missing 'type' field in action")?;
277
278 match action_type {
279 "call_xmlrpc_method" => {
280 let method_name = action
281 .get("method_name")
282 .and_then(|v| v.as_str())
283 .context("Missing 'method_name' field")?
284 .to_string();
285
286 let params = action
287 .get("params")
288 .and_then(|v| v.as_array())
289 .cloned()
290 .unwrap_or_default();
291
292 Ok(ClientActionResult::Custom {
294 name: "xmlrpc_call".to_string(),
295 data: json!({
296 "method_name": method_name,
297 "params": params,
298 }),
299 })
300 }
301 "disconnect" => Ok(ClientActionResult::Disconnect),
302 _ => Err(anyhow::anyhow!(
303 "Unknown XML-RPC client action: {}",
304 action_type
305 )),
306 }
307 }
308}