From bd99a8f19a8d2a1d1bfd64f6c52f7d6abee14236 Mon Sep 17 00:00:00 2001 From: sameer08055 Date: Mon, 8 Jun 2026 23:53:03 -0400 Subject: [PATCH] feat: add utilities/unit_converter skill --- .../utilities/unit_converter/instructions.md | 35 ++++++ skills/utilities/unit_converter/manifest.yaml | 34 +++++ skills/utilities/unit_converter/skill.py | 116 ++++++++++++++++++ skills/utilities/unit_converter/test_skill.py | 69 +++++++++++ 4 files changed, 254 insertions(+) create mode 100644 skills/utilities/unit_converter/instructions.md create mode 100644 skills/utilities/unit_converter/manifest.yaml create mode 100644 skills/utilities/unit_converter/skill.py create mode 100644 skills/utilities/unit_converter/test_skill.py diff --git a/skills/utilities/unit_converter/instructions.md b/skills/utilities/unit_converter/instructions.md new file mode 100644 index 0000000..e276cf7 --- /dev/null +++ b/skills/utilities/unit_converter/instructions.md @@ -0,0 +1,35 @@ +# Unit Converter Skill + +You have access to a unit conversion tool. Use it whenever the user asks to convert a value from one unit to another. + +## When to use this skill + +- The user mentions a numeric value alongside a unit (e.g. "100km", "70kg", "32°F") +- The user asks "how much is X in Y" +- The user needs a unit conversion as part of a larger calculation + +## Supported categories + +- **Length**: km, m, cm, mm, miles, yards, feet, inches +- **Weight**: kg, g, mg, lbs, oz +- **Temperature**: celsius, fahrenheit, kelvin +- **Speed**: kph, mph, mps, knots + +## How to call the tool + +Always pass three parameters: +- `value` — the numeric amount (number, not a string) +- `from_unit` — the unit to convert from, lowercase (e.g. "celsius", "km", "lbs") +- `to_unit` — the unit to convert to, lowercase + +## Handling errors + +If the tool returns an `error` key, explain the issue clearly to the user. Common causes: +- Unrecognised unit name — suggest the closest supported unit +- Mismatched categories — explain that e.g. weight cannot be converted to length +- Missing value — ask the user to provide a numeric amount + +## Response style + +Always confirm the original value and unit alongside the result. For example: +"100 km is equal to 62.14 miles." \ No newline at end of file diff --git a/skills/utilities/unit_converter/manifest.yaml b/skills/utilities/unit_converter/manifest.yaml new file mode 100644 index 0000000..1fac935 --- /dev/null +++ b/skills/utilities/unit_converter/manifest.yaml @@ -0,0 +1,34 @@ +name: "utilities/unit_converter" +version: "0.1.0" +description: "Converts values between common units of measurement including length, weight, temperature, and speed." +short_description: "Converts values between common units of measurement." +issuer: + name: Sameer + email: sam90509@gmail.comcom + github: sameer08055 + org: "" +category: "utilities" +parameters: + type: object + properties: + value: + type: "number" + description: "The numeric value to convert." + from_unit: + type: "string" + description: "The unit to convert from (e.g. 'km', 'kg', 'celsius', 'mph')." + to_unit: + type: "string" + description: "The unit to convert to (e.g. 'miles', 'lbs', 'fahrenheit', 'kph')." + required: + - value + - from_unit + - to_unit +requirements: [] +constitution: | + 1. ACCURACY: Use standard conversion factors only. Never approximate loosely. + 2. TRANSPARENCY: Always return the original value and units alongside the result. + 3. SAFETY: Return a clear error if a unit is unrecognised or a conversion is physically impossible. +presentation: + icon: "ruler" + color: "#2ecc71" \ No newline at end of file diff --git a/skills/utilities/unit_converter/skill.py b/skills/utilities/unit_converter/skill.py new file mode 100644 index 0000000..fb3737d --- /dev/null +++ b/skills/utilities/unit_converter/skill.py @@ -0,0 +1,116 @@ +from typing import Any, Dict +from skillware.core.base_skill import BaseSkill + +class UnitConverterSkill(BaseSkill): + """ + Converts values between common units of measurement: + length, weight, temperature, and speed. + """ + + @property + def manifest(self) -> Dict[str, Any]: + return { + "name": "utilities/unit_converter", + "version": "0.1.0", + } + + CONVERSIONS = { + # Length — base unit: meters + "km": ("length", 1000), + "m": ("length", 1), + "cm": ("length", 0.01), + "mm": ("length", 0.001), + "miles": ("length", 1609.34), + "mile": ("length", 1609.34), + "yards": ("length", 0.9144), + "feet": ("length", 0.3048), + "inches": ("length", 0.0254), + + # Weight — base unit: kilograms + "kg": ("weight", 1), + "g": ("weight", 0.001), + "mg": ("weight", 0.000001), + "lbs": ("weight", 0.453592), + "lb": ("weight", 0.453592), + "ounces": ("weight", 0.0283495), + "oz": ("weight", 0.0283495), + + # Speed — base unit: meters per second + "mps": ("speed", 1), + "kph": ("speed", 0.277778), + "mph": ("speed", 0.44704), + "knots": ("speed", 0.514444), + } + + def _convert_temperature(self, value: float, from_unit: str, to_unit: str): + """Temperature needs its own logic since it's not a simple multiply.""" + f = from_unit.lower() + t = to_unit.lower() + + # Convert to Celsius first + if f == "celsius": + celsius = value + elif f == "fahrenheit": + celsius = (value - 32) * 5 / 9 + elif f == "kelvin": + celsius = value - 273.15 + else: + return None + + # Convert from Celsius to target + if t == "celsius": + return celsius + elif t == "fahrenheit": + return (celsius * 9 / 5) + 32 + elif t == "kelvin": + return celsius + 273.15 + else: + return None + + def execute(self, params: Dict[str, Any]) -> Any: + value = params.get("value") + from_unit = str(params.get("from_unit", "")).lower().strip() + to_unit = str(params.get("to_unit", "")).lower().strip() + + if value is None: + return {"error": "value is required."} + if not from_unit or not to_unit: + return {"error": "from_unit and to_unit are required."} + + # Handle temperature separately + temp_units = {"celsius", "fahrenheit", "kelvin"} + if from_unit in temp_units or to_unit in temp_units: + if from_unit not in temp_units or to_unit not in temp_units: + return {"error": f"Cannot convert between temperature and non-temperature units."} + result = self._convert_temperature(value, from_unit, to_unit) + if result is None: + return {"error": f"Unrecognised temperature unit."} + return { + "original_value": value, + "from_unit": from_unit, + "to_unit": to_unit, + "converted_value": round(result, 6), + } + + # Handle all other unit types + if from_unit not in self.CONVERSIONS: + return {"error": f"Unrecognised unit: '{from_unit}'."} + if to_unit not in self.CONVERSIONS: + return {"error": f"Unrecognised unit: '{to_unit}'."} + + from_category, from_factor = self.CONVERSIONS[from_unit] + to_category, to_factor = self.CONVERSIONS[to_unit] + + if from_category != to_category: + return {"error": f"Cannot convert '{from_unit}' ({from_category}) to '{to_unit}' ({to_category})."} + + # Convert: source → base unit → target unit + base_value = value * from_factor + result = base_value / to_factor + + return { + "original_value": value, + "from_unit": from_unit, + "to_unit": to_unit, + "converted_value": round(result, 6), + } \ No newline at end of file diff --git a/skills/utilities/unit_converter/test_skill.py b/skills/utilities/unit_converter/test_skill.py new file mode 100644 index 0000000..748199f --- /dev/null +++ b/skills/utilities/unit_converter/test_skill.py @@ -0,0 +1,69 @@ +import pytest +from skills.utilities.unit_converter.skill import UnitConverterSkill + +@pytest.fixture +def skill(): + return UnitConverterSkill() + + +# --- Length --- + +def test_km_to_miles(skill): + result = skill.execute({"value": 100, "from_unit": "km", "to_unit": "miles"}) + assert abs(result["converted_value"] - 62.1371) < 0.01 + +def test_meters_to_feet(skill): + result = skill.execute({"value": 1, "from_unit": "m", "to_unit": "feet"}) + assert abs(result["converted_value"] - 3.28084) < 0.01 + + +# --- Weight --- + +def test_kg_to_lbs(skill): + result = skill.execute({"value": 70, "from_unit": "kg", "to_unit": "lbs"}) + assert abs(result["converted_value"] - 154.32) < 0.01 + +def test_oz_to_grams(skill): + result = skill.execute({"value": 16, "from_unit": "oz", "to_unit": "g"}) + assert abs(result["converted_value"] - 453.59) < 0.01 + + +# --- Temperature --- + +def test_celsius_to_fahrenheit(skill): + result = skill.execute({"value": 100, "from_unit": "celsius", "to_unit": "fahrenheit"}) + assert result["converted_value"] == 212.0 + +def test_freezing_point(skill): + result = skill.execute({"value": 0, "from_unit": "celsius", "to_unit": "fahrenheit"}) + assert result["converted_value"] == 32.0 + +def test_celsius_to_kelvin(skill): + result = skill.execute({"value": 0, "from_unit": "celsius", "to_unit": "kelvin"}) + assert result["converted_value"] == 273.15 + + +# --- Speed --- + +def test_mph_to_kph(skill): + result = skill.execute({"value": 60, "from_unit": "mph", "to_unit": "kph"}) + assert abs(result["converted_value"] - 96.56) < 0.1 + + +# --- Error handling --- + +def test_unknown_unit(skill): + result = skill.execute({"value": 10, "from_unit": "furlongs", "to_unit": "km"}) + assert "error" in result + +def test_mismatched_categories(skill): + result = skill.execute({"value": 10, "from_unit": "kg", "to_unit": "km"}) + assert "error" in result + +def test_temperature_mixed_with_length(skill): + result = skill.execute({"value": 100, "from_unit": "celsius", "to_unit": "km"}) + assert "error" in result + +def test_missing_value(skill): + result = skill.execute({"from_unit": "km", "to_unit": "miles"}) + assert "error" in result \ No newline at end of file