Skip to main content

netget/client/dict/
actions.rs

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}