Skip to content
This repository was archived by the owner on Nov 26, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 8 additions & 6 deletions examples/tool-call-example.ro
Original file line number Diff line number Diff line change
@@ -1,23 +1,25 @@
use weatherTool

function buildWeatherPrompt(city): String {
promptText = "What's the weather like in " + city + "?"
promptText = "What's the weather like in " + city + "? Use the weatherTool to get the current weather."
return promptText
}

city = "San Francisco"
promptText = buildWeatherPrompt(city)

print "Prompt: " + promptText

prompt promptText
tools: [
{
name: "weatherTool",
description: "Provides forecast data"
description: "Provides forecast data for a given city. Returns temperature, conditions, and forecast.",
parameters: {
city: "String"
}
}
]
model: "gpt-4"
temperature: 0.2
maxTokens: 200

call weatherTool(city)


8 changes: 8 additions & 0 deletions plugins/weather-tool/plugin.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
name = "weatherTool"
type = "subprocess"
description = "Weather tool that provides forecast data for a given city"
version = "1.0.0"
path = "weatherTool.py"
interpreter = "python3"
args = []

126 changes: 126 additions & 0 deletions plugins/weather-tool/weatherTool.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
#!/usr/bin/env python3

import json
import sys
from typing import Any, Dict, List, Union
import random

def parse_value(value: Any) -> Dict[str, Any]:
"""Convert Python value to Pronto Value format"""
if value is None:
return {"Null": None}
elif isinstance(value, str):
return {"String": value}
elif isinstance(value, (int, float)):
return {"Number": float(value)}
elif isinstance(value, bool):
return {"Boolean": value}
elif isinstance(value, list):
return {"Array": [parse_value(v) for v in value]}
elif isinstance(value, dict):
return {"Object": {k: parse_value(v) for k, v in value.items()}}
else:
return {"Null": None}

def extract_value(value: Dict[str, Any]) -> Any:
"""Extract Python value from Pronto Value format"""
if "String" in value:
return value["String"]
elif "Number" in value:
return value["Number"]
elif "Boolean" in value:
return value["Boolean"]
elif "Null" in value:
return None
elif "Array" in value:
return [extract_value(v) for v in value["Array"]]
elif "Object" in value:
return {k: extract_value(v) for k, v in value["Object"].items()}
else:
return None

def get_weather(city: str) -> Dict[str, Any]:
"""Get weather information for a given city"""
conditions = ["Sunny", "Cloudy", "Partly Cloudy", "Rainy", "Clear"]
temperatures = {
"San Francisco": (15, 20),
"New York": (10, 18),
"London": (8, 15),
"Tokyo": (12, 22),
"Paris": (10, 16),
}

temp_range = temperatures.get(city, (10, 25))
temp = round(random.uniform(temp_range[0], temp_range[1]), 1)
condition = random.choice(conditions)

return {
"city": city,
"temperature": temp,
"condition": condition,
"unit": "Celsius",
"forecast": f"{condition} with a temperature of {temp}°C"
}

def main():
try:
input_data = sys.stdin.read()
request = json.loads(input_data)

if request.get("tool") != "weatherTool":
response = {
"success": False,
"error": f"Unknown tool: {request.get('tool')}",
}
print(json.dumps(response))
sys.exit(1)

args = request.get("arguments", [])

city = None

if len(args) == 0:
response = {
"success": False,
"error": "weatherTool requires a city argument",
}
print(json.dumps(response))
sys.exit(1)

if len(args) == 1 and isinstance(args[0], dict) and "Object" in args[0]:
obj = extract_value(args[0])
if isinstance(obj, dict) and "city" in obj:
city = obj["city"]
elif isinstance(obj, dict) and len(obj) == 1:
city = list(obj.values())[0]
elif len(args) >= 1:
city = extract_value(args[0])

if not city or not isinstance(city, str):
response = {
"success": False,
"error": "weatherTool requires a city argument (String)",
}
print(json.dumps(response))
sys.exit(1)

