Skip to main content

geop_core_editor/agent/
resources.rs

1//! What a [`Session`] offers an agent to read besides its tools — the
2//! program, its status and elements, the operations, the examples — and
3//! the prompts it starts a task with. MCP's resources and prompts are
4//! these (`geop mcp`); like the tools, none names an operation.
5
6use geop_core_math::geop_error::{GeopError, GeopResult};
7use serde_json::{Value, json};
8
9use super::{Image, Session};
10
11/// Something to read: its URI, name, what it is and its media type. A
12/// `template` URI has one `{…}` part, filled in to read it.
13#[derive(Clone, Debug, PartialEq)]
14pub struct AgentResource {
15    pub uri: &'static str,
16    pub name: &'static str,
17    pub description: &'static str,
18    pub media_type: &'static str,
19}
20
21/// Every resource to read as it is, in the order they are offered.
22pub fn resources() -> Vec<AgentResource> {
23    let resource = |uri, name, description, media_type| AgentResource {
24        uri,
25        name,
26        description,
27        media_type,
28    };
29    vec![
30        resource(
31            "geop://program",
32            "program",
33            "The program as it stands, as a .geop file holds it: what `load_program` takes as `text`.",
34            "application/json",
35        ),
36        resource(
37            "geop://status",
38            "status",
39            "Every step's status (as `run` answers, without running), the step open, the tool in hand, the tools and panels there are.",
40            "application/json",
41        ),
42        resource(
43            "geop://elements",
44            "elements",
45            "The part's elements by their stable names, as `list_elements` answers.",
46            "application/json",
47        ),
48        resource(
49            "geop://operations",
50            "operations",
51            "Every operation a step can be, as `list_operations` answers.",
52            "application/json",
53        ),
54        resource(
55            "geop://examples",
56            "examples",
57            "The names of the example programs, and of the examples of several files.",
58            "application/json",
59        ),
60        resource(
61            "geop://picture",
62            "picture",
63            "The part as `look` last saw it — isometric, or facing the plane the open step works in, if it has not looked yet — as a PNG.",
64            "image/png",
65        ),
66    ]
67}
68
69/// Every resource read by filling in its URI's one `{…}` part.
70pub fn resource_templates() -> Vec<AgentResource> {
71    vec![
72        AgentResource {
73            uri: "geop://operations/{kind}",
74            name: "operation",
75            description: "One operation in full, as `list_operations` given its `kind` answers: the arguments a new step of it starts with and its form.",
76            media_type: "application/json",
77        },
78        AgentResource {
79            uri: "geop://examples/{name}",
80            name: "example",
81            description: "An example program as a .geop file holds it — or, for an example of several files, each file's program by its path: how a whole part is written.",
82            media_type: "application/json",
83        },
84    ]
85}
86
87/// What a resource holds: text, or a picture.
88#[derive(Clone, Debug, PartialEq)]
89pub enum Contents {
90    Text(String),
91    Image(Image),
92}
93
94/// A prompt an agent starts a task with: its name, what it is for and its
95/// arguments, by name, with what each is and whether it is required.
96#[derive(Clone, Debug, PartialEq)]
97pub struct AgentPrompt {
98    pub name: &'static str,
99    pub description: &'static str,
100    pub arguments: Vec<(&'static str, &'static str, bool)>,
101}
102
103/// Every prompt, in the order they are offered.
104pub fn prompts() -> Vec<AgentPrompt> {
105    vec![
106        AgentPrompt {
107            name: "model_part",
108            description: "Model a part from a description, step by step, checking each step and looking at the result.",
109            arguments: vec![(
110                "description",
111                "What to model: its shape, sizes and features.",
112                true,
113            )],
114        },
115        AgentPrompt {
116            name: "fix_failing_steps",
117            description: "Find out why the program's failing steps fail, and fix them.",
118            arguments: vec![],
119        },
120        AgentPrompt {
121            name: "review_part",
122            description: "Look the part over from every side, weigh it and check it for overlapping solids, and say what is wrong with it.",
123            arguments: vec![],
124        },
125    ]
126}
127
128/// How a part is modelled with the tools: what every prompt starts from.
129const WORKFLOW: &str = "\
130Model it with the geop tools, one step at a time:
1311. `list_operations` for what there is, and `list_operations` with a `kind` for the arguments a step of it takes (`default_args`) and what each field is (`form`). The example programs (resources `geop://examples/{name}`) show how whole parts are written.
1322. `add_step` each step, and read the status it answers with: a step that does not build says why. Fix it with `edit_step` before going on.
1333. `list_elements` for the names of faces, edges and vertices a later step refers to (a sketch on a face, a fillet of edges).
1344. `look` at the part from a few sides (`iso`, `front`, `top`, ...) to see that it is what was asked for, and `inspect` it: `mass_properties`, `measure` the sizes that matter.
135Where writing a step's arguments is hard — a sketch to draw, a part placed to drag — `open_step` it and do what a user does: `step_event` clicks at points of the plane the step works in, keys pick its tools (a sketch's `l` draws lines), then `commit_step`. `undo` takes back an edit.";
136
137impl Session {
138    /// The resource `uri`, read: what it holds and its media type.
139    pub fn read_resource(&mut self, uri: &str) -> GeopResult<(Contents, &'static str)> {
140        let json = |value: Value| -> GeopResult<(Contents, &'static str)> {
141            let text = serde_json::to_string_pretty(&value)
142                .map_err(|e| GeopError::new(format!("writing {uri}: {e}")))?;
143            Ok((Contents::Text(text), "application/json"))
144        };
145        let path = (uri.strip_prefix("geop://")).ok_or_else(|| {
146            GeopError::new(format!("there is no resource {uri}: geop's are geop://…"))
147        })?;
148        match path.split_once('/') {
149            None => match path {
150                "program" => Ok((
151                    Contents::Text(self.editor.program().to_json()?),
152                    "application/json",
153                )),
154                "status" => json(self.status()?),
155                "elements" => json(self.list_elements()?),
156                "operations" => json(self.list_operations(None)?),
157                "examples" => {
158                    let program = self.shown()?;
159                    json(json!({
160                        "examples": program.examples,
161                        "workspace_examples": program.workspace_examples,
162                    }))
163                }
164                "picture" => {
165                    let data = self.picture(None)?.0;
166                    let media_type = "image/png";
167                    Ok((Contents::Image(Image { media_type, data }), media_type))
168                }
169                _ => Err(self.no_resource(uri)),
170            },
171            Some(("operations", kind)) => json(self.list_operations(Some(kind))?),
172            Some(("examples", name)) => {
173                let registry = self.editor.registry();
174                if self.shown()?.examples.contains(&name) {
175                    let program = registry.example(name)?;
176                    return Ok((Contents::Text(program.to_json()?), "application/json"));
177                }
178                let files = registry.workspace_example(name)?;
179                let files = (files.into_iter())
180                    .map(|(path, program)| Ok((path.to_string(), serde_json::to_value(&program)?)))
181                    .collect::<Result<serde_json::Map<_, _>, serde_json::Error>>()
182                    .map_err(|e| GeopError::new(format!("writing the example {name}: {e}")))?;
183                json(Value::Object(files))
184            }
185            _ => Err(self.no_resource(uri)),
186        }
187    }
188
189    fn no_resource(&self, uri: &str) -> GeopError {
190        let uris: Vec<&str> = (resources().into_iter().chain(resource_templates()))
191            .map(|r| r.uri)
192            .collect();
193        GeopError::new(format!(
194            "there is no resource {uri}; there are {}",
195            uris.join(", ")
196        ))
197    }
198
199    /// The prompt `name` with `arguments` filled in: what it is for, and
200    /// its text.
201    pub fn prompt(&mut self, name: &str, arguments: &Value) -> GeopResult<(&'static str, String)> {
202        let prompt = (prompts().into_iter())
203            .find(|p| p.name == name)
204            .ok_or_else(|| {
205                let names: Vec<&str> = prompts().iter().map(|p| p.name).collect();
206                GeopError::new(format!(
207                    "there is no prompt {name}; there are {}",
208                    names.join(", ")
209                ))
210            })?;
211        let argument = |key: &str| {
212            (arguments.get(key).and_then(Value::as_str))
213                .ok_or_else(|| GeopError::new(format!("the prompt {name} needs `{key}`")))
214        };
215        let status = || -> GeopResult<String> {
216            serde_json::to_string_pretty(&self.status()?)
217                .map_err(|e| GeopError::new(format!("writing the status: {e}")))
218        };
219        let text = match name {
220            "model_part" => format!(
221                "Model this part in geop:\n\n{}\n\n{WORKFLOW}\n\nStart from `new_part`, unless the program open is the one to change.",
222                argument("description")?
223            ),
224            "fix_failing_steps" => format!(
225                "Some steps of the geop program open fail. Its status:\n\n{}\n\nFor each failing step, the first first: read its error — it names the step, the operation and the argument, with the kernel's context — look at what the steps before it built (`list_elements`, `look`), and fix the step's arguments with `edit_step`, or the step it refers to. Do not delete a step to make the status clean: the part has to stay what it was meant to be. Then `run`, and `look` at the part.\n\n{WORKFLOW}",
226                status()?
227            ),
228            "review_part" => format!(
229                "Review the geop part open. Its status:\n\n{}\n\n`look` at it from `iso`, `front`, `top` and `right` (and from below, if it has features there); `inspect` its `mass_properties` and its `interference`; `measure` the sizes that look wrong. Then say what is wrong with it, by the names of its steps and elements, and what to change. Change nothing yet.",
230                status()?
231            ),
232            _ => return Err(GeopError::new(format!("the prompt {name} has no text"))),
233        };
234        Ok((prompt.description, text))
235    }
236}