1pub mod app;
10pub mod compose;
11
12use crate::schema;
13use serde_json::{json, Map, Value};
14use std::{future::Future, pin::Pin, sync::Arc};
15
16pub type ToolFuture = Pin<Box<dyn Future<Output = Result<ToolOutput, String>> + Send>>;
17
18#[derive(Default)]
20pub struct ToolOutput {
21 pub json: Value,
22 pub images: Vec<ToolImage>,
23}
24
25pub struct ToolImage {
26 pub png: Vec<u8>,
27 pub mime: &'static str,
28}
29
30impl ToolOutput {
31 pub fn json(v: Value) -> Self {
32 Self { json: v, images: Vec::new() }
33 }
34}
35
36#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
39pub struct Annotations {
40 pub read_only: bool,
41 pub destructive: bool,
42 pub idempotent: bool,
43 pub waits: bool,
44}
45
46impl Annotations {
47 pub const READ: Annotations = Annotations { read_only: true, destructive: false, idempotent: true, waits: false };
48 pub const MUTATE: Annotations = Annotations { read_only: false, destructive: false, idempotent: false, waits: true };
49 pub const DESTRUCTIVE: Annotations = Annotations { read_only: false, destructive: true, idempotent: false, waits: true };
50}
51
52#[derive(Clone)]
53pub struct ToolSpec {
54 pub name: String,
55 pub group: &'static str,
56 pub doc: String,
57 pub input_schema: Value,
58 pub annotations: Annotations,
59 pub handler: Arc<dyn Fn(Value) -> ToolFuture + Send + Sync>,
60}
61
62impl ToolSpec {
63 pub fn new(
64 name: impl Into<String>,
65 group: &'static str,
66 doc: impl Into<String>,
67 input_schema: Value,
68 annotations: Annotations,
69 handler: impl Fn(Value) -> ToolFuture + Send + Sync + 'static,
70 ) -> Self {
71 Self {
72 name: name.into(),
73 group,
74 doc: doc.into(),
75 input_schema,
76 annotations,
77 handler: Arc::new(handler),
78 }
79 }
80}
81
82impl std::fmt::Debug for ToolSpec {
83 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
84 f.debug_struct("ToolSpec")
85 .field("name", &self.name)
86 .field("group", &self.group)
87 .finish()
88 }
89}
90
91#[derive(Debug, Default)]
94pub struct ToolSet {
95 pub specs: Vec<ToolSpec>,
96}
97
98impl ToolSet {
99 pub fn new(specs: Vec<ToolSpec>) -> Self {
100 Self { specs }
101 }
102 pub fn extend(&mut self, more: Vec<ToolSpec>) {
103 self.specs.extend(more);
104 }
105 pub fn get(&self, name: &str) -> Option<&ToolSpec> {
106 self.specs.iter().find(|s| s.name == name)
107 }
108 pub fn names(&self) -> Vec<&str> {
109 self.specs.iter().map(|s| s.name.as_str()).collect()
110 }
111}
112
113pub fn object_schema(properties: Value, required: &[&str]) -> Value {
115 json!({
116 "type": "object",
117 "properties": properties,
118 "required": required,
119 "additionalProperties": false,
120 })
121}
122
123fn arg_str(args: &Value, key: &str) -> Result<String, String> {
124 args.get(key)
125 .and_then(Value::as_str)
126 .map(str::to_string)
127 .ok_or_else(|| format!("missing string argument `{key}`"))
128}
129
130pub fn engine_tools() -> Vec<ToolSpec> {
133 vec![
134 ToolSpec::new(
135 "feature_catalogue",
136 "features",
137 "List every feature the kernel catalogue exposes: type, shortName, longName, displayBuilder. \
138 Read `brep://schema/features/{type}` or call `feature_schema` for a feature's parameter schema.",
139 object_schema(json!({}), &[]),
140 Annotations::READ,
141 |_args| {
142 Box::pin(async move {
143 let features: Vec<Value> = schema::entries()
144 .iter()
145 .map(|e| {
146 let id = schema::identity(e);
147 json!({
148 "type": id.feature_type,
149 "shortName": id.short_name,
150 "longName": id.long_name,
151 "displayBuilder": id.display_builder,
152 })
153 })
154 .collect();
155 Ok(ToolOutput::json(json!({ "features": features })))
156 })
157 },
158 ),
159 ToolSpec::new(
160 "feature_schema",
161 "features",
162 "The JSON Schema of one feature's inputParams (derived from the kernel's inputParamsSchema) \
163 and its default parameter values. `type` is the catalogue type or shortName, e.g. `E`, `P.CU`, `B`. \
164 A feature that carries a `persistentData` block (the sketch profile Extrude and Revolve consume) also \
165 returns `persistentData` — its schema — and `persistentDataExample`, a complete working block.",
166 object_schema(json!({ "type": { "type": "string", "description": "feature type or shortName" } }), &["type"]),
167 Annotations::READ,
168 |args| {
169 Box::pin(async move {
170 let ty = arg_str(&args, "type")?;
171 let entry = schema::entry(&ty).ok_or_else(|| format!("unknown feature type `{ty}`"))?;
172 let id = schema::identity(&entry);
173 let mut out = json!({
174 "type": id.feature_type,
175 "longName": id.long_name,
176 "schema": schema::to_json_schema(&entry),
177 "defaults": schema::defaults(&id.feature_type),
178 });
179 if schema::carries_sketch(&id.feature_type) {
183 out["persistentData"] = schema::sketch_persistent_schema();
184 out["persistentDataExample"] = schema::sketch_example();
185 }
186 Ok(ToolOutput::json(out))
187 })
188 },
189 ),
190 ]
191}
192
193pub fn args_object(args: &Value) -> Map<String, Value> {
195 args.as_object().cloned().unwrap_or_default()
196}
197
198