Skip to main content

oxicode_ai/dialect/
render.rs

1//! Dialect prompt rendering — tool catalog + format-guide injection.
2//!
3//! Port of omp's `dialect/catalog.ts` + `prompt-template.md`.
4
5use crate::tools::Tool;
6use serde_json::json;
7
8/// The prompt template wrapping the tool catalog and the dialect guide.
9///
10/// Mirrors omp's `dialect/prompt-template.md`. The two placeholders are filled
11/// by [`render_inband_tool_prompt`].
12const PROMPT_TEMPLATE: &str = r#"# Tools
13
14You may call one or more functions to assist with the user query.
15Tool calls are emitted as text using the exact syntax below, not as native provider tool messages.
16
17Available functions are listed inside `<tools></tools>` as one JSON object per line:
18
19<tools>
20{{TOOLS}}
21</tools>
22
23{{DIALECT}}
24"#;
25
26const TOOLS_TOKEN: &str = "{{TOOLS}}";
27const DIALECT_TOKEN: &str = "{{DIALECT}}";
28
29/// Render the tool catalog — one JSON object per line.
30///
31/// Each line is `{"type":"function","function":{"name":..,"description":..,"parameters":..}}`,
32/// matching omp's `renderToolCatalog`.
33pub fn render_tool_catalog(tools: &[Tool]) -> String {
34    tools
35        .iter()
36        .map(|tool| {
37            let obj = json!({
38                "type": "function",
39                "function": {
40                    "name": tool.name,
41                    "description": tool.description,
42                    "parameters": tool.parameters,
43                },
44            });
45            serde_json::to_string(&obj).unwrap_or_default()
46        })
47        .collect::<Vec<_>>()
48        .join("\n")
49}
50
51/// Render the full in-band tool prompt: catalog + dialect format guide.
52pub fn render_inband_tool_prompt(tools: &[Tool], dialect: super::Dialect) -> String {
53    let guide = dialect.prompt();
54    let guide = guide.trim();
55    PROMPT_TEMPLATE
56        .replace(TOOLS_TOKEN, &render_tool_catalog(tools))
57        .replace(DIALECT_TOKEN, guide)
58}
59
60#[cfg(test)]
61mod tests {
62    use super::*;
63    use crate::dialect::Dialect;
64    use serde_json::json;
65
66    fn echo_tool() -> Tool {
67        Tool {
68            name: "echo".to_string(),
69            description: "Echo a message".to_string(),
70            parameters: json!({
71                "type": "object",
72                "properties": {"msg": {"type": "string"}},
73                "required": ["msg"],
74            }),
75        }
76    }
77
78    #[test]
79    fn catalog_is_one_json_object_per_line() {
80        let tools = vec![echo_tool(), echo_tool()];
81        let catalog = render_tool_catalog(&tools);
82        let lines: Vec<&str> = catalog.lines().collect();
83        assert_eq!(lines.len(), 2);
84        for line in lines {
85            let v: serde_json::Value = serde_json::from_str(line).unwrap();
86            assert_eq!(v["type"], "function");
87            assert_eq!(v["function"]["name"], "echo");
88        }
89    }
90
91    #[test]
92    fn prompt_substitutes_both_tokens() {
93        let prompt = render_inband_tool_prompt(&[echo_tool()], Dialect::Xml);
94        assert!(!prompt.contains(TOOLS_TOKEN));
95        assert!(!prompt.contains(DIALECT_TOKEN));
96        assert!(prompt.contains("<tools>"));
97        assert!(prompt.contains("\"name\":\"echo\""));
98        // The XML format guide is injected.
99        assert!(prompt.contains("<invoke"));
100    }
101}