weather_data = get_weather(city)

response = {
"success": True,
"result": parse_value(weather_data),
}

print(json.dumps(response))

except Exception as e:
response = {
"success": False,
"error": f"Plugin error: {str(e)}",
}
print(json.dumps(response))
sys.exit(1)

if __name__ == "__main__":
main()

51 changes: 48 additions & 3 deletions rohas-core/src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -464,10 +464,28 @@ impl Parser {
}
})
.ok_or(ParseError::InvalidExpression)?;
let parameters = properties
.get("parameters")
.and_then(|e| {
if let Expression::ObjectLiteral { properties: param_props } = e {
let mut params = HashMap::new();
for (param_name, param_value) in param_props {
if let Expression::Literal(Literal::String(type_str)) = param_value {
if let Ok(param_type) = Self::parse_type_from_string(&type_str) {
params.insert(param_name.clone(), param_type);
}
}
}
Some(params)
} else {
None
}
})
.unwrap_or_else(HashMap::new);
tool_defs.push(ToolDefinition {
name,
description,
parameters: HashMap::new(),
parameters,
});
} else {
return Err(ParseError::InvalidExpression);
Expand Down Expand Up @@ -603,7 +621,6 @@ impl Parser {
"maxTokens" => *max_tokens = Some(value),
"stream" => *stream = Some(value),
"tools" => {

if let Expression::ArrayLiteral { elements } = value {
let mut tool_defs = Vec::new();
for elem in elements {
Expand All @@ -628,10 +645,28 @@ impl Parser {
}
})
.ok_or(ParseError::InvalidExpression)?;
let parameters = properties
.get("parameters")
.and_then(|e| {
if let Expression::ObjectLiteral { properties: param_props } = e {
let mut params = HashMap::new();
for (param_name, param_value) in param_props {
if let Expression::Literal(Literal::String(type_str)) = param_value {
if let Ok(param_type) = Self::parse_type_from_string(&type_str) {
params.insert(param_name.clone(), param_type);
}
}
}
Some(params)
} else {
None
}
})
.unwrap_or_else(HashMap::new);
tool_defs.push(ToolDefinition {
name,
description,
parameters: HashMap::new(),
parameters,
});
} else {
return Err(ParseError::InvalidExpression);
Expand Down Expand Up @@ -1222,6 +1257,16 @@ impl Parser {
}
}

fn parse_type_from_string(type_str: &str) -> Result<Type, ParseError> {
match type_str {
"String" | "string" => Ok(Type::String),
"Number" | "number" => Ok(Type::Number),
"Boolean" | "boolean" => Ok(Type::Boolean),
"Any" => Ok(Type::Any),
_ => Ok(Type::Named(type_str.to_string())),
}
}

fn peek(&self) -> Option<&Token> {
self.tokens.get(self.current).map(|t| &t.token)
}
Expand Down
36 changes: 35 additions & 1 deletion rohas-llm/src/providers/openai.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,41 @@ impl Provider for OpenAIProvider {
}

if let Some(tools) = request.tools {
body["tools"] = json!(tools);
let openai_tools: Vec<serde_json::Value> = tools
.into_iter()
.map(|tool| {
let mut properties = serde_json::Map::new();
let mut required = Vec::new();
for (param_name, param) in tool.parameters {
let mut prop = serde_json::Map::new();
prop.insert("type".to_string(), json!(param.param_type));
if let Some(desc) = param.description {
prop.insert("description".to_string(), json!(desc));
}
properties.insert(param_name.clone(), json!(prop));
if param.required.unwrap_or(false) {
required.push(param_name);
}
}

let parameters = json!({
"type": "object",
"properties": properties,
"required": required
});

json!({
"type": "function",
"function": {
"name": tool.name,
"description": tool.description,
"parameters": parameters
}
})
})
.collect();

body["tools"] = json!(openai_tools);
body["tool_choice"] = json!("auto");
}

Expand Down
Loading
Loading