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 +21 -16
- liteai_sdk/param_parser.py +18 -2
- liteai_sdk/tool/__init__.py +0 -310
- liteai_sdk/tool/execute.py +1 -1
- liteai_sdk/tool/prepare.py +281 -0
- liteai_sdk/tool/toolset.py +18 -0
- liteai_sdk/tool/utils.py +1 -1
- liteai_sdk/types/__init__.py +4 -2
- liteai_sdk/types/message.py +44 -20
- liteai_sdk/types/tool.py +33 -0
- {liteai_sdk-0.3.21.dist-info → liteai_sdk-0.4.0.dist-info}/METADATA +1 -1
- liteai_sdk-0.4.0.dist-info/RECORD +18 -0
- liteai_sdk-0.3.21.dist-info/RECORD +0 -15
- {liteai_sdk-0.3.21.dist-info → liteai_sdk-0.4.0.dist-info}/WHEEL +0 -0
- {liteai_sdk-0.3.21.dist-info → liteai_sdk-0.4.0.dist-info}/licenses/LICENSE +0 -0
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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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[
|
|
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",
|
liteai_sdk/param_parser.py
CHANGED
|
@@ -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
|
-
|
|
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
|
liteai_sdk/tool/__init__.py
CHANGED
|
@@ -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
|
liteai_sdk/tool/execute.py
CHANGED
|
@@ -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
liteai_sdk/types/__init__.py
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import dataclasses
|
|
3
3
|
import queue
|
|
4
|
-
from typing import Any,
|
|
4
|
+
from typing import Any, Literal
|
|
5
5
|
from collections.abc import AsyncGenerator, Generator
|
|
6
|
-
from
|
|
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
|
|
liteai_sdk/types/message.py
CHANGED
|
@@ -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
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
|
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,
|
|
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=
|
|
172
|
-
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,
|
|
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 {
|
|
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
|
liteai_sdk/types/tool.py
ADDED
|
@@ -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
|
|
@@ -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,,
|
|
File without changes
|
|
File without changes
|