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.
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()
@@ -0,0 +1,7 @@
1
+ from .api_model import APIModel
2
+ from .local_model import LocalModel
3
+
4
+ __all__ = [
5
+ "APIModel",
6
+ "LocalModel",
7
+ ]
@@ -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
- def __init__(self, address: str = "inproc://"):
19
- self.address: str = address
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
- start_threads(self.address)
25
- self._client: BrokerClient = BrokerClient(address)
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._client:
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
- stop_threads(self.address)
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 _send_type1(self, ptype: Literal["connect", "disconnect"]) -> Optional[str]:
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
- if self._client.send_type1(txid, ptype):
58
- return txid
59
- raise RuntimeError("Failed to send packet")
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.send_type2(txid, ptype, status, *args):
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, address: str = "inproc://"):
116
- super().__init__(address)
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, address: str = "inproc://"):
197
- super().__init__(address)
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}
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] = dict(),
50
- vector_store_attrs: dict[str, Any] = dict(),
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] = dict(),
94
- vector_store_attrs: dict[str, Any] = dict(),
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']