diff --git a/src/commonMain/kotlin/Money.kt b/src/commonMain/kotlin/Money.kt new file mode 100644 index 0000000..02fe923 --- /dev/null +++ b/src/commonMain/kotlin/Money.kt @@ -0,0 +1,82 @@ +/* + * Copyright 2024 Kazimierz Pogoda / Xemantic + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.xemantic.ai.money + +import com.xemantic.ai.money.serialization.MoneyRatioSerializer +import com.xemantic.ai.money.serialization.MoneySerializer +import com.xemantic.ai.tool.schema.meta.Description +import com.xemantic.ai.tool.schema.meta.Pattern +import kotlinx.serialization.Serializable + +/** + * Represents a monetary amount, including small fractional amounts, + * so it can be used for expressing amounts like per token usage cost of LLM APIs, + * and real-time summarization of these costs. + * + * Note: This interface is not intended as a full implementation of + * arbitrary precision arithmetics related to monetary amounts. + * + * The [Money] is representing a multiplatform convenience wrapper around decimal + * arithmetics of each platform, to avoid floating point arithmetics + * in calculating total costs. + * + * The [Pattern] annotation, when serialized as a part of the JSON schema representing + * LLM tool use (function calling) input, will give the LLM a hint how to encode + * the financial value as a decimal number, without any string formatting. + */ +@Description("Represents a monetary amount with arbitrary precision and no currency information") +@Pattern($$"""^-?\d*\.?\d+$""") +@Serializable(MoneySerializer::class) +public interface Money { + + public operator fun plus(money: Money): Money + + public operator fun minus(money: Money): Money + + public operator fun times(money: Money): Money + + public operator fun times(ratio: Ratio): Money + + public operator fun compareTo(money: Money): Int + + /** + * A ratio to multiply Money. I can be used for representing + * small ratios, like price per LLM token. + */ + @Description("Represents a ratio used to multiply Money") + @Pattern($$"""^-?\d*\.?\d+$""") + @Serializable(MoneyRatioSerializer::class) + public interface Ratio { + + public operator fun times(money: Money): Money + + public companion object + + } + +} + +public expect fun Money(amount: String): Money + +public expect val Money.Companion.ZERO: Money + +public expect val Money.Companion.ONE: Money + +public expect fun Money.Companion.Ratio(value: String): Money.Ratio + +public expect val Money.Ratio.Companion.ONE: Money.Ratio + diff --git a/src/commonMain/kotlin/serialization/MoneySerialization.kt b/src/commonMain/kotlin/serialization/MoneySerialization.kt new file mode 100644 index 0000000..f66664c --- /dev/null +++ b/src/commonMain/kotlin/serialization/MoneySerialization.kt @@ -0,0 +1,79 @@ +/* + * Copyright 2024 Kazimierz Pogoda / Xemantic + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.xemantic.ai.money.serialization + +import com.xemantic.ai.money.Money +import com.xemantic.ai.money.Ratio +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.InternalSerializationApi +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializer +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.buildSerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder + +public object MoneySerializer : KSerializer { + + // we can autogenerate a serializer which will retain annotations of serialized class + @OptIn(ExperimentalSerializationApi::class) + @Serializer(forClass = Money::class) + private object AutoSerializer + + @OptIn(InternalSerializationApi::class, ExperimentalSerializationApi::class) + public override val descriptor: SerialDescriptor = buildSerialDescriptor( + serialName = AutoSerializer.descriptor.serialName, + kind = PrimitiveKind.STRING + ) { + annotations = AutoSerializer.descriptor.annotations + } + + override fun serialize(encoder: Encoder, value: Money) { + encoder.encodeString(value.toString()) + } + + override fun deserialize( + decoder: Decoder + ): Money = Money(decoder.decodeString()) + +} + +public object MoneyRatioSerializer : KSerializer { + + // we can autogenerate a serializer which will retain annotations of serialized class + @OptIn(ExperimentalSerializationApi::class) + @Serializer(forClass = Money.Ratio::class) + private object AutoSerializer + + @OptIn(InternalSerializationApi::class, ExperimentalSerializationApi::class) + public override val descriptor: SerialDescriptor = buildSerialDescriptor( + serialName = AutoSerializer.descriptor.serialName, + kind = PrimitiveKind.STRING + ) { + annotations = AutoSerializer.descriptor.annotations + } + + override fun serialize(encoder: Encoder, value: Money.Ratio) { + encoder.encodeString(value.toString()) + } + + override fun deserialize( + decoder: Decoder + ): Money.Ratio = Money.Ratio(decoder.decodeString()) + +} diff --git a/src/commonTest/kotlin/serialization/MoneyRatioSerializationTest.kt b/src/commonTest/kotlin/serialization/MoneyRatioSerializationTest.kt new file mode 100644 index 0000000..9558170 --- /dev/null +++ b/src/commonTest/kotlin/serialization/MoneyRatioSerializationTest.kt @@ -0,0 +1,52 @@ +/* + * Copyright 2024 Kazimierz Pogoda / Xemantic + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.xemantic.ai.money.serialization + +import com.xemantic.ai.money.Money +import com.xemantic.ai.money.Ratio +import com.xemantic.ai.money.test.shouldBe +import com.xemantic.ai.money.test.testJson +import com.xemantic.ai.tool.schema.meta.Description +import com.xemantic.ai.tool.schema.meta.Pattern +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.encodeToString +import kotlin.test.Test + +class MoneyRatioSerializationTest { + + @Test + fun `Should serialize Money Ratio to JSON`() { + val ratio = Money.Ratio("0.000001") + testJson.encodeToString(ratio) shouldBe """"0.000001"""" + } + + @Test + fun `Should deserialize Money Ratio from JSON`() { + val json = """"0.000001"""" + val ratio = testJson.decodeFromString(json) + ratio shouldBe Money.Ratio("0.000001") + } + + @Test + fun `Should preserve tool schema annotations of Money Ratio`() { + @OptIn(ExperimentalSerializationApi::class) + val meta = Money.Ratio.serializer().descriptor.annotations + (meta[0] as Description).value shouldBe "Represents a ratio used to multiply Money" + (meta[1] as Pattern).regex shouldBe $$"""^-?\d*\.?\d+$""" + } + +}