ailoy-py 0.0.2__cp312-cp312-macosx_14_0_arm64.whl → 0.0.3__cp312-cp312-macosx_14_0_arm64.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/.dylibs/libomp.dylib +0 -0
- ailoy/.dylibs/libtvm_runtime.dylib +0 -0
- ailoy/__init__.py +2 -1
- ailoy/agent.py +128 -165
- ailoy/ailoy_py.cpython-312-darwin.so +0 -0
- ailoy/mcp.py +60 -48
- ailoy/models/__init__.py +7 -0
- ailoy/models/api_model.py +71 -0
- ailoy/models/local_model.py +44 -0
- ailoy/utils/__init__.py +0 -0
- ailoy/utils/image.py +11 -0
- {ailoy_py-0.0.2.dist-info → ailoy_py-0.0.3.dist-info}/METADATA +4 -3
- ailoy_py-0.0.3.dist-info/RECORD +25 -0
- {ailoy_py-0.0.2.dist-info → ailoy_py-0.0.3.dist-info}/WHEEL +1 -1
- ailoy_py-0.0.2.dist-info/RECORD +0 -20
- {ailoy_py-0.0.2.dist-info → ailoy_py-0.0.3.dist-info}/entry_points.txt +0 -0
ailoy/.dylibs/libomp.dylib
CHANGED
Binary file
|
Binary file
|
ailoy/__init__.py
CHANGED
@@ -16,6 +16,7 @@ if __doc__ is None:
|
|
16
16
|
else: # fallback docstring
|
17
17
|
__doc__ = "# ailoy-py\n\nPython binding for Ailoy runtime APIs"
|
18
18
|
|
19
|
-
from .agent import Agent # noqa: F401
|
19
|
+
from .agent import Agent, AudioContent, BearerAuthenticator, ImageContent, TextContent, ToolAuthenticator # noqa: F401
|
20
|
+
from .models import APIModel, LocalModel # noqa: F401
|
20
21
|
from .runtime import AsyncRuntime, Runtime # noqa: F401
|
21
22
|
from .vector_store import VectorStore # noqa: F401
|
ailoy/agent.py
CHANGED
@@ -1,69 +1,102 @@
|
|
1
|
+
import base64
|
1
2
|
import json
|
2
3
|
import warnings
|
3
4
|
from abc import ABC, abstractmethod
|
4
|
-
from collections.abc import
|
5
|
+
from collections.abc import Callable, Generator
|
5
6
|
from functools import partial
|
6
7
|
from pathlib import Path
|
7
8
|
from typing import (
|
9
|
+
Annotated,
|
8
10
|
Any,
|
9
11
|
Literal,
|
10
12
|
Optional,
|
11
|
-
TypeVar,
|
12
13
|
Union,
|
13
14
|
)
|
14
15
|
from urllib.parse import urlencode, urlparse, urlunparse
|
15
16
|
|
16
17
|
import jmespath
|
17
|
-
from
|
18
|
+
from PIL.Image import Image
|
19
|
+
from pydantic import BaseModel, ConfigDict, Field, TypeAdapter
|
18
20
|
from rich.console import Console
|
19
21
|
from rich.panel import Panel
|
20
22
|
|
21
23
|
from ailoy.ailoy_py import generate_uuid
|
22
24
|
from ailoy.mcp import MCPServer, MCPTool, StdioServerParameters
|
25
|
+
from ailoy.models import APIModel, LocalModel
|
23
26
|
from ailoy.runtime import Runtime
|
24
27
|
from ailoy.tools import DocstringParsingException, TypeHintParsingException, get_json_schema
|
25
|
-
|
26
|
-
__all__ = ["Agent"]
|
28
|
+
from ailoy.utils.image import pillow_image_to_base64
|
27
29
|
|
28
30
|
## Types for internal data structures
|
29
31
|
|
30
32
|
|
31
|
-
class
|
32
|
-
type: Literal["text"]
|
33
|
+
class TextContent(BaseModel):
|
34
|
+
type: Literal["text"] = "text"
|
33
35
|
text: str
|
34
36
|
|
35
37
|
|
38
|
+
class ImageContent(BaseModel):
|
39
|
+
class UrlData(BaseModel):
|
40
|
+
url: str
|
41
|
+
|
42
|
+
type: Literal["image_url"] = "image_url"
|
43
|
+
image_url: UrlData
|
44
|
+
|
45
|
+
@staticmethod
|
46
|
+
def from_url(url: str):
|
47
|
+
return ImageContent(image_url={"url": url})
|
48
|
+
|
49
|
+
@staticmethod
|
50
|
+
def from_pillow(image: Image):
|
51
|
+
return ImageContent(image_url={"url": pillow_image_to_base64(image)})
|
52
|
+
|
53
|
+
|
54
|
+
class AudioContent(BaseModel):
|
55
|
+
class AudioData(BaseModel):
|
56
|
+
data: str
|
57
|
+
format: Literal["mp3", "wav"]
|
58
|
+
|
59
|
+
type: Literal["input_audio"] = "input_audio"
|
60
|
+
input_audio: AudioData
|
61
|
+
|
62
|
+
@staticmethod
|
63
|
+
def from_bytes(data: bytes, format: Literal["mp3", "wav"]):
|
64
|
+
return AudioContent(input_audio={"data": base64.b64encode(data).decode("utf-8"), "format": format})
|
65
|
+
|
66
|
+
|
36
67
|
class FunctionData(BaseModel):
|
37
68
|
class FunctionBody(BaseModel):
|
38
69
|
name: str
|
39
70
|
arguments: Any
|
40
71
|
|
41
|
-
type: Literal["function"]
|
72
|
+
type: Literal["function"] = "function"
|
42
73
|
id: Optional[str] = None
|
43
74
|
function: FunctionBody
|
44
75
|
|
45
76
|
|
46
77
|
class SystemMessage(BaseModel):
|
47
|
-
role: Literal["system"]
|
48
|
-
content: list[
|
78
|
+
role: Literal["system"] = "system"
|
79
|
+
content: str | list[TextContent]
|
49
80
|
|
50
81
|
|
51
82
|
class UserMessage(BaseModel):
|
52
|
-
role: Literal["user"]
|
53
|
-
content: list[
|
83
|
+
role: Literal["user"] = "user"
|
84
|
+
content: str | list[TextContent | ImageContent | AudioContent]
|
54
85
|
|
55
86
|
|
56
87
|
class AssistantMessage(BaseModel):
|
57
|
-
role: Literal["assistant"]
|
58
|
-
|
59
|
-
|
88
|
+
role: Literal["assistant"] = "assistant"
|
89
|
+
content: Optional[str | list[TextContent]] = None
|
90
|
+
name: Optional[str] = None
|
60
91
|
tool_calls: Optional[list[FunctionData]] = None
|
61
92
|
|
93
|
+
# Non-OpenAI fields
|
94
|
+
reasoning: Optional[list[TextContent]] = None
|
95
|
+
|
62
96
|
|
63
97
|
class ToolMessage(BaseModel):
|
64
|
-
role: Literal["tool"]
|
65
|
-
|
66
|
-
content: list[TextData]
|
98
|
+
role: Literal["tool"] = "tool"
|
99
|
+
content: str | list[TextContent]
|
67
100
|
tool_call_id: Optional[str] = None
|
68
101
|
|
69
102
|
|
@@ -76,72 +109,10 @@ Message = Union[
|
|
76
109
|
|
77
110
|
|
78
111
|
class MessageOutput(BaseModel):
|
79
|
-
|
80
|
-
content: Optional[list[TextData]] = None
|
81
|
-
reasoning: Optional[list[TextData]] = None
|
82
|
-
tool_calls: Optional[list[FunctionData]] = None
|
83
|
-
|
84
|
-
message: AssistantMessageDelta
|
112
|
+
message: AssistantMessage
|
85
113
|
finish_reason: Optional[Literal["stop", "tool_calls", "invalid_tool_call", "length", "error"]] = None
|
86
114
|
|
87
115
|
|
88
|
-
## Types for LLM Model Definitions
|
89
|
-
|
90
|
-
TVMModelName = Literal["Qwen/Qwen3-0.6B", "Qwen/Qwen3-1.7B", "Qwen/Qwen3-4B", "Qwen/Qwen3-8B"]
|
91
|
-
OpenAIModelName = Literal["gpt-4o"]
|
92
|
-
ModelName = Union[TVMModelName, OpenAIModelName]
|
93
|
-
|
94
|
-
|
95
|
-
class TVMModel(BaseModel):
|
96
|
-
name: TVMModelName
|
97
|
-
quantization: Optional[Literal["q4f16_1"]] = None
|
98
|
-
mode: Optional[Literal["interactive"]] = None
|
99
|
-
|
100
|
-
|
101
|
-
class OpenAIModel(BaseModel):
|
102
|
-
name: OpenAIModelName
|
103
|
-
api_key: str
|
104
|
-
|
105
|
-
|
106
|
-
class ModelDescription(BaseModel):
|
107
|
-
model_id: str
|
108
|
-
component_type: str
|
109
|
-
default_system_message: Optional[str] = None
|
110
|
-
|
111
|
-
|
112
|
-
model_descriptions: dict[ModelName, ModelDescription] = {
|
113
|
-
"Qwen/Qwen3-0.6B": ModelDescription(
|
114
|
-
model_id="Qwen/Qwen3-0.6B",
|
115
|
-
component_type="tvm_language_model",
|
116
|
-
default_system_message="You are Qwen, created by Alibaba Cloud. You are a helpful assistant.",
|
117
|
-
),
|
118
|
-
"Qwen/Qwen3-1.7B": ModelDescription(
|
119
|
-
model_id="Qwen/Qwen3-1.7B",
|
120
|
-
component_type="tvm_language_model",
|
121
|
-
default_system_message="You are Qwen, created by Alibaba Cloud. You are a helpful assistant.",
|
122
|
-
),
|
123
|
-
"Qwen/Qwen3-4B": ModelDescription(
|
124
|
-
model_id="Qwen/Qwen3-4B",
|
125
|
-
component_type="tvm_language_model",
|
126
|
-
default_system_message="You are Qwen, created by Alibaba Cloud. You are a helpful assistant.",
|
127
|
-
),
|
128
|
-
"Qwen/Qwen3-8B": ModelDescription(
|
129
|
-
model_id="Qwen/Qwen3-8B",
|
130
|
-
component_type="tvm_language_model",
|
131
|
-
default_system_message="You are Qwen, created by Alibaba Cloud. You are a helpful assistant.",
|
132
|
-
),
|
133
|
-
"gpt-4o": ModelDescription(
|
134
|
-
model_id="gpt-4o",
|
135
|
-
component_type="openai",
|
136
|
-
),
|
137
|
-
}
|
138
|
-
|
139
|
-
|
140
|
-
class ComponentState(BaseModel):
|
141
|
-
name: str
|
142
|
-
valid: bool
|
143
|
-
|
144
|
-
|
145
116
|
## Types for agent's responses
|
146
117
|
|
147
118
|
_console = Console(highlight=False, force_jupyter=False, force_terminal=True)
|
@@ -149,7 +120,7 @@ _console = Console(highlight=False, force_jupyter=False, force_terminal=True)
|
|
149
120
|
|
150
121
|
class AgentResponseOutputText(BaseModel):
|
151
122
|
type: Literal["output_text", "reasoning"]
|
152
|
-
role: Literal["assistant"]
|
123
|
+
role: Literal["assistant"] = "assistant"
|
153
124
|
is_type_switched: bool = False
|
154
125
|
content: str
|
155
126
|
|
@@ -160,14 +131,14 @@ class AgentResponseOutputText(BaseModel):
|
|
160
131
|
|
161
132
|
|
162
133
|
class AgentResponseToolCall(BaseModel):
|
163
|
-
type: Literal["tool_call"]
|
164
|
-
role: Literal["assistant"]
|
134
|
+
type: Literal["tool_call"] = "tool_call"
|
135
|
+
role: Literal["assistant"] = "assistant"
|
165
136
|
is_type_switched: bool = False
|
166
137
|
content: FunctionData
|
167
138
|
|
168
139
|
def print(self):
|
169
140
|
title = f"[magenta]Tool Call[/magenta]: [bold]{self.content.function.name}[/bold]"
|
170
|
-
if self.content.id is not None:
|
141
|
+
if self.content.id is not None and len(self.content.id) > 0:
|
171
142
|
title += f" ({self.content.id})"
|
172
143
|
panel = Panel(
|
173
144
|
json.dumps(self.content.function.arguments, indent=2),
|
@@ -178,8 +149,8 @@ class AgentResponseToolCall(BaseModel):
|
|
178
149
|
|
179
150
|
|
180
151
|
class AgentResponseToolResult(BaseModel):
|
181
|
-
type: Literal["tool_call_result"]
|
182
|
-
role: Literal["tool"]
|
152
|
+
type: Literal["tool_call_result"] = "tool_call_result"
|
153
|
+
role: Literal["tool"] = "tool"
|
183
154
|
is_type_switched: bool = False
|
184
155
|
content: ToolMessage
|
185
156
|
|
@@ -194,8 +165,8 @@ class AgentResponseToolResult(BaseModel):
|
|
194
165
|
if len(content) > 500:
|
195
166
|
content = content[:500] + "...(truncated)"
|
196
167
|
|
197
|
-
title =
|
198
|
-
if self.content.tool_call_id is not None:
|
168
|
+
title = "[green]Tool Result[/green]"
|
169
|
+
if self.content.tool_call_id is not None and len(self.content.tool_call_id) > 0:
|
199
170
|
title += f" ({self.content.tool_call_id})"
|
200
171
|
panel = Panel(
|
201
172
|
content,
|
@@ -206,8 +177,8 @@ class AgentResponseToolResult(BaseModel):
|
|
206
177
|
|
207
178
|
|
208
179
|
class AgentResponseError(BaseModel):
|
209
|
-
type: Literal["error"]
|
210
|
-
role: Literal["assistant"]
|
180
|
+
type: Literal["error"] = "error"
|
181
|
+
role: Literal["assistant"] = "assistant"
|
211
182
|
is_type_switched: bool = False
|
212
183
|
content: str
|
213
184
|
|
@@ -308,22 +279,6 @@ class BearerAuthenticator(ToolAuthenticator):
|
|
308
279
|
return {**request, "headers": headers}
|
309
280
|
|
310
281
|
|
311
|
-
T_Retval = TypeVar("T_Retval")
|
312
|
-
|
313
|
-
|
314
|
-
def run_async(coro: Callable[..., Awaitable[T_Retval]]) -> T_Retval:
|
315
|
-
try:
|
316
|
-
import anyio
|
317
|
-
|
318
|
-
# Running outside async loop
|
319
|
-
return anyio.run(lambda: coro)
|
320
|
-
except RuntimeError:
|
321
|
-
import anyio.from_thread
|
322
|
-
|
323
|
-
# Already in a running event loop: use anyio from_thread
|
324
|
-
return anyio.from_thread.run(coro)
|
325
|
-
|
326
|
-
|
327
282
|
class Agent:
|
328
283
|
"""
|
329
284
|
The `Agent` class provides a high-level interface for interacting with large language models (LLMs) in Ailoy.
|
@@ -337,28 +292,22 @@ class Agent:
|
|
337
292
|
def __init__(
|
338
293
|
self,
|
339
294
|
runtime: Runtime,
|
340
|
-
|
295
|
+
model: APIModel | LocalModel,
|
341
296
|
system_message: Optional[str] = None,
|
342
|
-
api_key: Optional[str] = None,
|
343
|
-
**attrs,
|
344
297
|
):
|
345
298
|
"""
|
346
299
|
Create an instance.
|
347
300
|
|
348
301
|
:param runtime: The runtime environment associated with the agent.
|
349
|
-
:param
|
302
|
+
:param model: The model instance.
|
350
303
|
:param system_message: Optional system message to set the initial assistant context.
|
351
|
-
:param api_key: (web agent only) The API key for AI API.
|
352
|
-
:param attrs: Additional initialization parameters (for `define_component` runtime call)
|
353
304
|
:raises ValueError: If model name is not supported or validation fails.
|
354
305
|
"""
|
355
306
|
self._runtime = runtime
|
356
307
|
|
357
308
|
# Initialize component state
|
358
|
-
self.
|
359
|
-
|
360
|
-
valid=False,
|
361
|
-
)
|
309
|
+
self._component_name = generate_uuid()
|
310
|
+
self._component_ready = False
|
362
311
|
|
363
312
|
# Initialize messages
|
364
313
|
self._messages: list[Message] = []
|
@@ -373,7 +322,7 @@ class Agent:
|
|
373
322
|
self._mcp_servers: list[MCPServer] = []
|
374
323
|
|
375
324
|
# Define the component
|
376
|
-
self.define(
|
325
|
+
self.define(model)
|
377
326
|
|
378
327
|
def __del__(self):
|
379
328
|
self.delete()
|
@@ -384,70 +333,55 @@ class Agent:
|
|
384
333
|
def __exit__(self, type, value, traceback):
|
385
334
|
self.delete()
|
386
335
|
|
387
|
-
def define(self,
|
336
|
+
def define(self, model: APIModel | LocalModel) -> None:
|
388
337
|
"""
|
389
338
|
Initializes the agent by defining its model in the runtime.
|
390
339
|
This must be called before running the agent. If already initialized, this is a no-op.
|
391
|
-
:param
|
392
|
-
:param api_key: (web agent only) The API key for AI API.
|
393
|
-
:param attrs: Additional initialization parameters (for `define_component` runtime call)
|
340
|
+
:param model: The model instance.
|
394
341
|
"""
|
395
|
-
if self.
|
342
|
+
if self._component_ready:
|
396
343
|
return
|
397
344
|
|
398
345
|
if not self._runtime.is_alive():
|
399
346
|
raise ValueError("Runtime is currently stopped.")
|
400
347
|
|
401
|
-
if model_name not in model_descriptions:
|
402
|
-
raise ValueError(f"Model `{model_name}` not supported")
|
403
|
-
|
404
|
-
model_desc = model_descriptions[model_name]
|
405
|
-
|
406
|
-
# Add model name into attrs
|
407
|
-
if "model" not in attrs:
|
408
|
-
attrs["model"] = model_desc.model_id
|
409
|
-
|
410
348
|
# Set default system message if not given; still can be None
|
411
349
|
if self._system_message is None:
|
412
|
-
self._system_message =
|
350
|
+
self._system_message = getattr(model, "default_system_message", None)
|
413
351
|
|
414
352
|
self.clear_messages()
|
415
353
|
|
416
|
-
# Add API key
|
417
|
-
if api_key:
|
418
|
-
attrs["api_key"] = api_key
|
419
|
-
|
420
354
|
# Call runtime's define
|
421
355
|
self._runtime.define(
|
422
|
-
|
423
|
-
self.
|
424
|
-
|
356
|
+
model.component_type,
|
357
|
+
self._component_name,
|
358
|
+
model.to_attrs(),
|
425
359
|
)
|
426
360
|
|
427
361
|
# Mark as defined
|
428
|
-
self.
|
362
|
+
self._component_ready = True
|
429
363
|
|
430
364
|
def delete(self) -> None:
|
431
365
|
"""
|
432
366
|
Deinitializes the agent and releases resources in the runtime.
|
433
367
|
This should be called when the agent is no longer needed. If already deinitialized, this is a no-op.
|
434
368
|
"""
|
435
|
-
if not self.
|
369
|
+
if not self._component_ready:
|
436
370
|
return
|
437
371
|
|
438
372
|
if self._runtime.is_alive():
|
439
|
-
self._runtime.delete(self.
|
373
|
+
self._runtime.delete(self._component_name)
|
440
374
|
|
441
375
|
self.clear_messages()
|
442
376
|
|
443
377
|
for mcp_server in self._mcp_servers:
|
444
378
|
mcp_server.cleanup()
|
445
379
|
|
446
|
-
self.
|
380
|
+
self._component_ready = False
|
447
381
|
|
448
382
|
def query(
|
449
383
|
self,
|
450
|
-
message: str,
|
384
|
+
message: str | list[str | Image | dict | TextContent | ImageContent | AudioContent],
|
451
385
|
reasoning: bool = False,
|
452
386
|
) -> Generator[AgentResponse, None, None]:
|
453
387
|
"""
|
@@ -458,13 +392,36 @@ class Agent:
|
|
458
392
|
:return: An iterator over the output, where each item represents either a generated token from the assistant or a tool call.
|
459
393
|
:rtype: Iterator[:class:`AgentResponse`]
|
460
394
|
""" # noqa: E501
|
461
|
-
if not self.
|
395
|
+
if not self._component_ready:
|
462
396
|
raise ValueError("Agent is not valid. Create one or define newly.")
|
463
397
|
|
464
398
|
if not self._runtime.is_alive():
|
465
399
|
raise ValueError("Runtime is currently stopped.")
|
466
400
|
|
467
|
-
|
401
|
+
if isinstance(message, str):
|
402
|
+
self._messages.append(UserMessage(content=[TextContent(text=message)]))
|
403
|
+
elif isinstance(message, list):
|
404
|
+
if len(message) == 0:
|
405
|
+
raise ValueError("Message is empty")
|
406
|
+
|
407
|
+
contents = []
|
408
|
+
for content in message:
|
409
|
+
if isinstance(content, str):
|
410
|
+
contents.append(TextContent(text=content))
|
411
|
+
elif isinstance(content, Image):
|
412
|
+
contents.append(ImageContent.from_pillow(image=content))
|
413
|
+
elif isinstance(content, dict):
|
414
|
+
ta: TypeAdapter[TextContent | ImageContent | AudioContent] = TypeAdapter(
|
415
|
+
Annotated[TextContent | ImageContent | AudioContent, Field(discriminator="type")]
|
416
|
+
)
|
417
|
+
validated_content = ta.validate_python(content)
|
418
|
+
contents.append(validated_content)
|
419
|
+
else:
|
420
|
+
contents.append(content)
|
421
|
+
|
422
|
+
self._messages.append(UserMessage(content=contents))
|
423
|
+
else:
|
424
|
+
raise ValueError(f"Invalid message type: {type(message)}")
|
468
425
|
|
469
426
|
prev_resp_type = None
|
470
427
|
|
@@ -480,7 +437,7 @@ class Agent:
|
|
480
437
|
assistant_content = None
|
481
438
|
assistant_tool_calls = None
|
482
439
|
finish_reason = ""
|
483
|
-
for result in self._runtime.call_iter_method(self.
|
440
|
+
for result in self._runtime.call_iter_method(self._component_name, "infer", infer_args):
|
484
441
|
msg = MessageOutput.model_validate(result)
|
485
442
|
|
486
443
|
if msg.message.reasoning:
|
@@ -491,13 +448,16 @@ class Agent:
|
|
491
448
|
assistant_reasoning[0].text += v.text
|
492
449
|
resp = AgentResponseOutputText(
|
493
450
|
type="reasoning",
|
494
|
-
role="assistant",
|
495
451
|
is_type_switched=(prev_resp_type != "reasoning"),
|
496
452
|
content=v.text,
|
497
453
|
)
|
498
454
|
prev_resp_type = resp.type
|
499
455
|
yield resp
|
500
|
-
if msg.message.content:
|
456
|
+
if msg.message.content is not None:
|
457
|
+
# Canonicalize message content to the array of TextContent
|
458
|
+
if isinstance(msg.message.content, str):
|
459
|
+
msg.message.content = [TextContent(text=msg.message.content)]
|
460
|
+
|
501
461
|
for v in msg.message.content:
|
502
462
|
if not assistant_content:
|
503
463
|
assistant_content = [v]
|
@@ -505,7 +465,6 @@ class Agent:
|
|
505
465
|
assistant_content[0].text += v.text
|
506
466
|
resp = AgentResponseOutputText(
|
507
467
|
type="output_text",
|
508
|
-
role="assistant",
|
509
468
|
is_type_switched=(prev_resp_type != "output_text"),
|
510
469
|
content=v.text,
|
511
470
|
)
|
@@ -518,8 +477,6 @@ class Agent:
|
|
518
477
|
else:
|
519
478
|
assistant_tool_calls.append(v)
|
520
479
|
resp = AgentResponseToolCall(
|
521
|
-
type="tool_call",
|
522
|
-
role="assistant",
|
523
480
|
is_type_switched=True,
|
524
481
|
content=v,
|
525
482
|
)
|
@@ -532,7 +489,6 @@ class Agent:
|
|
532
489
|
# Append output
|
533
490
|
self._messages.append(
|
534
491
|
AssistantMessage(
|
535
|
-
role="assistant",
|
536
492
|
reasoning=assistant_reasoning,
|
537
493
|
content=assistant_content,
|
538
494
|
tool_calls=assistant_tool_calls,
|
@@ -550,18 +506,14 @@ class Agent:
|
|
550
506
|
raise RuntimeError("Tool not found")
|
551
507
|
tool_result = tool_.call(**tool_call.function.arguments)
|
552
508
|
return ToolMessage(
|
553
|
-
|
554
|
-
|
555
|
-
content=[TextData(type="text", text=json.dumps(tool_result))],
|
556
|
-
tool_call_id=tool_call.id if tool_call.id else None,
|
509
|
+
content=[TextContent(text=json.dumps(tool_result))],
|
510
|
+
tool_call_id=tool_call.id,
|
557
511
|
)
|
558
512
|
|
559
513
|
tool_call_results = [run_tool(tc) for tc in assistant_tool_calls]
|
560
514
|
for result_msg in tool_call_results:
|
561
515
|
self._messages.append(result_msg)
|
562
516
|
resp = AgentResponseToolResult(
|
563
|
-
type="tool_call_result",
|
564
|
-
role="tool",
|
565
517
|
is_type_switched=True,
|
566
518
|
content=result_msg,
|
567
519
|
)
|
@@ -571,6 +523,7 @@ class Agent:
|
|
571
523
|
continue
|
572
524
|
|
573
525
|
# Finish this generator
|
526
|
+
yield AgentResponseOutputText(type="output_text", content="\n")
|
574
527
|
break
|
575
528
|
|
576
529
|
def get_messages(self) -> list[Message]:
|
@@ -589,9 +542,7 @@ class Agent:
|
|
589
542
|
"""
|
590
543
|
self._messages.clear()
|
591
544
|
if self._system_message is not None:
|
592
|
-
self._messages.append(
|
593
|
-
SystemMessage(role="system", content=[TextData(type="text", text=self._system_message)])
|
594
|
-
)
|
545
|
+
self._messages.append(SystemMessage(role="system", content=[TextContent(text=self._system_message)]))
|
595
546
|
|
596
547
|
def print(self, resp: AgentResponse):
|
597
548
|
resp.print()
|
@@ -779,7 +730,7 @@ class Agent:
|
|
779
730
|
continue
|
780
731
|
|
781
732
|
desc = ToolDescription(
|
782
|
-
name=f"{name}
|
733
|
+
name=f"{name}-{tool.name}", description=tool.description, parameters=tool.inputSchema
|
783
734
|
)
|
784
735
|
|
785
736
|
def call(tool: MCPTool, **inputs: dict[str, Any]) -> list[str]:
|
@@ -803,4 +754,16 @@ class Agent:
|
|
803
754
|
mcp_server.cleanup()
|
804
755
|
|
805
756
|
# Remove tools registered from the MCP server
|
806
|
-
self._tools = list(filter(lambda t: not t.desc.name.startswith(f"{mcp_server.name}
|
757
|
+
self._tools = list(filter(lambda t: not t.desc.name.startswith(f"{mcp_server.name}-"), self._tools))
|
758
|
+
|
759
|
+
def get_tools(self):
|
760
|
+
"""
|
761
|
+
Get the list of registered tools.
|
762
|
+
"""
|
763
|
+
return self._tools
|
764
|
+
|
765
|
+
def clear_tools(self):
|
766
|
+
"""
|
767
|
+
Clear the registered tools.
|
768
|
+
"""
|
769
|
+
self._tools.clear()
|
Binary file
|
ailoy/mcp.py
CHANGED
@@ -2,7 +2,7 @@ import asyncio
|
|
2
2
|
import json
|
3
3
|
import multiprocessing
|
4
4
|
import platform
|
5
|
-
import
|
5
|
+
import tempfile
|
6
6
|
from multiprocessing.connection import Connection
|
7
7
|
from typing import Annotated, Any, Literal, Union
|
8
8
|
|
@@ -13,6 +13,7 @@ from mcp.client.stdio import (
|
|
13
13
|
StdioServerParameters,
|
14
14
|
stdio_client,
|
15
15
|
)
|
16
|
+
from mcp.shared.exceptions import McpError
|
16
17
|
from pydantic import BaseModel, Field, TypeAdapter
|
17
18
|
|
18
19
|
__all__ = ["MCPServer"]
|
@@ -73,11 +74,15 @@ class MCPServer:
|
|
73
74
|
self._parent_conn, self._child_conn = multiprocessing.Pipe()
|
74
75
|
|
75
76
|
ctx = multiprocessing.get_context("fork" if platform.system() != "Windows" else "spawn")
|
76
|
-
self._proc = ctx.Process(target=self._run_process, args=(self._child_conn,))
|
77
|
+
self._proc: multiprocessing.Process = ctx.Process(target=self._run_process, args=(self._child_conn,))
|
77
78
|
self._proc.start()
|
78
79
|
|
79
80
|
# Wait for subprocess to signal initialization complete
|
80
|
-
|
81
|
+
try:
|
82
|
+
self._recv_response()
|
83
|
+
except RuntimeError as e:
|
84
|
+
self.cleanup()
|
85
|
+
raise e
|
81
86
|
|
82
87
|
def __del__(self):
|
83
88
|
self.cleanup()
|
@@ -86,52 +91,59 @@ class MCPServer:
|
|
86
91
|
asyncio.run(self._process_main(conn))
|
87
92
|
|
88
93
|
async def _process_main(self, conn: Connection):
|
89
|
-
|
90
|
-
async with
|
91
|
-
|
92
|
-
|
93
|
-
await session.initialize()
|
94
|
-
conn.send(ResultMessage(result=True).model_dump())
|
95
|
-
except Exception as e:
|
96
|
-
conn.send(ErrorMessage(error=f"Failed to initialize MCP subprocess: {e}").model_dump())
|
97
|
-
|
98
|
-
while True:
|
99
|
-
if not conn.poll(0.1):
|
100
|
-
await asyncio.sleep(0.1)
|
101
|
-
continue
|
102
|
-
|
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
|
103
98
|
try:
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
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())
|
135
147
|
|
136
148
|
def _send_request(self, msg: RequestMessage):
|
137
149
|
self._parent_conn.send(msg.model_dump())
|
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/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}"
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: ailoy-py
|
3
|
-
Version: 0.0.
|
3
|
+
Version: 0.0.3
|
4
4
|
Summary: Python binding for Ailoy runtime APIs
|
5
5
|
Author-Email: "Brekkylab Inc." <contact@brekkylab.com>
|
6
6
|
License-Expression: Apache-2.0
|
@@ -18,6 +18,7 @@ Requires-Dist: anyio>=4.9.0
|
|
18
18
|
Requires-Dist: jmespath>=1.0.1
|
19
19
|
Requires-Dist: mcp>=1.8.0
|
20
20
|
Requires-Dist: numpy>=2.0.2
|
21
|
+
Requires-Dist: pillow>=11.2.1
|
21
22
|
Requires-Dist: pydantic>=2.11.4
|
22
23
|
Requires-Dist: rich>=14.0.0
|
23
24
|
Requires-Dist: typer>=0.15.4
|
@@ -38,14 +39,14 @@ pip install ailoy-py
|
|
38
39
|
## Quickstart
|
39
40
|
|
40
41
|
```python
|
41
|
-
from ailoy import Runtime, Agent
|
42
|
+
from ailoy import Runtime, Agent, LocalModel
|
42
43
|
|
43
44
|
# The runtime must be started to use Ailoy
|
44
45
|
rt = Runtime()
|
45
46
|
|
46
47
|
# Defines an agent
|
47
48
|
# During this step, the model parameters are downloaded and the LLM is set up for execution
|
48
|
-
with Agent(rt,
|
49
|
+
with Agent(rt, LocalModel("Qwen/Qwen3-0.6B")) as agent:
|
49
50
|
# This is where the actual LLM call happens
|
50
51
|
for resp in agent.query("Please give me a short poem about AI"):
|
51
52
|
agent.print(resp)
|
@@ -0,0 +1,25 @@
|
|
1
|
+
ailoy/ailoy_py.cpython-312-darwin.so,sha256=ibnFI77OPkrq4X0SDy2FiB04pEtWHQ-av5oi-LaGyr8,15149856
|
2
|
+
ailoy/ailoy_py.pyi,sha256=Yf90FEXkslpCpr1r2eqQ3-_1jLo65zmG94bBXDRqinU,991
|
3
|
+
ailoy/vector_store.py,sha256=ZfIuGYKv2dQmjOuDlSKDc-BBPlQ8no_70mZwnPzbBzo,7515
|
4
|
+
ailoy/tools.py,sha256=RnTfmWlqYY1q0V377CpAAyAK-yET7k45GgEhgM9G8eI,8207
|
5
|
+
ailoy/__init__.py,sha256=INeFsnHtrqTwkMKUPSdD_9l8DhudnW574e9rsRtrMJc,783
|
6
|
+
ailoy/runtime.py,sha256=-75KawEMQSwxGvX5wtECVCWiTNdcHojsQ1e-OVB4IQ8,10545
|
7
|
+
ailoy/mcp.py,sha256=wVzXfwUh4UcU60PYq17kCFG7ZClmEDBRt8LxetuSkns,6800
|
8
|
+
ailoy/agent.py,sha256=F15s3FzyrQ3pfNb0ZCLOPV2mrFDNCcFbYFP02hEbAk4,26780
|
9
|
+
ailoy/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
10
|
+
ailoy/utils/image.py,sha256=zkufequcQVTmIkArreYUyB8r2nrcOL_8O6KOv5B-yis,288
|
11
|
+
ailoy/models/__init__.py,sha256=1AtlJV9gYThw_3snu0jPEH_aQGI74ip7ZcVJLtN5nMU,117
|
12
|
+
ailoy/models/local_model.py,sha256=Iyur0UEUSbLKzptx9croP_OAF-qh9S-ZDukDthHNz9w,1206
|
13
|
+
ailoy/models/api_model.py,sha256=_-2TSunBh8kZQOF1CkQXBKi81XNClPWH4U5hQA7ioQ4,1787
|
14
|
+
ailoy/cli/model.py,sha256=cerCHE-VY9TOwqRcLBtmqnV-5vphpvyhtrfPFZiTKCM,2979
|
15
|
+
ailoy/cli/__main__.py,sha256=HnBVb2em1F2NLPeNX5r3xRndRrnGaXVCduo8WBULAI0,179
|
16
|
+
ailoy/.dylibs/libomp.dylib,sha256=s963nLMIJ2lfA448nbin9lL52fnoeGySlgLfm4Y3HHs,735616
|
17
|
+
ailoy/.dylibs/libtvm_runtime.dylib,sha256=Fqp5JsiIMAsS7p94oDdxY_-kAqbwGpBGpkS2LhYfhZs,4620384
|
18
|
+
ailoy/presets/tools/tmdb.json,sha256=UGLN5uAJ2b-Hu3nLcW95WXDLB3mfC3rBYfQANp_e8Ps,7046
|
19
|
+
ailoy/presets/tools/calculator.json,sha256=ePnZsjZChnvS08s9eVdIp4Bys_PlJBXPHCCjv6oMvzA,1040
|
20
|
+
ailoy/presets/tools/nytimes.json,sha256=wrfe9bnAlSPzHladoGEX2oCAeE0wed3BvgXQ_Z2PdXg,918
|
21
|
+
ailoy/presets/tools/frankfurter.json,sha256=bZ5vhszf_aR-B_QN4L2xrI5nR-f4AMZk41UUDq1dTXg,1152
|
22
|
+
ailoy_py-0.0.3.dist-info/RECORD,,
|
23
|
+
ailoy_py-0.0.3.dist-info/WHEEL,sha256=QOe84RDUunyjg7JwgI5YbDt4ixoQu4ZhxxlOWcSiCqI,141
|
24
|
+
ailoy_py-0.0.3.dist-info/entry_points.txt,sha256=gVG45uDE6kef0wm6SEMYSgZgRNNRhSAeP2n2lPR00dI,50
|
25
|
+
ailoy_py-0.0.3.dist-info/METADATA,sha256=ZvX6pFs2Fb3uBmm_73Cq3rtYs52pfjd5_cEVZSluK68,2053
|
ailoy_py-0.0.2.dist-info/RECORD
DELETED
@@ -1,20 +0,0 @@
|
|
1
|
-
ailoy/ailoy_py.cpython-312-darwin.so,sha256=6G8_ANcUX79PwlaN3G9cglXLeE-2zAdP96xAJVmBVx4,14964576
|
2
|
-
ailoy/ailoy_py.pyi,sha256=Yf90FEXkslpCpr1r2eqQ3-_1jLo65zmG94bBXDRqinU,991
|
3
|
-
ailoy/vector_store.py,sha256=ZfIuGYKv2dQmjOuDlSKDc-BBPlQ8no_70mZwnPzbBzo,7515
|
4
|
-
ailoy/tools.py,sha256=RnTfmWlqYY1q0V377CpAAyAK-yET7k45GgEhgM9G8eI,8207
|
5
|
-
ailoy/__init__.py,sha256=mzkLUc95OCc2okURWm9iA5xR8WZdxwvPgaanc9fwoH4,647
|
6
|
-
ailoy/runtime.py,sha256=-75KawEMQSwxGvX5wtECVCWiTNdcHojsQ1e-OVB4IQ8,10545
|
7
|
-
ailoy/mcp.py,sha256=bC58tAWqhvMdZVCKHSdOVNUoAuYfZiou1hSH1oa_9Ag,6190
|
8
|
-
ailoy/agent.py,sha256=uQ1o4CjQEO1vP7y0frGtVknN-A_iLNtkqt8h3vobgiM,27613
|
9
|
-
ailoy/cli/model.py,sha256=cerCHE-VY9TOwqRcLBtmqnV-5vphpvyhtrfPFZiTKCM,2979
|
10
|
-
ailoy/cli/__main__.py,sha256=HnBVb2em1F2NLPeNX5r3xRndRrnGaXVCduo8WBULAI0,179
|
11
|
-
ailoy/.dylibs/libomp.dylib,sha256=GX9Rol1LBJAoha7wZJMQxuCUi_o41Yo5QLBTMN85rRI,735616
|
12
|
-
ailoy/.dylibs/libtvm_runtime.dylib,sha256=uEIIGsPZAY0SSfGtqSeyHljo2QsjBrd3CSGZSpwPwd8,3678320
|
13
|
-
ailoy/presets/tools/tmdb.json,sha256=UGLN5uAJ2b-Hu3nLcW95WXDLB3mfC3rBYfQANp_e8Ps,7046
|
14
|
-
ailoy/presets/tools/calculator.json,sha256=ePnZsjZChnvS08s9eVdIp4Bys_PlJBXPHCCjv6oMvzA,1040
|
15
|
-
ailoy/presets/tools/nytimes.json,sha256=wrfe9bnAlSPzHladoGEX2oCAeE0wed3BvgXQ_Z2PdXg,918
|
16
|
-
ailoy/presets/tools/frankfurter.json,sha256=bZ5vhszf_aR-B_QN4L2xrI5nR-f4AMZk41UUDq1dTXg,1152
|
17
|
-
ailoy_py-0.0.2.dist-info/RECORD,,
|
18
|
-
ailoy_py-0.0.2.dist-info/WHEEL,sha256=eIeGDHdBM78ikRuuQz8ZKfrdHQwszxUfpZIUvykpuZI,141
|
19
|
-
ailoy_py-0.0.2.dist-info/entry_points.txt,sha256=gVG45uDE6kef0wm6SEMYSgZgRNNRhSAeP2n2lPR00dI,50
|
20
|
-
ailoy_py-0.0.2.dist-info/METADATA,sha256=B5RbxeITquJfdiw9bhA6w02q9OvLkuFH7jMRg6Lxc2A,2010
|
File without changes
|