ailoy-py 0.0.1__cp311-cp311-win_amd64.whl → 0.0.3__cp311-cp311-win_amd64.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.
- ailoy/__init__.py +20 -1
- ailoy/agent.py +349 -309
- ailoy/ailoy_py.cp311-win_amd64.pyd +0 -0
- ailoy/mcp.py +171 -0
- ailoy/models/__init__.py +7 -0
- ailoy/models/api_model.py +71 -0
- ailoy/models/local_model.py +44 -0
- ailoy/runtime.py +34 -19
- ailoy/tools.py +205 -0
- ailoy/utils/__init__.py +0 -0
- ailoy/utils/image.py +11 -0
- ailoy/vector_store.py +10 -9
- ailoy_py-0.0.3.dist-info/DELVEWHEEL +2 -0
- {ailoy_py-0.0.1.dist-info → ailoy_py-0.0.3.dist-info}/METADATA +5 -4
- ailoy_py-0.0.3.dist-info/RECORD +27 -0
- {ailoy_py-0.0.1.dist-info → ailoy_py-0.0.3.dist-info}/WHEEL +1 -1
- ailoy_py.libs/msvcp140-0c97ddc05c5b9024aa6af9538804ea77.dll +0 -0
- ailoy_py.libs/tvm_runtime-9f92bf96bed615c59113b3421aae022f.dll +0 -0
- ailoy_py-0.0.1.dist-info/DELVEWHEEL +0 -2
- ailoy_py-0.0.1.dist-info/RECORD +0 -20
- ailoy_py.libs/msvcp140-9867ece6bcf7e4746fa7e6671b0a17bd.dll +0 -0
- ailoy_py.libs/tvm_runtime-d2432b09972b70bd5375e7bd4a3ca45e.dll +0 -0
- {ailoy_py-0.0.1.dist-info → ailoy_py-0.0.3.dist-info}/entry_points.txt +0 -0
Binary file
|
ailoy/mcp.py
ADDED
@@ -0,0 +1,171 @@
|
|
1
|
+
import asyncio
|
2
|
+
import json
|
3
|
+
import multiprocessing
|
4
|
+
import platform
|
5
|
+
import tempfile
|
6
|
+
from multiprocessing.connection import Connection
|
7
|
+
from typing import Annotated, Any, Literal, Union
|
8
|
+
|
9
|
+
import mcp.types as mcp_types
|
10
|
+
from mcp import Tool as MCPTool
|
11
|
+
from mcp.client.session import ClientSession
|
12
|
+
from mcp.client.stdio import (
|
13
|
+
StdioServerParameters,
|
14
|
+
stdio_client,
|
15
|
+
)
|
16
|
+
from mcp.shared.exceptions import McpError
|
17
|
+
from pydantic import BaseModel, Field, TypeAdapter
|
18
|
+
|
19
|
+
__all__ = ["MCPServer"]
|
20
|
+
|
21
|
+
|
22
|
+
class ListToolsRequest(BaseModel):
|
23
|
+
type: Literal["list_tools"] = "list_tools"
|
24
|
+
|
25
|
+
|
26
|
+
class CallToolRequest(BaseModel):
|
27
|
+
type: Literal["call_tool"] = "call_tool"
|
28
|
+
tool: MCPTool
|
29
|
+
arguments: dict[str, Any]
|
30
|
+
|
31
|
+
|
32
|
+
class ShutdownRequest(BaseModel):
|
33
|
+
type: Literal["shutdown"] = "shutdown"
|
34
|
+
|
35
|
+
|
36
|
+
# Requests (main -> subprocess)
|
37
|
+
RequestMessage = Annotated[Union[ListToolsRequest, CallToolRequest, ShutdownRequest], Field(discriminator="type")]
|
38
|
+
|
39
|
+
|
40
|
+
class ResultMessage(BaseModel):
|
41
|
+
type: Literal["result"] = "result"
|
42
|
+
result: Any
|
43
|
+
|
44
|
+
|
45
|
+
class ErrorMessage(BaseModel):
|
46
|
+
type: Literal["error"] = "error"
|
47
|
+
error: str
|
48
|
+
|
49
|
+
|
50
|
+
# Response (subprocess -> main)
|
51
|
+
ResponseMessage = Annotated[Union[ResultMessage, ErrorMessage], Field(discriminator="type")]
|
52
|
+
|
53
|
+
|
54
|
+
class MCPServer:
|
55
|
+
"""
|
56
|
+
MCPServer manages a subprocess that acts as a bridge between an MCP stdio server and the main process.
|
57
|
+
|
58
|
+
- The subprocess communicates with the MCP stdio server using the official MCP Python SDK.
|
59
|
+
- Communication between the main process and the subprocess is handled through a multiprocessing Pipe.
|
60
|
+
Messages sent over this Pipe are serialized and deserialized using structured Pydantic models:
|
61
|
+
- `RequestMessage` for requests from the main process to the subprocess.
|
62
|
+
- `ResponseMessage` for responses from the subprocess to the main process.
|
63
|
+
|
64
|
+
This design ensures:
|
65
|
+
- Type-safe, structured inter-process communication.
|
66
|
+
- Synchronous interaction with an asynchronous MCP session (via message passing).
|
67
|
+
- Subprocess lifecycle control (including initialization and shutdown).
|
68
|
+
"""
|
69
|
+
|
70
|
+
def __init__(self, name: str, params: StdioServerParameters):
|
71
|
+
self.name = name
|
72
|
+
self.params = params
|
73
|
+
|
74
|
+
self._parent_conn, self._child_conn = multiprocessing.Pipe()
|
75
|
+
|
76
|
+
ctx = multiprocessing.get_context("fork" if platform.system() != "Windows" else "spawn")
|
77
|
+
self._proc: multiprocessing.Process = ctx.Process(target=self._run_process, args=(self._child_conn,))
|
78
|
+
self._proc.start()
|
79
|
+
|
80
|
+
# Wait for subprocess to signal initialization complete
|
81
|
+
try:
|
82
|
+
self._recv_response()
|
83
|
+
except RuntimeError as e:
|
84
|
+
self.cleanup()
|
85
|
+
raise e
|
86
|
+
|
87
|
+
def __del__(self):
|
88
|
+
self.cleanup()
|
89
|
+
|
90
|
+
def _run_process(self, conn: Connection):
|
91
|
+
asyncio.run(self._process_main(conn))
|
92
|
+
|
93
|
+
async def _process_main(self, conn: Connection):
|
94
|
+
with tempfile.TemporaryFile(mode="w+t") as _errlog:
|
95
|
+
async with stdio_client(self.params, errlog=_errlog) as (read, write):
|
96
|
+
async with ClientSession(read, write) as session:
|
97
|
+
# Notify to main process that the initialization has been finished and ready to receive requests
|
98
|
+
try:
|
99
|
+
await session.initialize()
|
100
|
+
conn.send(ResultMessage(result=True).model_dump())
|
101
|
+
except McpError:
|
102
|
+
_errlog.seek(0)
|
103
|
+
error = _errlog.read()
|
104
|
+
conn.send(
|
105
|
+
ErrorMessage(
|
106
|
+
error=f"Failed to initialize MCP subprocess. Check the error output below.\n\n{error}"
|
107
|
+
).model_dump()
|
108
|
+
)
|
109
|
+
|
110
|
+
while True:
|
111
|
+
if not conn.poll(0.1):
|
112
|
+
await asyncio.sleep(0.1)
|
113
|
+
continue
|
114
|
+
|
115
|
+
try:
|
116
|
+
raw = conn.recv()
|
117
|
+
req = TypeAdapter(RequestMessage).validate_python(raw)
|
118
|
+
|
119
|
+
if isinstance(req, ListToolsRequest):
|
120
|
+
result = await session.list_tools()
|
121
|
+
conn.send(ResultMessage(result=result.tools).model_dump())
|
122
|
+
|
123
|
+
elif isinstance(req, CallToolRequest):
|
124
|
+
result = await session.call_tool(req.tool.name, req.arguments)
|
125
|
+
contents: list[str] = []
|
126
|
+
for item in result.content:
|
127
|
+
if isinstance(item, mcp_types.TextContent):
|
128
|
+
try:
|
129
|
+
content = json.loads(item.text)
|
130
|
+
contents.append(json.dumps(content))
|
131
|
+
except json.JSONDecodeError:
|
132
|
+
contents.append(item.text)
|
133
|
+
elif isinstance(item, mcp_types.ImageContent):
|
134
|
+
contents.append(item.data)
|
135
|
+
elif isinstance(item, mcp_types.EmbeddedResource):
|
136
|
+
if isinstance(item.resource, mcp_types.TextResourceContents):
|
137
|
+
contents.append(item.resource.text)
|
138
|
+
else:
|
139
|
+
contents.append(item.resource.blob)
|
140
|
+
conn.send(ResultMessage(result=contents).model_dump())
|
141
|
+
|
142
|
+
elif isinstance(req, ShutdownRequest):
|
143
|
+
break
|
144
|
+
|
145
|
+
except Exception as e:
|
146
|
+
conn.send(ErrorMessage(error=str(e)).model_dump())
|
147
|
+
|
148
|
+
def _send_request(self, msg: RequestMessage):
|
149
|
+
self._parent_conn.send(msg.model_dump())
|
150
|
+
|
151
|
+
def _recv_response(self) -> ResultMessage:
|
152
|
+
raw = self._parent_conn.recv()
|
153
|
+
msg = TypeAdapter(ResponseMessage).validate_python(raw)
|
154
|
+
if isinstance(msg, ErrorMessage):
|
155
|
+
raise RuntimeError(msg.error)
|
156
|
+
return msg
|
157
|
+
|
158
|
+
def list_tools(self) -> list[MCPTool]:
|
159
|
+
self._send_request(ListToolsRequest())
|
160
|
+
msg = self._recv_response()
|
161
|
+
return [MCPTool.model_validate(tool) for tool in msg.result]
|
162
|
+
|
163
|
+
def call_tool(self, tool: MCPTool, arguments: dict[str, Any]) -> list[str]:
|
164
|
+
self._send_request(CallToolRequest(tool=tool, arguments=arguments))
|
165
|
+
msg = self._recv_response()
|
166
|
+
return msg.result
|
167
|
+
|
168
|
+
def cleanup(self) -> None:
|
169
|
+
if self._proc.is_alive():
|
170
|
+
self._send_request(ShutdownRequest())
|
171
|
+
self._proc.join()
|
ailoy/models/__init__.py
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
from typing import Literal, Optional, Self, get_args
|
2
|
+
|
3
|
+
from pydantic import model_validator
|
4
|
+
from pydantic.dataclasses import dataclass
|
5
|
+
|
6
|
+
OpenAIModelId = Literal[
|
7
|
+
"o4-mini",
|
8
|
+
"o3",
|
9
|
+
"o3-pro",
|
10
|
+
"o3-mini",
|
11
|
+
"gpt-4o",
|
12
|
+
"gpt-4o-mini",
|
13
|
+
"gpt-4.1",
|
14
|
+
"gpt-4.1-mini",
|
15
|
+
"gpt-4.1-nano",
|
16
|
+
]
|
17
|
+
|
18
|
+
GeminiModelId = Literal[
|
19
|
+
"gemini-2.5-flash",
|
20
|
+
"gemini-2.5-pro",
|
21
|
+
"gemini-2.0-flash",
|
22
|
+
"gemini-1.5-flash",
|
23
|
+
"gemini-1.5-pro",
|
24
|
+
]
|
25
|
+
|
26
|
+
ClaudeModelId = Literal[
|
27
|
+
"claude-sonnet-4-20250514",
|
28
|
+
"claude-3-7-sonnet-20250219",
|
29
|
+
"claude-3-5-sonnet-20241022",
|
30
|
+
"claude-3-5-sonnet-20240620",
|
31
|
+
"claude-opus-4-20250514",
|
32
|
+
"claude-3-opus-20240229",
|
33
|
+
"claude-3-5-haiku-20241022",
|
34
|
+
"claude-3-haiku-20240307",
|
35
|
+
]
|
36
|
+
|
37
|
+
APIModelProvider = Literal["openai", "gemini", "claude"]
|
38
|
+
|
39
|
+
|
40
|
+
@dataclass
|
41
|
+
class APIModel:
|
42
|
+
id: OpenAIModelId | GeminiModelId | ClaudeModelId | str
|
43
|
+
api_key: str
|
44
|
+
provider: Optional[APIModelProvider] = None
|
45
|
+
|
46
|
+
@model_validator(mode="after")
|
47
|
+
def validate_provider(self) -> Self:
|
48
|
+
if self.provider is None:
|
49
|
+
if self.id in get_args(OpenAIModelId):
|
50
|
+
self.provider = "openai"
|
51
|
+
elif self.id in get_args(GeminiModelId):
|
52
|
+
self.provider = "gemini"
|
53
|
+
elif self.id in get_args(ClaudeModelId):
|
54
|
+
self.provider = "claude"
|
55
|
+
else:
|
56
|
+
raise ValueError(
|
57
|
+
f'Failed to infer the model provider based on the model id "{self.id}". '
|
58
|
+
"Please provide an explicit model provider."
|
59
|
+
)
|
60
|
+
|
61
|
+
return self
|
62
|
+
|
63
|
+
@property
|
64
|
+
def component_type(self) -> str:
|
65
|
+
return self.provider
|
66
|
+
|
67
|
+
def to_attrs(self):
|
68
|
+
return {
|
69
|
+
"model": self.id,
|
70
|
+
"api_key": self.api_key,
|
71
|
+
}
|
@@ -0,0 +1,44 @@
|
|
1
|
+
from typing import Literal, Optional
|
2
|
+
|
3
|
+
from pydantic.dataclasses import dataclass
|
4
|
+
|
5
|
+
LocalModelBackend = Literal["tvm"]
|
6
|
+
LocalModelId = Literal[
|
7
|
+
"Qwen/Qwen3-0.6B",
|
8
|
+
"Qwen/Qwen3-1.7B",
|
9
|
+
"Qwen/Qwen3-4B",
|
10
|
+
"Qwen/Qwen3-8B",
|
11
|
+
"Qwen/Qwen3-14B",
|
12
|
+
"Qwen/Qwen3-32B",
|
13
|
+
"Qwen/Qwen3-30B-A3B",
|
14
|
+
]
|
15
|
+
Quantization = Literal["q4f16_1"]
|
16
|
+
|
17
|
+
|
18
|
+
@dataclass
|
19
|
+
class LocalModel:
|
20
|
+
id: LocalModelId
|
21
|
+
backend: LocalModelBackend = "tvm"
|
22
|
+
quantization: Quantization = "q4f16_1"
|
23
|
+
device: int = 0
|
24
|
+
|
25
|
+
@property
|
26
|
+
def default_system_message(self) -> Optional[str]:
|
27
|
+
if self.id.startswith("Qwen"):
|
28
|
+
return "You are Qwen, created by Alibaba Cloud. You are a helpful assistant."
|
29
|
+
return None
|
30
|
+
|
31
|
+
@property
|
32
|
+
def component_type(self) -> str:
|
33
|
+
if self.backend == "tvm":
|
34
|
+
return "tvm_language_model"
|
35
|
+
raise ValueError(f"Unknown local model backend: {self.backend}")
|
36
|
+
|
37
|
+
def to_attrs(self) -> dict:
|
38
|
+
if self.backend == "tvm":
|
39
|
+
return {
|
40
|
+
"model": self.id,
|
41
|
+
"quantization": self.quantization,
|
42
|
+
"device": self.device,
|
43
|
+
}
|
44
|
+
raise ValueError(f"Unknown local model backend: {self.backend}")
|
ailoy/runtime.py
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
import time
|
1
2
|
from asyncio import Event, to_thread
|
2
3
|
from collections import defaultdict
|
3
4
|
from typing import Any, AsyncGenerator, Generator, Literal, Optional, TypedDict
|
@@ -15,21 +16,25 @@ class Packet(TypedDict):
|
|
15
16
|
|
16
17
|
|
17
18
|
class RuntimeBase:
|
18
|
-
|
19
|
-
|
19
|
+
__client_count: dict[str, int] = {}
|
20
|
+
|
21
|
+
def __init__(self, url: str = "inproc://"):
|
22
|
+
self.url: str = url
|
20
23
|
self._responses: dict[str, Packet] = {}
|
21
24
|
self._exec_responses: defaultdict[str, dict[int, Packet]] = defaultdict(dict)
|
22
25
|
self._listen_lock: Optional[Event] = None
|
23
26
|
|
24
|
-
|
25
|
-
|
27
|
+
if RuntimeBase.__client_count.get(self.url, 0) == 0:
|
28
|
+
start_threads(self.url)
|
29
|
+
RuntimeBase.__client_count[self.url] = 0
|
30
|
+
|
31
|
+
self._client: BrokerClient = BrokerClient(self.url)
|
26
32
|
txid = self._send_type1("connect")
|
27
|
-
if not txid:
|
28
|
-
raise RuntimeError("Connection failed")
|
29
33
|
self._sync_listen()
|
30
34
|
if not self._responses[txid]["body"]["status"]:
|
31
35
|
raise RuntimeError("Connection failed")
|
32
36
|
del self._responses[txid]
|
37
|
+
RuntimeBase.__client_count[self.url] += 1
|
33
38
|
|
34
39
|
def __del__(self):
|
35
40
|
self.stop()
|
@@ -41,22 +46,32 @@ class RuntimeBase:
|
|
41
46
|
self.stop()
|
42
47
|
|
43
48
|
def stop(self):
|
44
|
-
if self.
|
49
|
+
if self.is_alive():
|
45
50
|
txid = self._send_type1("disconnect")
|
46
|
-
if not txid:
|
47
|
-
raise RuntimeError("Disconnection failed")
|
48
51
|
while txid not in self._responses:
|
49
52
|
self._sync_listen()
|
50
53
|
if not self._responses[txid]["body"]["status"]:
|
51
54
|
raise RuntimeError("Disconnection failed")
|
52
55
|
self._client = None
|
53
|
-
|
56
|
+
RuntimeBase.__client_count[self.url] -= 1
|
57
|
+
if RuntimeBase.__client_count.get(self.url, 0) <= 0:
|
58
|
+
stop_threads(self.url)
|
59
|
+
RuntimeBase.__client_count.pop(self.url, 0)
|
54
60
|
|
55
|
-
def
|
61
|
+
def is_alive(self):
|
62
|
+
return self._client is not None
|
63
|
+
|
64
|
+
def _send_type1(self, ptype: Literal["connect", "disconnect"]) -> str:
|
56
65
|
txid = generate_uuid()
|
57
|
-
|
58
|
-
|
59
|
-
|
66
|
+
retry_count = 0
|
67
|
+
# Since the broker thread might start slightly later than the runtime client,
|
68
|
+
# we retry sending the packat a few times to ensure delivery.
|
69
|
+
while retry_count < 3:
|
70
|
+
if self._client.send_type1(txid, ptype):
|
71
|
+
return txid
|
72
|
+
time.sleep(0.001)
|
73
|
+
retry_count += 1
|
74
|
+
raise RuntimeError(f'Failed to send packet "{ptype}"')
|
60
75
|
|
61
76
|
def _send_type2(
|
62
77
|
self,
|
@@ -76,7 +91,7 @@ class RuntimeBase:
|
|
76
91
|
*args,
|
77
92
|
):
|
78
93
|
txid = generate_uuid()
|
79
|
-
if self._client.
|
94
|
+
if self._client.send_type3(txid, ptype, status, *args):
|
80
95
|
return txid
|
81
96
|
raise RuntimeError("Failed to send packet")
|
82
97
|
|
@@ -112,8 +127,8 @@ class RuntimeBase:
|
|
112
127
|
|
113
128
|
|
114
129
|
class Runtime(RuntimeBase):
|
115
|
-
def __init__(self,
|
116
|
-
super().__init__(
|
130
|
+
def __init__(self, url: str = "inproc://"):
|
131
|
+
super().__init__(url)
|
117
132
|
|
118
133
|
def call(self, func_name: str, input: Any) -> Any:
|
119
134
|
rv = [v for v in self.call_iter(func_name, input)]
|
@@ -193,8 +208,8 @@ class Runtime(RuntimeBase):
|
|
193
208
|
|
194
209
|
|
195
210
|
class AsyncRuntime(RuntimeBase):
|
196
|
-
def __init__(self,
|
197
|
-
super().__init__(
|
211
|
+
def __init__(self, url: str = "inproc://"):
|
212
|
+
super().__init__(url)
|
198
213
|
|
199
214
|
async def call(self, func_name: str, input: Any) -> Any:
|
200
215
|
rv = [v async for v in self.call_iter(func_name, input)]
|
ailoy/tools.py
ADDED
@@ -0,0 +1,205 @@
|
|
1
|
+
import inspect
|
2
|
+
import json
|
3
|
+
import re
|
4
|
+
import types
|
5
|
+
from typing import (
|
6
|
+
Any,
|
7
|
+
Callable,
|
8
|
+
Optional,
|
9
|
+
Union,
|
10
|
+
get_args,
|
11
|
+
get_origin,
|
12
|
+
get_type_hints,
|
13
|
+
)
|
14
|
+
|
15
|
+
description_re = re.compile(r"^(.*?)[\n\s]*(Args:|Returns:|Raises:|\Z)", re.DOTALL)
|
16
|
+
# Extracts the Args: block from the docstring
|
17
|
+
args_re = re.compile(r"\n\s*Args:\n\s*(.*?)[\n\s]*(Returns:|Raises:|\Z)", re.DOTALL)
|
18
|
+
# Splits the Args: block into individual arguments
|
19
|
+
args_split_re = re.compile(
|
20
|
+
r"""
|
21
|
+
(?:^|\n) # Match the start of the args block, or a newline
|
22
|
+
\s*(\w+):\s* # Capture the argument name and strip spacing
|
23
|
+
(.*?)\s* # Capture the argument description, which can span multiple lines, and strip trailing spacing
|
24
|
+
(?=\n\s*\w+:|\Z) # Stop when you hit the next argument or the end of the block
|
25
|
+
""",
|
26
|
+
re.DOTALL | re.VERBOSE,
|
27
|
+
)
|
28
|
+
# Extracts the Returns: block from the docstring, if present. Note that most chat templates ignore the return type/doc!
|
29
|
+
returns_re = re.compile(r"\n\s*Returns:\n\s*(.*?)[\n\s]*(Raises:|\Z)", re.DOTALL)
|
30
|
+
|
31
|
+
|
32
|
+
class TypeHintParsingException(Exception):
|
33
|
+
"""Exception raised for errors in parsing type hints to generate JSON schemas"""
|
34
|
+
|
35
|
+
pass
|
36
|
+
|
37
|
+
|
38
|
+
class DocstringParsingException(Exception):
|
39
|
+
"""Exception raised for errors in parsing docstrings to generate JSON schemas"""
|
40
|
+
|
41
|
+
pass
|
42
|
+
|
43
|
+
|
44
|
+
def _get_json_schema_type(param_type: str) -> dict[str, str]:
|
45
|
+
type_mapping = {
|
46
|
+
int: {"type": "integer"},
|
47
|
+
float: {"type": "number"},
|
48
|
+
str: {"type": "string"},
|
49
|
+
bool: {"type": "boolean"},
|
50
|
+
type(None): {"type": "null"},
|
51
|
+
Any: {},
|
52
|
+
}
|
53
|
+
# if is_vision_available():
|
54
|
+
# type_mapping[Image] = {"type": "image"}
|
55
|
+
# if is_torch_available():
|
56
|
+
# type_mapping[Tensor] = {"type": "audio"}
|
57
|
+
return type_mapping.get(param_type, {"type": "object"})
|
58
|
+
|
59
|
+
|
60
|
+
def _parse_type_hint(hint: str) -> dict:
|
61
|
+
origin = get_origin(hint)
|
62
|
+
args = get_args(hint)
|
63
|
+
|
64
|
+
if origin is None:
|
65
|
+
try:
|
66
|
+
return _get_json_schema_type(hint)
|
67
|
+
except KeyError:
|
68
|
+
raise TypeHintParsingException(
|
69
|
+
"Couldn't parse this type hint, likely due to a custom class or object: ", hint
|
70
|
+
)
|
71
|
+
|
72
|
+
elif origin is Union or (hasattr(types, "UnionType") and origin is types.UnionType):
|
73
|
+
# Recurse into each of the subtypes in the Union, except None, which is handled separately at the end
|
74
|
+
subtypes = [_parse_type_hint(t) for t in args if t is not type(None)]
|
75
|
+
if len(subtypes) == 1:
|
76
|
+
# A single non-null type can be expressed directly
|
77
|
+
return_dict = subtypes[0]
|
78
|
+
elif all(isinstance(subtype["type"], str) for subtype in subtypes):
|
79
|
+
# A union of basic types can be expressed as a list in the schema
|
80
|
+
return_dict = {"type": sorted([subtype["type"] for subtype in subtypes])}
|
81
|
+
else:
|
82
|
+
# A union of more complex types requires "anyOf"
|
83
|
+
return_dict = {"anyOf": subtypes}
|
84
|
+
if type(None) in args:
|
85
|
+
return_dict["nullable"] = True
|
86
|
+
return return_dict
|
87
|
+
|
88
|
+
elif origin is list:
|
89
|
+
if not args:
|
90
|
+
return {"type": "array"}
|
91
|
+
else:
|
92
|
+
# Lists can only have a single type argument, so recurse into it
|
93
|
+
return {"type": "array", "items": _parse_type_hint(args[0])}
|
94
|
+
|
95
|
+
elif origin is tuple:
|
96
|
+
if not args:
|
97
|
+
return {"type": "array"}
|
98
|
+
if len(args) == 1:
|
99
|
+
raise TypeHintParsingException(
|
100
|
+
f"The type hint {str(hint).replace('typing.', '')} is a Tuple with a single element, which "
|
101
|
+
"we do not automatically convert to JSON schema as it is rarely necessary. If this input can contain "
|
102
|
+
"more than one element, we recommend "
|
103
|
+
"using a List[] type instead, or if it really is a single element, remove the Tuple[] wrapper and just "
|
104
|
+
"pass the element directly."
|
105
|
+
)
|
106
|
+
if ... in args:
|
107
|
+
raise TypeHintParsingException(
|
108
|
+
"Conversion of '...' is not supported in Tuple type hints. "
|
109
|
+
"Use List[] types for variable-length"
|
110
|
+
" inputs instead."
|
111
|
+
)
|
112
|
+
return {"type": "array", "prefixItems": [_parse_type_hint(t) for t in args]}
|
113
|
+
|
114
|
+
elif origin is dict:
|
115
|
+
# The JSON equivalent to a dict is 'object', which mandates that all keys are strings
|
116
|
+
# However, we can specify the type of the dict values with "additionalProperties"
|
117
|
+
out = {"type": "object"}
|
118
|
+
if len(args) == 2:
|
119
|
+
out["additionalProperties"] = _parse_type_hint(args[1])
|
120
|
+
return out
|
121
|
+
|
122
|
+
raise TypeHintParsingException("Couldn't parse this type hint, likely due to a custom class or object: ", hint)
|
123
|
+
|
124
|
+
|
125
|
+
def _convert_type_hints_to_json_schema(func: Callable) -> dict:
|
126
|
+
type_hints = get_type_hints(func)
|
127
|
+
signature = inspect.signature(func)
|
128
|
+
required = []
|
129
|
+
for param_name, param in signature.parameters.items():
|
130
|
+
if param.annotation == inspect.Parameter.empty:
|
131
|
+
raise TypeHintParsingException(f"Argument {param.name} is missing a type hint in function {func.__name__}")
|
132
|
+
if param.default == inspect.Parameter.empty:
|
133
|
+
required.append(param_name)
|
134
|
+
|
135
|
+
properties = {}
|
136
|
+
for param_name, param_type in type_hints.items():
|
137
|
+
properties[param_name] = _parse_type_hint(param_type)
|
138
|
+
|
139
|
+
schema = {"type": "object", "properties": properties}
|
140
|
+
if required:
|
141
|
+
schema["required"] = required
|
142
|
+
|
143
|
+
return schema
|
144
|
+
|
145
|
+
|
146
|
+
def parse_google_format_docstring(docstring: str) -> tuple[Optional[str], Optional[dict], Optional[str]]:
|
147
|
+
"""
|
148
|
+
Parses a Google-style docstring to extract the function description,
|
149
|
+
argument descriptions, and return description.
|
150
|
+
|
151
|
+
Args:
|
152
|
+
docstring (str): The docstring to parse.
|
153
|
+
|
154
|
+
Returns:
|
155
|
+
The function description, arguments, and return description.
|
156
|
+
"""
|
157
|
+
|
158
|
+
# Extract the sections
|
159
|
+
description_match = description_re.search(docstring)
|
160
|
+
args_match = args_re.search(docstring)
|
161
|
+
returns_match = returns_re.search(docstring)
|
162
|
+
|
163
|
+
# Clean and store the sections
|
164
|
+
description = description_match.group(1).strip() if description_match else None
|
165
|
+
docstring_args = args_match.group(1).strip() if args_match else None
|
166
|
+
returns = returns_match.group(1).strip() if returns_match else None
|
167
|
+
|
168
|
+
# Parsing the arguments into a dictionary
|
169
|
+
if docstring_args is not None:
|
170
|
+
docstring_args = "\n".join([line for line in docstring_args.split("\n") if line.strip()]) # Remove blank lines
|
171
|
+
matches = args_split_re.findall(docstring_args)
|
172
|
+
args_dict = {match[0]: re.sub(r"\s*\n+\s*", " ", match[1].strip()) for match in matches}
|
173
|
+
else:
|
174
|
+
args_dict = {}
|
175
|
+
|
176
|
+
return description, args_dict, returns
|
177
|
+
|
178
|
+
|
179
|
+
def get_json_schema(func: Callable) -> dict:
|
180
|
+
doc = inspect.getdoc(func)
|
181
|
+
if not doc:
|
182
|
+
raise DocstringParsingException(f"Cannot generate JSON schema for {func.__name__} because it has no docstring!")
|
183
|
+
doc = doc.strip()
|
184
|
+
main_doc, param_descriptions, return_doc = parse_google_format_docstring(doc)
|
185
|
+
|
186
|
+
json_schema = _convert_type_hints_to_json_schema(func)
|
187
|
+
if (return_dict := json_schema["properties"].pop("return", None)) is not None:
|
188
|
+
if return_doc is not None: # We allow a missing return docstring since most templates ignore it
|
189
|
+
return_dict["description"] = return_doc
|
190
|
+
for arg, schema in json_schema["properties"].items():
|
191
|
+
if arg not in param_descriptions:
|
192
|
+
raise DocstringParsingException(
|
193
|
+
f"Cannot generate JSON schema for {func.__name__} because the docstring has no description for the argument '{arg}'"
|
194
|
+
)
|
195
|
+
desc = param_descriptions[arg]
|
196
|
+
enum_choices = re.search(r"\(choices:\s*(.*?)\)\s*$", desc, flags=re.IGNORECASE)
|
197
|
+
if enum_choices:
|
198
|
+
schema["enum"] = [c.strip() for c in json.loads(enum_choices.group(1))]
|
199
|
+
desc = enum_choices.string[: enum_choices.start()].strip()
|
200
|
+
schema["description"] = desc
|
201
|
+
|
202
|
+
output = {"name": func.__name__, "description": main_doc, "parameters": json_schema}
|
203
|
+
if return_dict is not None:
|
204
|
+
output["return"] = return_dict
|
205
|
+
return {"type": "function", "function": output}
|
ailoy/utils/__init__.py
ADDED
File without changes
|
ailoy/utils/image.py
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
import base64
|
2
|
+
import io
|
3
|
+
|
4
|
+
from PIL.Image import Image
|
5
|
+
|
6
|
+
|
7
|
+
def pillow_image_to_base64(img: Image):
|
8
|
+
buffered = io.BytesIO()
|
9
|
+
img.save(buffered, format=img.format)
|
10
|
+
b64 = base64.b64encode(buffered.getvalue()).decode("utf-8")
|
11
|
+
return f"data:image/{img.format.lower()};base64,{b64}"
|
ailoy/vector_store.py
CHANGED
@@ -46,8 +46,8 @@ class VectorStore:
|
|
46
46
|
vector_store_name: Literal["faiss", "chromadb"],
|
47
47
|
url: Optional[str] = None,
|
48
48
|
collection: Optional[str] = None,
|
49
|
-
embedding_model_attrs: dict[str, Any] =
|
50
|
-
vector_store_attrs: dict[str, Any] =
|
49
|
+
embedding_model_attrs: Optional[dict[str, Any]] = None,
|
50
|
+
vector_store_attrs: Optional[dict[str, Any]] = None,
|
51
51
|
):
|
52
52
|
"""
|
53
53
|
Creates an instance.
|
@@ -69,10 +69,10 @@ class VectorStore:
|
|
69
69
|
self.define(
|
70
70
|
embedding_model_name,
|
71
71
|
vector_store_name,
|
72
|
-
url,
|
73
|
-
collection,
|
74
|
-
embedding_model_attrs,
|
75
|
-
vector_store_attrs,
|
72
|
+
url=url,
|
73
|
+
collection=collection,
|
74
|
+
embedding_model_attrs=embedding_model_attrs,
|
75
|
+
vector_store_attrs=vector_store_attrs,
|
76
76
|
)
|
77
77
|
|
78
78
|
def __del__(self):
|
@@ -90,8 +90,8 @@ class VectorStore:
|
|
90
90
|
vector_store_name: Literal["faiss", "chromadb"],
|
91
91
|
url: Optional[str] = None,
|
92
92
|
collection: Optional[str] = None,
|
93
|
-
embedding_model_attrs: dict[str, Any] =
|
94
|
-
vector_store_attrs: dict[str, Any] =
|
93
|
+
embedding_model_attrs: Optional[dict[str, Any]] = None,
|
94
|
+
vector_store_attrs: Optional[dict[str, Any]] = None,
|
95
95
|
):
|
96
96
|
"""
|
97
97
|
Defines the embedding model and vector store components to the runtime.
|
@@ -111,13 +111,14 @@ class VectorStore:
|
|
111
111
|
self._component_state.embedding_model_name,
|
112
112
|
{
|
113
113
|
"model": "BAAI/bge-m3",
|
114
|
-
**embedding_model_attrs,
|
114
|
+
**(embedding_model_attrs or {}),
|
115
115
|
},
|
116
116
|
)
|
117
117
|
else:
|
118
118
|
raise NotImplementedError(f"Unsupprted embedding model: {embedding_model_name}")
|
119
119
|
|
120
120
|
# Initialize vector store
|
121
|
+
vector_store_attrs = vector_store_attrs or {}
|
121
122
|
if vector_store_name == "faiss":
|
122
123
|
if "dimension" not in vector_store_attrs:
|
123
124
|
vector_store_attrs["dimension"] = dimension
|
@@ -0,0 +1,2 @@
|
|
1
|
+
Version: 1.10.1
|
2
|
+
Arguments: ['C:\\hostedtoolcache\\windows\\Python\\3.11.9\\x64\\Scripts\\delvewheel', 'repair', '-w', 'wheelhouse', 'D:\\a\\ailoy\\ailoy\\bindings\\python\\dist\\ailoy_py-0.0.3-cp311-cp311-win_amd64.whl', '--add-path', 'D:\\a\\ailoy\\ailoy\\bindings\\python\\build\\_deps\\tvm-build\\Release', '--exclude', 'vulkan-1.dll']
|