1use super::wire::Request;
2use crate::llm::actions::{
3 client_trait::{Client, ClientActionResult},
4 protocol_trait::Protocol,
5 ActionDefinition, Parameter,
6};
7use crate::protocol::log_template::LogTemplate;
8use crate::protocol::{ConnectContext, EventType};
9use crate::state::app_state::AppState;
10use anyhow::{bail, Result};
11use serde_json::{json, Value};
12use std::sync::LazyLock;
13#[derive(Default)]
14pub struct DictClientProtocol;
15impl DictClientProtocol {
16 pub fn new() -> Self {
17 Self
18 }
19}
20fn parameter(name: &str, kind: &str, description: &str, required: bool) -> Parameter {
21 Parameter {
22 name: name.into(),
23 type_hint: kind.into(),
24 description: description.into(),
25 required,
26 }
27}
28fn action(
29 name: &str,
30 description: &str,
31 parameters: Vec<Parameter>,
32 example: Value,
33 log_template: &str,
34) -> ActionDefinition {
35 ActionDefinition {
36 name: name.into(),
37 description: description.into(),
38 parameters,
39 example,
40 log_template: Some(LogTemplate::new().with_info(log_template)),
41 }
42}
43fn request_action() -> ActionDefinition {
44 action(
45 "dict_request",
46 "Send a structured RFC 2229 dictionary request",
47 vec![
48 parameter(
49 "operation",
50 "string",
51 "define/match/databases/strategies/info/server/status/help/client/quit",
52 true,
53 ),
54 parameter(
55 "database",
56 "string",
57 "Database name; define/match default to * (all databases)",
58 false,
59 ),
60 parameter("word", "string", "Word to define or match", false),
61 parameter(
62 "strategy",
63 "string",
64 "Match strategy, default . (server default)",
65 false,
66 ),
67 parameter(
68 "name",
69 "string",
70 "Client identification for operation client",
71 false,
72 ),
73 ],
74 json!({"type":"dict_request","operation":"databases"}),
75 "Request DICT operation {operation}",
76 )
77}
78fn disconnect_action() -> ActionDefinition {
79 action(
80 "disconnect",
81 "Close the DICT connection",
82 vec![],
83 json!({"type":"disconnect"}),
84 "Disconnect DICT server",
85 )
86}
87fn actions() -> Vec<ActionDefinition> {
88 vec![request_action(), disconnect_action()]
89}
90pub static CONNECTED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
91 EventType::new(
92 "dict_connected",
93 "Connected to DICT; send a request",
94 request_action().example.clone(),
95 )
96 .with_parameters(vec![
97 parameter("remote_addr", "string", "DICT server address", true),
98 parameter(
99 "greeting",
100 "string",
101 "Server greeting, capabilities and message ID",
102 true,
103 ),
104 ])
105 .with_actions(actions())
106});
107pub static RESPONSE_EVENT: LazyLock<EventType> = LazyLock::new(|| {
108 EventType::new(
109 "dict_response",
110 "Complete, validated dictionary response including protocol refusals.",
111 disconnect_action().example.clone(),
112 )
113 .with_parameters(vec![
114 parameter("request", "object", "Structured request", true),
115 parameter(
116 "response",
117 "object",
118 "code plus definitions, entries, text, message or protocol error",
119 true,
120 ),
121 ])
122 .with_actions(actions())
123});
124impl Protocol for DictClientProtocol {
125 fn protocol_name(&self) -> &'static str {
126 "DICT"
127 }
128 fn stack_name(&self) -> &'static str {
129 "ETH>IP>TCP>DICT"
130 }
131 fn keywords(&self) -> Vec<&'static str> {
132 vec!["dict", "dictionary", "definitions", "lookup"]
133 }
134 fn description(&self) -> &'static str {
135 "DICT dictionary client with structured definitions and search results"
136 }
137 fn get_async_actions(&self, _: &AppState) -> Vec<ActionDefinition> {
138 actions()
139 }
140 fn get_sync_actions(&self) -> Vec<ActionDefinition> {
141 vec![]
142 }
143 fn get_event_types(&self) -> Vec<EventType> {
144 vec![CONNECTED_EVENT.clone(), RESPONSE_EVENT.clone()]
145 }
146 fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
147 use crate::protocol::metadata::*;
148 ProtocolMetadataV2::builder().state(DevelopmentState::Experimental).privilege_requirement(PrivilegeRequirement::None).well_known_port(2628)
149 .implementation("Tokio TCP with RFC 2229 status lines, dot-stuffed blocks and bounded complete responses")
150 .llm_control("Dictionary definitions, match searches, database and strategy discovery, information and session commands")
151 .e2e_testing("tests/client/dict: independent dictd daemon, NetGet server pair, malformed framing, command injection and cleanup")
152 .notes("Plain UTF-8 DICT. No AUTH/SASL or MIME negotiation. Connect/write/greeting/response deadlines30s; commands1024 bytes, response lines64KiB, total response1MiB, entries4096. Tracked tasks; injected disconnect interrupts greeting and pending replies.")
153 .max_inbound_bytes(super::wire::MAX_RESPONSE_BYTES).build()
154 }
155 fn example_prompt(&self) -> &'static str {
156 "Connect to DICT at localhost:2628 and discover dictionary databases"
157 }
158 fn get_startup_examples(&self) -> crate::llm::actions::StartupExamples {
159 let llm = json!({"type":"open_client","protocol":"dict","remote_addr":"127.0.0.1:2628","instruction":"Discover dictionary databases"});
160 let mut static_example = llm.clone();
161 static_example["event_handlers"] = json!([{"event_pattern":"dict_connected","handler":{"type":"static","actions":[{"type":"dict_request","operation":"databases"}]}},{"event_pattern":"dict_response","handler":{"type":"static","actions":[]}}]);
162 let mut scripted = static_example.clone();
163 scripted["event_handlers"][0]["handler"] = json!({"type":"script","language":"python","code":"respond([{'type':'dict_request','operation':'databases'}])"});
164 crate::llm::actions::StartupExamples::new(llm, scripted, static_example)
165 }
166 fn group_name(&self) -> &'static str {
167 "Discovery"
168 }
169}
170impl Client for DictClientProtocol {
171 fn connect(
172 &self,
173 ctx: ConnectContext,
174 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<std::net::SocketAddr>> + Send>>
175 {
176 Box::pin(super::connect(ctx))
177 }
178 fn execute_action(&self, v: Value) -> Result<ClientActionResult> {
179 match v["type"].as_str() {
180 Some("dict_request") => {
181 Request::from_action(&v)?;
182 Ok(ClientActionResult::Custom {
183 name: "dict_request".into(),
184 data: v,
185 })
186 }
187 Some("disconnect") => Ok(ClientActionResult::Disconnect),
188 _ => bail!("Unknown DICT client action"),
189 }
190 }
191}