liteai-sdk 0.3.21__py3-none-any.whl → 0.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
liteai_sdk/__init__.py CHANGED
@@ -3,7 +3,7 @@ import queue
3
3
  from typing import cast
4
4
  from collections.abc import AsyncGenerator, Generator
5
5
  from litellm import CustomStreamWrapper, completion, acompletion
6
- from litellm.exceptions import (
6
+ from litellm.exceptions import (
7
7
  AuthenticationError,
8
8
  PermissionDeniedError,
9
9
  RateLimitError,
@@ -19,18 +19,18 @@ from litellm.exceptions import (
19
19
  from litellm.utils import get_valid_models
20
20
  from litellm.types.utils import LlmProviders,\
21
21
  ModelResponse as LiteLlmModelResponse,\
22
- ModelResponseStream as LiteLlmModelResponseStream,\
23
- Choices as LiteLlmModelResponseChoices
22
+ ModelResponseStream as LiteLlmModelResponseStream
24
23
  from .debug import enable_debugging
25
24
  from .param_parser import ParamParser
26
25
  from .stream import AssistantMessageCollector
27
- from .tool import ToolFn, ToolDef, RawToolDef, ToolLike
28
26
  from .tool.execute import execute_tool_sync, execute_tool
27
+ from .tool.toolset import tool, Toolset
29
28
  from .tool.utils import find_tool_by_name
30
29
  from .types import LlmRequestParams, GenerateTextResponse, StreamTextResponseSync, StreamTextResponseAsync
30
+ from .types.tool import ToolFn, ToolDef, RawToolDef, ToolLike
31
31
  from .types.exceptions import *
32
32
  from .types.message import ChatMessage, UserMessage, SystemMessage, AssistantMessage, ToolMessage,\
33
- MessageChunk, TextChunk, ReasoningChunk, AudioChunk, ImageChunk, ToolCallChunk,\
33
+ MessageChunk, TextChunk, UsageChunk, ReasoningChunk, AudioChunk, ImageChunk, ToolCallChunk,\
34
34
  ToolCallTuple, openai_chunk_normalizer
35
35
  from .logger import logger, enable_logging
36
36
 
@@ -87,7 +87,9 @@ class LLM:
87
87
  ) -> list[ToolMessage]:
88
88
  results = []
89
89
  for tool_call_tuple in tool_call_tuples:
90
- id, function_name, function_arguments = tool_call_tuple
90
+ function_name = tool_call_tuple.function_name
91
+ function_arguments = tool_call_tuple.function_arguments
92
+
91
93
  if (target_tool := find_tool_by_name(tools, function_name)) is None:
92
94
  logger.warning(f"Tool \"{function_name}\" not found, skipping execution.")
93
95
  continue
@@ -101,7 +103,7 @@ class LLM:
101
103
  except Exception as e:
102
104
  error = f"{type(e).__name__}: {str(e)}"
103
105
  results.append(ToolMessage(
104
- id=id,
106
+ id=tool_call_tuple.id,
105
107
  name=function_name,
106
108
  arguments=function_arguments,
107
109
  result=result,
@@ -115,7 +117,9 @@ class LLM:
115
117
  ) -> list[ToolMessage]:
116
118
  results = []
117
119
  for tool_call_tuple in tool_call_tuples:
118
- id, function_name, function_arguments = tool_call_tuple
120
+ function_name = tool_call_tuple.function_name
121
+ function_arguments = tool_call_tuple.function_arguments
122
+
119
123
  if (target_tool := find_tool_by_name(tools, function_name)) is None:
120
124
  logger.warning(f"Tool \"{function_name}\" not found, skipping execution.")
121
125
  continue
@@ -129,7 +133,7 @@ class LLM:
129
133
  except Exception as e:
130
134
  error = f"{type(e).__name__}: {str(e)}"
131
135
  results.append(ToolMessage(
132
- id=id,
136
+ id=tool_call_tuple.id,
133
137
  name=function_name,
134
138
  arguments=function_arguments,
135
139
  result=result,
@@ -146,10 +150,8 @@ class LLM:
146
150
  def generate_text_sync(self, params: LlmRequestParams) -> GenerateTextResponse:
147
151
  response = completion(**self._param_parser.parse_nonstream(params))
148
152
  response = cast(LiteLlmModelResponse, response)
149
- choices = cast(list[LiteLlmModelResponseChoices], response.choices)
150
- message = choices[0].message
151
153
  assistant_message = AssistantMessage\
152
- .from_litellm_message(message)\
154
+ .from_litellm_message(response)\
153
155
  .with_request_params(params)
154
156
  result: GenerateTextResponse = [assistant_message]
155
157
  if (tools_and_tool_calls := self._should_resolve_tool_calls(params, assistant_message)):
@@ -160,10 +162,8 @@ class LLM:
160
162
  async def generate_text(self, params: LlmRequestParams) -> GenerateTextResponse:
161
163
  response = await acompletion(**self._param_parser.parse_nonstream(params))
162
164
  response = cast(LiteLlmModelResponse, response)
163
- choices = cast(list[LiteLlmModelResponseChoices], response.choices)
164
- message = choices[0].message
165
165
  assistant_message = AssistantMessage\
166
- .from_litellm_message(message)\
166
+ .from_litellm_message(response)\
167
167
  .with_request_params(params)
168
168
  result: GenerateTextResponse = [assistant_message]
169
169
  if (tools_and_tool_calls := self._should_resolve_tool_calls(params, assistant_message)):
@@ -195,7 +195,7 @@ class LLM:
195
195
  return returned_stream, full_message_queue
196
196
 
197
197
  async def stream_text(self, params: LlmRequestParams) -> StreamTextResponseAsync:
198
- async def stream(response: CustomStreamWrapper) -> AsyncGenerator[TextChunk | ReasoningChunk | AudioChunk | ImageChunk | ToolCallChunk]:
198
+ async def stream(response: CustomStreamWrapper) -> AsyncGenerator[MessageChunk]:
199
199
  nonlocal message_collector
200
200
  async for chunk in response:
201
201
  chunk = cast(LiteLlmModelResponseStream, chunk)
@@ -233,8 +233,12 @@ __all__ = [
233
233
  "Timeout",
234
234
 
235
235
  "LLM",
236
+ "LlmProviders",
236
237
  "LlmRequestParams",
237
238
 
239
+ "tool",
240
+ "Toolset",
241
+
238
242
  "ToolFn",
239
243
  "ToolDef",
240
244
  "RawToolDef",
@@ -250,6 +254,7 @@ __all__ = [
250
254
 
251
255
  "MessageChunk",
252
256
  "TextChunk",
257
+ "UsageChunk",
253
258
  "ReasoningChunk",
254
259
  "AudioChunk",
255
260
  "ImageChunk",
@@ -1,7 +1,8 @@
1
1
  from typing import Any
2
2
  from litellm.types.utils import LlmProviders
3
- from .tool import prepare_tools
3
+ from .tool.prepare import prepare_tools
4
4
  from .types import LlmRequestParams, ToolMessage
5
+ from .types.tool import ToolLike
5
6
 
6
7
  ParsedParams = dict[str, Any]
7
8
 
@@ -14,8 +15,22 @@ class ParamParser:
14
15
  self._base_url = base_url
15
16
  self._api_key = api_key
16
17
 
18
+ @staticmethod
19
+ def _extract_tool_params(params: LlmRequestParams) -> list[ToolLike] | None:
20
+ if params.tools is None and params.toolsets is None:
21
+ return None
22
+ tools = []
23
+ if params.tools:
24
+ tools = params.tools
25
+ if params.toolsets:
26
+ for toolset in params.toolsets:
27
+ tools.extend(toolset.get_tool_methods())
28
+ return tools
29
+
17
30
  def _parse(self, params: LlmRequestParams) -> ParsedParams:
18
- tools = params.tools and prepare_tools(params.tools)
31
+ extracted_tool_likes = self._extract_tool_params(params)
32
+ tools = extracted_tool_likes and prepare_tools(extracted_tool_likes)
33
+
19
34
  transformed_messages = []
20
35
  for message in params.messages:
21
36
  if type(message) is ToolMessage and\
@@ -45,4 +60,5 @@ class ParamParser:
45
60
  def parse_stream(self, params: LlmRequestParams) -> ParsedParams:
46
61
  parsed = self._parse(params)
47
62
  parsed["stream"] = True
63
+ parsed["stream_options"] = {"include_usage": True}
48
64
  return parsed
@@ -1,310 +0,0 @@
1
- """
2
- source: https://github.com/mozilla-ai/any-llm/blob/main/src/any_llm/tools.py
3
- """
4
-
5
- import dataclasses
6
- import enum
7
- import inspect
8
- import types as _types
9
- from collections.abc import Callable, Mapping, Sequence
10
- from datetime import date, datetime, time
11
- from typing import Annotated as _Annotated, Literal as _Literal, is_typeddict as _is_typeddict,\
12
- Any, Awaitable, get_args, get_origin, get_type_hints
13
- from pydantic import BaseModel as PydanticBaseModel
14
-
15
- ToolFn = Callable[..., Any] | Callable[..., Awaitable[Any]]
16
-
17
- """
18
- RawToolDef example:
19
- {
20
- "name": "get_current_weather",
21
- "description": "Get the current weather in a given location",
22
- "parameters": {
23
- "type": "object",
24
- "properties": {
25
- "location": {
26
- "type": "string",
27
- "description": "The city and state, e.g. San Francisco, CA",
28
- },
29
- "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]},
30
- },
31
- "required": ["location"],
32
- }
33
- }
34
- """
35
- RawToolDef = dict[str, Any]
36
-
37
- @dataclasses.dataclass
38
- class ToolDef:
39
- name: str
40
- description: str
41
- execute: ToolFn
42
-
43
- ToolLike = ToolDef | RawToolDef | ToolFn
44
-
45
- def _python_type_to_json_schema(python_type: Any) -> dict[str, Any]:
46
- """Convert Python type annotation to a JSON Schema for a parameter.
47
-
48
- Supported mappings (subset tailored for LLM tool schemas):
49
- - Primitives: str/int/float/bool -> string/integer/number/boolean
50
- - bytes -> string with contentEncoding base64
51
- - datetime/date/time -> string with format date-time/date/time
52
- - list[T] / Sequence[T] / set[T] / frozenset[T] -> array with items=schema(T)
53
- - set/frozenset include uniqueItems=true
54
- - list without type args defaults items to string
55
- - dict[K,V] / Mapping[K,V] -> object with additionalProperties=schema(V)
56
- - dict without type args defaults additionalProperties to string
57
- - tuple[T1, T2, ...] -> array with prefixItems per element and min/maxItems
58
- - tuple[T, ...] -> array with items=schema(T)
59
- - Union[X, Y] and X | Y -> oneOf=[schema(X), schema(Y)] (without top-level type)
60
- - Optional[T] (Union[T, None]) -> schema(T) (nullability not encoded)
61
- - Literal[...]/Enum -> enum with appropriate type inference when uniform
62
- - TypedDict -> object with properties/required per annotations
63
- - dataclass/Pydantic BaseModel -> object with nested properties inferred from fields
64
- """
65
- origin = get_origin(python_type)
66
- args = get_args(python_type)
67
-
68
- if _Annotated is not None and origin is _Annotated and len(args) >= 1:
69
- python_type = args[0]
70
- origin = get_origin(python_type)
71
- args = get_args(python_type)
72
-
73
- if python_type is Any:
74
- return {"type": "string"}
75
-
76
- primitive_map = {str: "string", int: "integer", float: "number", bool: "boolean"}
77
- if python_type in primitive_map:
78
- return {"type": primitive_map[python_type]}
79
-
80
- if python_type is bytes:
81
- return {"type": "string", "contentEncoding": "base64"}
82
- if python_type is datetime:
83
- return {"type": "string", "format": "date-time"}
84
- if python_type is date:
85
- return {"type": "string", "format": "date"}
86
- if python_type is time:
87
- return {"type": "string", "format": "time"}
88
-
89
- if python_type is list:
90
- return {"type": "array", "items": {"type": "string"}}
91
- if python_type is dict:
92
- return {"type": "object", "additionalProperties": {"type": "string"}}
93
-
94
- if origin is _Literal:
95
- literal_values = list(args)
96
- schema_lit: dict[str, Any] = {"enum": literal_values}
97
- if all(isinstance(v, bool) for v in literal_values):
98
- schema_lit["type"] = "boolean"
99
- elif all(isinstance(v, str) for v in literal_values):
100
- schema_lit["type"] = "string"
101
- elif all(isinstance(v, int) and not isinstance(v, bool) for v in literal_values):
102
- schema_lit["type"] = "integer"
103
- elif all(isinstance(v, int | float) and not isinstance(v, bool) for v in literal_values):
104
- schema_lit["type"] = "number"
105
- return schema_lit
106
-
107
- if inspect.isclass(python_type) and issubclass(python_type, enum.Enum):
108
- enum_values = [e.value for e in python_type]
109
- value_types = {type(v) for v in enum_values}
110
- schema: dict[str, Any] = {"enum": enum_values}
111
- if value_types == {str}:
112
- schema["type"] = "string"
113
- elif value_types == {int}:
114
- schema["type"] = "integer"
115
- elif value_types <= {int, float}:
116
- schema["type"] = "number"
117
- elif value_types == {bool}:
118
- schema["type"] = "boolean"
119
- return schema
120
-
121
- if _is_typeddict(python_type):
122
- annotations: dict[str, Any] = getattr(python_type, "__annotations__", {}) or {}
123
- required_keys = set(getattr(python_type, "__required_keys__", set()))
124
- td_properties: dict[str, Any] = {}
125
- td_required: list[str] = []
126
- for field_name, field_type in annotations.items():
127
- td_properties[field_name] = _python_type_to_json_schema(field_type)
128
- if field_name in required_keys:
129
- td_required.append(field_name)
130
- schema_td: dict[str, Any] = {
131
- "type": "object",
132
- "properties": td_properties,
133
- }
134
- if td_required:
135
- schema_td["required"] = td_required
136
- return schema_td
137
-
138
- if inspect.isclass(python_type) and dataclasses.is_dataclass(python_type):
139
- type_hints = get_type_hints(python_type)
140
- dc_properties: dict[str, Any] = {}
141
- dc_required: list[str] = []
142
- for field in dataclasses.fields(python_type):
143
- field_type = type_hints.get(field.name, Any)
144
- dc_properties[field.name] = _python_type_to_json_schema(field_type)
145
- if (
146
- field.default is dataclasses.MISSING
147
- and getattr(field, "default_factory", dataclasses.MISSING) is dataclasses.MISSING
148
- ):
149
- dc_required.append(field.name)
150
- schema_dc: dict[str, Any] = {"type": "object", "properties": dc_properties}
151
- if dc_required:
152
- schema_dc["required"] = dc_required
153
- return schema_dc
154
-
155
- if inspect.isclass(python_type) and issubclass(python_type, PydanticBaseModel):
156
- model_type_hints = get_type_hints(python_type)
157
- pd_properties: dict[str, Any] = {}
158
- pd_required: list[str] = []
159
- model_fields = getattr(python_type, "model_fields", {})
160
- for name, field_info in model_fields.items():
161
- pd_properties[name] = _python_type_to_json_schema(model_type_hints.get(name, Any))
162
- is_required = getattr(field_info, "is_required", None)
163
- if callable(is_required) and is_required():
164
- pd_required.append(name)
165
- schema_pd: dict[str, Any] = {"type": "object", "properties": pd_properties}
166
- if pd_required:
167
- schema_pd["required"] = pd_required
168
- return schema_pd
169
-
170
- if origin in (list, Sequence, set, frozenset):
171
- item_type = args[0] if args else Any
172
- item_schema = _python_type_to_json_schema(item_type)
173
- schema_arr: dict[str, Any] = {"type": "array", "items": item_schema or {"type": "string"}}
174
- if origin in (set, frozenset):
175
- schema_arr["uniqueItems"] = True
176
- return schema_arr
177
- if origin is tuple:
178
- if not args:
179
- return {"type": "array", "items": {"type": "string"}}
180
- if len(args) == 2 and args[1] is Ellipsis:
181
- return {"type": "array", "items": _python_type_to_json_schema(args[0])}
182
- prefix_items = [_python_type_to_json_schema(a) for a in args]
183
- return {
184
- "type": "array",
185
- "prefixItems": prefix_items,
186
- "minItems": len(prefix_items),
187
- "maxItems": len(prefix_items),
188
- }
189
-
190
- if origin in (dict, Mapping):
191
- value_type = args[1] if len(args) >= 2 else Any
192
- value_schema = _python_type_to_json_schema(value_type)
193
- return {"type": "object", "additionalProperties": value_schema or {"type": "string"}}
194
-
195
- typing_union = getattr(__import__("typing"), "Union", None)
196
- if origin in (typing_union, _types.UnionType):
197
- non_none_args = [a for a in args if a is not type(None)]
198
- if len(non_none_args) > 1:
199
- schemas = [_python_type_to_json_schema(arg) for arg in non_none_args]
200
- return {"oneOf": schemas}
201
- if non_none_args:
202
- return _python_type_to_json_schema(non_none_args[0])
203
- return {"type": "string"}
204
-
205
- return {"type": "string"}
206
-
207
- def _parse_callable_properties(func: ToolFn) -> tuple[dict[str, dict[str, Any]], list[str]]:
208
- sig = inspect.signature(func)
209
- type_hints = get_type_hints(func)
210
-
211
- properties: dict[str, dict[str, Any]] = {}
212
- required: list[str] = []
213
-
214
- for param_name, param in sig.parameters.items():
215
- if param.kind in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD):
216
- continue
217
-
218
- annotated_type = type_hints.get(param_name, str)
219
- param_schema = _python_type_to_json_schema(annotated_type)
220
-
221
- type_name = getattr(annotated_type, "__name__", str(annotated_type))
222
- properties[param_name] = {
223
- **param_schema,
224
- "description": f"Parameter {param_name} of type {type_name}",
225
- }
226
-
227
- if param.default == inspect.Parameter.empty:
228
- required.append(param_name)
229
-
230
- return properties, required
231
-
232
- def generate_tool_definition_from_callable(func: ToolFn) -> dict[str, Any]:
233
- """Convert a Python callable to OpenAI tools format.
234
-
235
- Args:
236
- func: A Python callable (function) to convert to a tool
237
-
238
- Returns:
239
- Dictionary in OpenAI tools format
240
-
241
- Raises:
242
- ValueError: If the function doesn't have proper docstring or type annotations
243
-
244
- Example:
245
- >>> def get_weather(location: str, unit: str = "celsius") -> str:
246
- ... '''Get weather information for a location.'''
247
- ... return f"Weather in {location} is sunny, 25°{unit[0].upper()}"
248
- >>>
249
- >>> tool = generate_tool_definition_from_callable(get_weather)
250
- >>> # Returns OpenAI tools format dict
251
-
252
- """
253
- if not func.__doc__:
254
- msg = f"Function {func.__name__} must have a docstring"
255
- raise ValueError(msg)
256
-
257
- properties, required = _parse_callable_properties(func)
258
- return {
259
- "type": "function",
260
- "function": {
261
- "name": func.__name__,
262
- "description": func.__doc__.strip(),
263
- "parameters": {"type": "object", "properties": properties, "required": required},
264
- },
265
- }
266
-
267
- def generate_tool_definition_from_tool_def(tool_def: ToolDef) -> dict[str, Any]:
268
- """Convert a ToolDef to OpenAI tools format.
269
-
270
- Args:
271
- tool_def: A ToolDef to convert to a tool
272
-
273
- Returns:
274
- Dictionary in OpenAI tools format
275
-
276
- Example:
277
- >>> tool_def = ToolDef(
278
- ... name="get_weather",
279
- ... description="Get weather information for a location.",
280
- ... execute=SomeFunction(),
281
- ... )
282
- >>> tool = generate_tool_definition_from_tool_def(tool_def)
283
- >>> # Returns OpenAI tools format dict
284
- """
285
- properties, required = _parse_callable_properties(tool_def.execute)
286
- return {
287
- "type": "function",
288
- "function": {
289
- "name": tool_def.name,
290
- "description": tool_def.description,
291
- "parameters": {"type": "object", "properties": properties, "required": required},
292
- },
293
- }
294
-
295
- def generate_tool_definition_from_raw_tool_def(raw_tool_def: RawToolDef) -> dict[str, Any]:
296
- return {
297
- "type": "function",
298
- "function": raw_tool_def,
299
- }
300
-
301
- def prepare_tools(tools: list[ToolLike]) -> list[dict]:
302
- tool_defs = []
303
- for tool in tools:
304
- if callable(tool):
305
- tool_defs.append(generate_tool_definition_from_callable(tool))
306
- elif isinstance(tool, ToolDef):
307
- tool_defs.append(generate_tool_definition_from_tool_def(tool))
308
- else:
309
- tool_defs.append(generate_tool_definition_from_raw_tool_def(tool))
310
- return tool_defs
@@ -3,7 +3,7 @@ import json
3
3
  from functools import singledispatch
4
4
  from typing import Any, Awaitable, Callable, cast
5
5
  from types import FunctionType, MethodType, CoroutineType
6
- from . import ToolDef
6
+ from ..types.tool import ToolDef
7
7
 
8
8
  async def _coroutine_wrapper(awaitable: Awaitable[Any]) -> CoroutineType:
9
9
  return await awaitable
@@ -0,0 +1,281 @@
1
+ """
2
+ source: https://github.com/mozilla-ai/any-llm/blob/main/src/any_llm/tools.py
3
+ """
4
+
5
+ import dataclasses
6
+ import enum
7
+ import inspect
8
+ import types as _types
9
+ from collections.abc import Mapping, Sequence
10
+ from datetime import date, datetime, time
11
+ from typing import Annotated as _Annotated, Literal as _Literal, is_typeddict as _is_typeddict,\
12
+ Any, get_args, get_origin, get_type_hints
13
+ from pydantic import BaseModel as PydanticBaseModel
14
+ from ..types.tool import ToolFn, ToolDef, RawToolDef, ToolLike
15
+
16
+ def _python_type_to_json_schema(python_type: Any) -> dict[str, Any]:
17
+ """Convert Python type annotation to a JSON Schema for a parameter.
18
+
19
+ Supported mappings (subset tailored for LLM tool schemas):
20
+ - Primitives: str/int/float/bool -> string/integer/number/boolean
21
+ - bytes -> string with contentEncoding base64
22
+ - datetime/date/time -> string with format date-time/date/time
23
+ - list[T] / Sequence[T] / set[T] / frozenset[T] -> array with items=schema(T)
24
+ - set/frozenset include uniqueItems=true
25
+ - list without type args defaults items to string
26
+ - dict[K,V] / Mapping[K,V] -> object with additionalProperties=schema(V)
27
+ - dict without type args defaults additionalProperties to string
28
+ - tuple[T1, T2, ...] -> array with prefixItems per element and min/maxItems
29
+ - tuple[T, ...] -> array with items=schema(T)
30
+ - Union[X, Y] and X | Y -> oneOf=[schema(X), schema(Y)] (without top-level type)
31
+ - Optional[T] (Union[T, None]) -> schema(T) (nullability not encoded)
32
+ - Literal[...]/Enum -> enum with appropriate type inference when uniform
33
+ - TypedDict -> object with properties/required per annotations
34
+ - dataclass/Pydantic BaseModel -> object with nested properties inferred from fields
35
+ """
36
+ origin = get_origin(python_type)
37
+ args = get_args(python_type)
38
+
39
+ if _Annotated is not None and origin is _Annotated and len(args) >= 1:
40
+ python_type = args[0]
41
+ origin = get_origin(python_type)
42
+ args = get_args(python_type)
43
+
44
+ if python_type is Any:
45
+ return {"type": "string"}
46
+
47
+ primitive_map = {str: "string", int: "integer", float: "number", bool: "boolean"}
48
+ if python_type in primitive_map:
49
+ return {"type": primitive_map[python_type]}
50
+
51
+ if python_type is bytes:
52
+ return {"type": "string", "contentEncoding": "base64"}
53
+ if python_type is datetime:
54
+ return {"type": "string", "format": "date-time"}
55
+ if python_type is date:
56
+ return {"type": "string", "format": "date"}
57
+ if python_type is time:
58
+ return {"type": "string", "format": "time"}
59
+
60
+ if python_type is list:
61
+ return {"type": "array", "items": {"type": "string"}}
62
+ if python_type is dict:
63
+ return {"type": "object", "additionalProperties": {"type": "string"}}
64
+
65
+ if origin is _Literal:
66
+ literal_values = list(args)
67
+ schema_lit: dict[str, Any] = {"enum": literal_values}
68
+ if all(isinstance(v, bool) for v in literal_values):
69
+ schema_lit["type"] = "boolean"
70
+ elif all(isinstance(v, str) for v in literal_values):
71
+ schema_lit["type"] = "string"
72
+ elif all(isinstance(v, int) and not isinstance(v, bool) for v in literal_values):
73
+ schema_lit["type"] = "integer"
74
+ elif all(isinstance(v, int | float) and not isinstance(v, bool) for v in literal_values):
75
+ schema_lit["type"] = "number"
76
+ return schema_lit
77
+
78
+ if inspect.isclass(python_type) and issubclass(python_type, enum.Enum):
79
+ enum_values = [e.value for e in python_type]
80
+ value_types = {type(v) for v in enum_values}
81
+ schema: dict[str, Any] = {"enum": enum_values}
82
+ if value_types == {str}:
83
+ schema["type"] = "string"
84
+ elif value_types == {int}:
85
+ schema["type"] = "integer"
86
+ elif value_types <= {int, float}:
87
+ schema["type"] = "number"
88
+ elif value_types == {bool}:
89
+ schema["type"] = "boolean"
90
+ return schema
91
+
92
+ if _is_typeddict(python_type):
93
+ annotations: dict[str, Any] = getattr(python_type, "__annotations__", {}) or {}
94
+ required_keys = set(getattr(python_type, "__required_keys__", set()))
95
+ td_properties: dict[str, Any] = {}
96
+ td_required: list[str] = []
97
+ for field_name, field_type in annotations.items():
98
+ td_properties[field_name] = _python_type_to_json_schema(field_type)
99
+ if field_name in required_keys:
100
+ td_required.append(field_name)
101
+ schema_td: dict[str, Any] = {
102
+ "type": "object",
103
+ "properties": td_properties,
104
+ }
105
+ if td_required:
106
+ schema_td["required"] = td_required
107
+ return schema_td
108
+
109
+ if inspect.isclass(python_type) and dataclasses.is_dataclass(python_type):
110
+ type_hints = get_type_hints(python_type)
111
+ dc_properties: dict[str, Any] = {}
112
+ dc_required: list[str] = []
113
+ for field in dataclasses.fields(python_type):
114
+ field_type = type_hints.get(field.name, Any)
115
+ dc_properties[field.name] = _python_type_to_json_schema(field_type)
116
+ if (
117
+ field.default is dataclasses.MISSING
118
+ and getattr(field, "default_factory", dataclasses.MISSING) is dataclasses.MISSING
119
+ ):
120
+ dc_required.append(field.name)
121
+ schema_dc: dict[str, Any] = {"type": "object", "properties": dc_properties}
122
+ if dc_required:
123
+ schema_dc["required"] = dc_required
124
+ return schema_dc
125
+
126
+ if inspect.isclass(python_type) and issubclass(python_type, PydanticBaseModel):
127
+ model_type_hints = get_type_hints(python_type)
128
+ pd_properties: dict[str, Any] = {}
129
+ pd_required: list[str] = []
130
+ model_fields = getattr(python_type, "model_fields", {})
131
+ for name, field_info in model_fields.items():
132
+ pd_properties[name] = _python_type_to_json_schema(model_type_hints.get(name, Any))
133
+ is_required = getattr(field_info, "is_required", None)
134
+ if callable(is_required) and is_required():
135
+ pd_required.append(name)
136
+ schema_pd: dict[str, Any] = {"type": "object", "properties": pd_properties}
137
+ if pd_required:
138
+ schema_pd["required"] = pd_required
139
+ return schema_pd
140
+
141
+ if origin in (list, Sequence, set, frozenset):
142
+ item_type = args[0] if args else Any
143
+ item_schema = _python_type_to_json_schema(item_type)
144
+ schema_arr: dict[str, Any] = {"type": "array", "items": item_schema or {"type": "string"}}
145
+ if origin in (set, frozenset):
146
+ schema_arr["uniqueItems"] = True
147
+ return schema_arr
148
+ if origin is tuple:
149
+ if not args:
150
+ return {"type": "array", "items": {"type": "string"}}
151
+ if len(args) == 2 and args[1] is Ellipsis:
152
+ return {"type": "array", "items": _python_type_to_json_schema(args[0])}
153
+ prefix_items = [_python_type_to_json_schema(a) for a in args]
154
+ return {
155
+ "type": "array",
156
+ "prefixItems": prefix_items,
157
+ "minItems": len(prefix_items),
158
+ "maxItems": len(prefix_items),
159
+ }
160
+
161
+ if origin in (dict, Mapping):
162
+ value_type = args[1] if len(args) >= 2 else Any
163
+ value_schema = _python_type_to_json_schema(value_type)
164
+ return {"type": "object", "additionalProperties": value_schema or {"type": "string"}}
165
+
166
+ typing_union = getattr(__import__("typing"), "Union", None)
167
+ if origin in (typing_union, _types.UnionType):
168
+ non_none_args = [a for a in args if a is not type(None)]
169
+ if len(non_none_args) > 1:
170
+ schemas = [_python_type_to_json_schema(arg) for arg in non_none_args]
171
+ return {"oneOf": schemas}
172
+ if non_none_args:
173
+ return _python_type_to_json_schema(non_none_args[0])
174
+ return {"type": "string"}
175
+
176
+ return {"type": "string"}
177
+
178
+ def _parse_callable_properties(func: ToolFn) -> tuple[dict[str, dict[str, Any]], list[str]]:
179
+ sig = inspect.signature(func)
180
+ type_hints = get_type_hints(func)
181
+
182
+ properties: dict[str, dict[str, Any]] = {}
183
+ required: list[str] = []
184
+
185
+ for param_name, param in sig.parameters.items():
186
+ if param.kind in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD):
187
+ continue
188
+
189
+ annotated_type = type_hints.get(param_name, str)
190
+ param_schema = _python_type_to_json_schema(annotated_type)
191
+
192
+ type_name = getattr(annotated_type, "__name__", str(annotated_type))
193
+ properties[param_name] = {
194
+ **param_schema,
195
+ "description": f"Parameter {param_name} of type {type_name}",
196
+ }
197
+
198
+ if param.default == inspect.Parameter.empty:
199
+ required.append(param_name)
200
+
201
+ return properties, required
202
+
203
+ def generate_tool_definition_from_callable(func: ToolFn) -> dict[str, Any]:
204
+ """Convert a Python callable to OpenAI tools format.
205
+
206
+ Args:
207
+ func: A Python callable (function) to convert to a tool
208
+
209
+ Returns:
210
+ Dictionary in OpenAI tools format
211
+
212
+ Raises:
213
+ ValueError: If the function doesn't have proper docstring or type annotations
214
+
215
+ Example:
216
+ >>> def get_weather(location: str, unit: str = "celsius") -> str:
217
+ ... '''Get weather information for a location.'''
218
+ ... return f"Weather in {location} is sunny, 25°{unit[0].upper()}"
219
+ >>>
220
+ >>> tool = generate_tool_definition_from_callable(get_weather)
221
+ >>> # Returns OpenAI tools format dict
222
+
223
+ """
224
+ if not func.__doc__:
225
+ msg = f"Function {func.__name__} must have a docstring"
226
+ raise ValueError(msg)
227
+
228
+ properties, required = _parse_callable_properties(func)
229
+ return {
230
+ "type": "function",
231
+ "function": {
232
+ "name": func.__name__,
233
+ "description": inspect.cleandoc(func.__doc__),
234
+ "parameters": {"type": "object", "properties": properties, "required": required},
235
+ },
236
+ }
237
+
238
+ def generate_tool_definition_from_tool_def(tool_def: ToolDef) -> dict[str, Any]:
239
+ """Convert a ToolDef to OpenAI tools format.
240
+
241
+ Args:
242
+ tool_def: A ToolDef to convert to a tool
243
+
244
+ Returns:
245
+ Dictionary in OpenAI tools format
246
+
247
+ Example:
248
+ >>> tool_def = ToolDef(
249
+ ... name="get_weather",
250
+ ... description="Get weather information for a location.",
251
+ ... execute=SomeFunction(),
252
+ ... )
253
+ >>> tool = generate_tool_definition_from_tool_def(tool_def)
254
+ >>> # Returns OpenAI tools format dict
255
+ """
256
+ properties, required = _parse_callable_properties(tool_def.execute)
257
+ return {
258
+ "type": "function",
259
+ "function": {
260
+ "name": tool_def.name,
261
+ "description": tool_def.description,
262
+ "parameters": {"type": "object", "properties": properties, "required": required},
263
+ },
264
+ }
265
+
266
+ def generate_tool_definition_from_raw_tool_def(raw_tool_def: RawToolDef) -> dict[str, Any]:
267
+ return {
268
+ "type": "function",
269
+ "function": raw_tool_def,
270
+ }
271
+
272
+ def prepare_tools(tools: Sequence[ToolLike]) -> list[dict]:
273
+ tool_defs = []
274
+ for tool in tools:
275
+ if callable(tool):
276
+ tool_defs.append(generate_tool_definition_from_callable(tool))
277
+ elif isinstance(tool, ToolDef):
278
+ tool_defs.append(generate_tool_definition_from_tool_def(tool))
279
+ else:
280
+ tool_defs.append(generate_tool_definition_from_raw_tool_def(tool))
281
+ return tool_defs
@@ -0,0 +1,18 @@
1
+ import inspect
2
+ from typing import Any, Callable, TypeVar
3
+ from ..types.tool import ToolFn
4
+
5
+ F = TypeVar("F", bound=Callable[..., Any])
6
+ TOOL_FLAG = "__is_tool__"
7
+
8
+ def tool(func: F) -> F:
9
+ setattr(func, TOOL_FLAG, True)
10
+ return func
11
+
12
+ class Toolset:
13
+ def get_tool_methods(self) -> list[ToolFn]:
14
+ return [
15
+ method
16
+ for _, method in inspect.getmembers(self, predicate=inspect.ismethod)
17
+ if getattr(method, TOOL_FLAG, False)
18
+ ]
liteai_sdk/tool/utils.py CHANGED
@@ -1,4 +1,4 @@
1
- from . import ToolFn, ToolDef, ToolLike
1
+ from ..types.tool import ToolFn, ToolDef, ToolLike
2
2
 
3
3
  def find_tool_by_name(tools: list[ToolLike], name: str) -> ToolLike | None:
4
4
  for tool in tools:
@@ -1,9 +1,10 @@
1
1
  import asyncio
2
2
  import dataclasses
3
3
  import queue
4
- from typing import Any, Generator, Literal
4
+ from typing import Any, Literal
5
5
  from collections.abc import AsyncGenerator, Generator
6
- from ..tool import ToolLike
6
+ from .tool import ToolLike
7
+ from ..tool.toolset import Toolset
7
8
  from .message import ChatMessage, AssistantMessage, ToolMessage, MessageChunk
8
9
 
9
10
  @dataclasses.dataclass
@@ -11,6 +12,7 @@ class LlmRequestParams:
11
12
  model: str
12
13
  messages: list[ChatMessage]
13
14
  tools: list[ToolLike] | None = None
15
+ toolsets: list[Toolset] | None = None
14
16
  tool_choice: Literal["auto", "required", "none"] = "auto"
15
17
  execute_tools: bool = False
16
18
 
@@ -3,13 +3,18 @@ from __future__ import annotations
3
3
  import json
4
4
  import dataclasses
5
5
  from abc import ABC, abstractmethod
6
- from typing import TYPE_CHECKING, Any, Literal, cast
6
+ from typing import TYPE_CHECKING, Any, Literal, NamedTuple, cast
7
7
  from pydantic import BaseModel, ConfigDict, PrivateAttr, field_validator
8
- from litellm.types.utils import Message as LiteLlmMessage,\
9
- ModelResponseStream as LiteLlmModelResponseStream,\
10
- ChatCompletionAudioResponse,\
11
- ChatCompletionMessageToolCall,\
12
- ChatCompletionDeltaToolCall
8
+ from litellm.types.utils import (
9
+ Message as LiteLlmMessage,
10
+ ModelResponse as LiteLlmModelResponse,
11
+ ModelResponseStream as LiteLlmModelResponseStream,
12
+ Choices as LiteLlmModelResponseChoices,
13
+ ChatCompletionAudioResponse,
14
+ ChatCompletionMessageToolCall,
15
+ ChatCompletionDeltaToolCall,
16
+ Usage as LiteLlmUsage
17
+ )
13
18
  from litellm.types.llms.openai import (
14
19
  AllMessageValues,
15
20
  OpenAIMessageContent,
@@ -21,14 +26,14 @@ from litellm.types.llms.openai import (
21
26
  ChatCompletionToolMessage,
22
27
  ChatCompletionSystemMessage,
23
28
  )
24
- from ..tool import ToolLike
29
+ from ..types.tool import ToolLike
25
30
  from ..tool.utils import find_tool_by_name
26
31
  from ..logger import logger
27
32
 
28
33
  if TYPE_CHECKING:
29
34
  from . import LlmRequestParams
30
35
 
31
- class ChatMessage(BaseModel, ABC):
36
+ class ChatMessage(BaseModel, ABC):
32
37
  model_config = ConfigDict(
33
38
  arbitrary_types_allowed=True,
34
39
  validate_assignment=True,
@@ -46,7 +51,7 @@ class UserMessage(ChatMessage):
46
51
 
47
52
  class ToolMessage(ChatMessage):
48
53
  """
49
- The `tool_def` field is ref to the target tool of the tool call, and
54
+ The `tool_def` field is ref to the target tool of the tool call, and
50
55
  it will only be None when the target tool is not found
51
56
  """
52
57
  id: str
@@ -88,19 +93,27 @@ class ToolMessage(ChatMessage):
88
93
  content=content,
89
94
  tool_call_id=self.id)
90
95
 
91
- ToolCallTuple = tuple[str, str, str]
96
+ class ToolCallTuple(NamedTuple):
97
+ id: str
98
+ function_name: str
99
+ function_arguments: str
100
+
92
101
  class AssistantMessage(ChatMessage):
93
102
  content: str | None = None
94
103
  reasoning_content: str | None = None
95
104
  tool_calls: list[ChatCompletionAssistantToolCall] | None = None
96
105
  audio: ChatCompletionAudioResponse | None = None
97
106
  images: list[ChatCompletionImageURL] | None = None
107
+ usage: LiteLlmUsage | None = None
98
108
  role: Literal["assistant"] = "assistant"
99
109
 
100
110
  _request_params_ref: LlmRequestParams | None = PrivateAttr(default=None)
101
111
 
102
112
  @classmethod
103
- def from_litellm_message(cls, message: LiteLlmMessage) -> "AssistantMessage":
113
+ def from_litellm_message(cls, response: LiteLlmModelResponse) -> "AssistantMessage":
114
+ choices = cast(list[LiteLlmModelResponseChoices], response.choices)
115
+ message = choices[0].message
116
+
104
117
  tool_calls: list[ChatCompletionAssistantToolCall] | None = None
105
118
  if (message_tool_calls := message.get("tool_calls")) is not None:
106
119
  tool_calls = [ChatCompletionAssistantToolCall(
@@ -118,6 +131,7 @@ class AssistantMessage(ChatMessage):
118
131
  tool_calls=tool_calls,
119
132
  audio=message.get("audio"),
120
133
  images=message.get("images"),
134
+ usage=response.get("usage"),
121
135
  )
122
136
 
123
137
  def with_request_params(self, request_params: LlmRequestParams) -> "AssistantMessage":
@@ -143,7 +157,7 @@ class AssistantMessage(ChatMessage):
143
157
  function_name is None or\
144
158
  function_arguments is None:
145
159
  return None
146
- results.append((id, function_name, function_arguments))
160
+ results.append(ToolCallTuple(id, function_name, function_arguments))
147
161
  return results
148
162
 
149
163
  def get_partial_tool_messages(self) -> list[ToolMessage] | None:
@@ -164,22 +178,20 @@ class AssistantMessage(ChatMessage):
164
178
 
165
179
  results = []
166
180
  for tool_call in parsed_tool_calls:
167
- id, name, arguments = tool_call
168
-
169
181
  tool_message = ToolMessage(
170
- id=id,
171
- name=name,
172
- arguments=arguments,
182
+ id=tool_call.id,
183
+ name=tool_call.function_name,
184
+ arguments=tool_call.function_arguments,
173
185
  result=None,
174
186
  error=None)
175
187
 
176
188
  if has_tool_def:
177
189
  assert self._request_params_ref and self._request_params_ref.tools
178
- target_tool = find_tool_by_name(self._request_params_ref.tools, name)
190
+ target_tool = find_tool_by_name(self._request_params_ref.tools, tool_call.function_name)
179
191
  if target_tool:
180
192
  tool_message = tool_message.with_tool_def(target_tool)
181
193
  else:
182
- logger.warning(f"Tool {name} not found in request params, "
194
+ logger.warning(f"Tool {tool_call.function_name} not found in request params, "
183
195
  "tool_def will not be attached to the tool message")
184
196
 
185
197
  results.append(tool_message)
@@ -196,6 +208,12 @@ class SystemMessage(ChatMessage):
196
208
  class TextChunk:
197
209
  content: str
198
210
 
211
+ @dataclasses.dataclass
212
+ class UsageChunk:
213
+ input_tokens: int
214
+ output_tokens: int
215
+ total_tokens: int
216
+
199
217
  @dataclasses.dataclass
200
218
  class ReasoningChunk:
201
219
  content: str
@@ -215,7 +233,7 @@ class ToolCallChunk:
215
233
  arguments: str
216
234
  index: int
217
235
 
218
- MessageChunk = TextChunk | ReasoningChunk | AudioChunk | ImageChunk | ToolCallChunk
236
+ MessageChunk = TextChunk | UsageChunk | ReasoningChunk | AudioChunk | ImageChunk | ToolCallChunk
219
237
 
220
238
  def openai_chunk_normalizer(
221
239
  chunk: LiteLlmModelResponseStream
@@ -239,4 +257,10 @@ def openai_chunk_normalizer(
239
257
  tool_call.function.name,
240
258
  tool_call.function.arguments,
241
259
  tool_call.index))
260
+ if (usage := getattr(chunk, "usage", None)) is not None:
261
+ usage = cast(LiteLlmUsage, usage)
262
+ result.append(UsageChunk(
263
+ input_tokens=usage.prompt_tokens,
264
+ output_tokens=usage.completion_tokens,
265
+ total_tokens=usage.total_tokens))
242
266
  return result
@@ -0,0 +1,33 @@
1
+ import dataclasses
2
+ from collections.abc import Callable
3
+ from typing import Any, Awaitable
4
+
5
+ ToolFn = Callable[..., Any] | Callable[..., Awaitable[Any]]
6
+
7
+ """
8
+ RawToolDef example:
9
+ {
10
+ "name": "get_current_weather",
11
+ "description": "Get the current weather in a given location",
12
+ "parameters": {
13
+ "type": "object",
14
+ "properties": {
15
+ "location": {
16
+ "type": "string",
17
+ "description": "The city and state, e.g. San Francisco, CA",
18
+ },
19
+ "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]},
20
+ },
21
+ "required": ["location"],
22
+ }
23
+ }
24
+ """
25
+ RawToolDef = dict[str, Any]
26
+
27
+ @dataclasses.dataclass
28
+ class ToolDef:
29
+ name: str
30
+ description: str
31
+ execute: ToolFn
32
+
33
+ ToolLike = ToolDef | RawToolDef | ToolFn
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: liteai_sdk
3
- Version: 0.3.21
3
+ Version: 0.4.0
4
4
  Summary: A wrapper of LiteLLM
5
5
  Author-email: BHznJNs <bhznjns@outlook.com>
6
6
  Requires-Python: >=3.10
@@ -0,0 +1,18 @@
1
+ liteai_sdk/__init__.py,sha256=nXEHbRSYjpGFJnzmOB_cpx8NL12Eumw_IzY8UjD6GAM,10527
2
+ liteai_sdk/debug.py,sha256=T7qIy1BeeUGlF40l9JCMMVn8pvvMJAEQeG4adQbOydA,69
3
+ liteai_sdk/logger.py,sha256=99vJAQRKcu4CuHgZYAJ2zDQtGea6Bn3vJJrS-mtza7c,677
4
+ liteai_sdk/param_parser.py,sha256=ae_aaOfwBNhR3QW7xxmjOf4D5ssS_9VN0IeRH5aBUYQ,2249
5
+ liteai_sdk/stream.py,sha256=T9MLmgPC8te6qvSkBOh7vkl-I4OGCKuW1kEN6RkiCe0,3176
6
+ liteai_sdk/tool/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
+ liteai_sdk/tool/execute.py,sha256=AzYqX-oG89LLHrxouZtf7M7HohFf_WRtzqczlmw5Nks,2473
8
+ liteai_sdk/tool/prepare.py,sha256=8JJiLev3pALu36gaqXHX9CtfCFWVM6nBbVBIrqm24po,11499
9
+ liteai_sdk/tool/toolset.py,sha256=bl7qrrlBFz7HHpt8ZZlTBqjDin5MIOTtIz2o3H8kgRI,476
10
+ liteai_sdk/tool/utils.py,sha256=A_4Jx1BacRX1KmK3t_9rDXrmSXj6v4fzNtqLsN12S0I,420
11
+ liteai_sdk/types/__init__.py,sha256=WHp1YUOdINvv4shYBNo3xmEr_6B7boXvAAaefldkHbs,1071
12
+ liteai_sdk/types/exceptions.py,sha256=hIGu06htOJxfEBAHx7KTvLQr0Y8GYnBLFJFlr_IGpDs,602
13
+ liteai_sdk/types/message.py,sha256=vAp1uMv-WXFvEBhxFyw6rZt3IS-pLdANEuEFJJRJ8aY,9520
14
+ liteai_sdk/types/tool.py,sha256=XbqbANr8D-FHDConmKCULoX3KfXkQXhCiSTHvVmKOl0,797
15
+ liteai_sdk-0.4.0.dist-info/licenses/LICENSE,sha256=cTeVgQVJJcRdm1boa2P1FBnOeXfA_egV6s4PouyrCxg,1064
16
+ liteai_sdk-0.4.0.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82
17
+ liteai_sdk-0.4.0.dist-info/METADATA,sha256=6660sLr1g5VFHUcf57zfHBkYekJCWuOm58qOgBKlNr8,3023
18
+ liteai_sdk-0.4.0.dist-info/RECORD,,
@@ -1,15 +0,0 @@
1
- liteai_sdk/__init__.py,sha256=KTHDeLyGGtVA-H8nJhJMBr_rKFV1h5cD5voZ8oPXI00,10608
2
- liteai_sdk/debug.py,sha256=T7qIy1BeeUGlF40l9JCMMVn8pvvMJAEQeG4adQbOydA,69
3
- liteai_sdk/logger.py,sha256=99vJAQRKcu4CuHgZYAJ2zDQtGea6Bn3vJJrS-mtza7c,677
4
- liteai_sdk/param_parser.py,sha256=xykvUesZzwZNf4-n1j4JfVk0L2y_wvnSWSsHo5vjBU8,1655
5
- liteai_sdk/stream.py,sha256=T9MLmgPC8te6qvSkBOh7vkl-I4OGCKuW1kEN6RkiCe0,3176
6
- liteai_sdk/tool/__init__.py,sha256=c1qJaEpoYlgOCtAjFODhrSR73ZW17OuamsO__yeYAkY,12150
7
- liteai_sdk/tool/execute.py,sha256=1CfRlJZgqoev42fDH4vygXyEtCEEBPcRfbqaP77jxu4,2462
8
- liteai_sdk/tool/utils.py,sha256=Djd1-EoLPfIqgPbWWvOreozQ76NHX4FZ6OXc1evKqPM,409
9
- liteai_sdk/types/__init__.py,sha256=CMmweIGMgreZlbvBtRTKfvdcC7war2ApLNf-9Fz0yzc,1006
10
- liteai_sdk/types/exceptions.py,sha256=hIGu06htOJxfEBAHx7KTvLQr0Y8GYnBLFJFlr_IGpDs,602
11
- liteai_sdk/types/message.py,sha256=AnhJ5wKKcWuAt0lW3mPXpIyvUBy3u-iFLa1dpeUTp18,8785
12
- liteai_sdk-0.3.21.dist-info/licenses/LICENSE,sha256=cTeVgQVJJcRdm1boa2P1FBnOeXfA_egV6s4PouyrCxg,1064
13
- liteai_sdk-0.3.21.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82
14
- liteai_sdk-0.3.21.dist-info/METADATA,sha256=uUYWHL4MKkSTsqokLBEN2EcJd60HkinQthNXRRTabzU,3024
15
- liteai_sdk-0.3.21.dist-info/RECORD,,