ailoy-py 0.2.4__cp310-abi3-manylinux_2_28_x86_64.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 ADDED
@@ -0,0 +1,5 @@
1
+ # ruff: noqa: F401
2
+ # ruff: noqa: I001
3
+
4
+ from ._core import * # noqa: F403
5
+ import ailoy._patches
ailoy/_core.abi3.so ADDED
Binary file
ailoy/_core.pyi ADDED
@@ -0,0 +1,918 @@
1
+ # This file is automatically generated by pyo3_stub_gen
2
+ # ruff: noqa: E501, F401
3
+
4
+ import builtins
5
+ import enum
6
+ import typing
7
+
8
+ @typing.final
9
+ class Agent:
10
+ r"""
11
+ The Agent is the central orchestrator that connects the **language model**, **tools**, and **knowledge** components.
12
+ It manages the entire reasoning and action loop, coordinating how each subsystem contributes to the final response.
13
+
14
+ In essence, the Agent:
15
+
16
+ - Understands user input
17
+ - Interprets structured responses from the language model (such as tool calls)
18
+ - Executes tools as needed
19
+ - Retrieves and integrates contextual knowledge before or during inference
20
+
21
+ # Public APIs
22
+ - `run_delta`: Runs a user query and streams incremental deltas (partial outputs)
23
+ - `run`: Runs a user query and returns a complete message once all deltas are accumulated
24
+
25
+ ## Delta vs. Complete Message
26
+ A *delta* represents a partial piece of model output, such as a text fragment or intermediate reasoning step.
27
+ Deltas can be accumulated into a full message using the provided accumulation utilities.
28
+ This allows real-time streaming while preserving the ability to reconstruct the final structured result.
29
+
30
+ See `MessageDelta`.
31
+
32
+ # Components
33
+ - **Language Model**: Generates natural language and structured outputs. It interprets the conversation context and predicts the assistant’s next action.
34
+ - **Tool**: Represents external functions or APIs that the model can dynamically invoke. The `Agent` detects tool calls and automatically executes them during the reasoning loop.
35
+ - **Knowledge**: Provides retrieval-augmented reasoning by fetching relevant information from stored documents or databases. When available, the `Agent` enriches model input with these results before generating an answer.
36
+ """
37
+ @property
38
+ def lm(self) -> LangModel: ...
39
+ @property
40
+ def tools(self) -> builtins.list[Tool]: ...
41
+ def __new__(cls, lm: LangModel, tools: typing.Optional[typing.Sequence[Tool]] = None, knowledge: typing.Optional[Knowledge] = None) -> Agent: ...
42
+ def __repr__(self) -> builtins.str: ...
43
+ def add_tools(self, tools: typing.Sequence[Tool]) -> None: ...
44
+ def add_tool(self, tool: Tool) -> None: ...
45
+ def remove_tools(self, tool_names: typing.Sequence[builtins.str]) -> None: ...
46
+ def remove_tool(self, tool_name: builtins.str) -> None: ...
47
+ def clear_tools(self) -> None: ...
48
+ def set_knowledge(self, knowledge: Knowledge) -> None: ...
49
+ def remove_knowledge(self) -> None: ...
50
+ def run_delta(self, messages: str | list[Message], config: typing.Optional[AgentConfig] = None) -> MessageDeltaOutputIterator: ...
51
+ def run_delta_sync(self, messages: str | list[Message], config: typing.Optional[AgentConfig] = None) -> MessageDeltaOutputSyncIterator: ...
52
+ def run(self, messages: str | list[Message], config: typing.Optional[AgentConfig] = None) -> MessageOutputIterator: ...
53
+ def run_sync(self, messages: str | list[Message], config: typing.Optional[AgentConfig] = None) -> MessageOutputSyncIterator: ...
54
+
55
+ @typing.final
56
+ class AgentConfig:
57
+ r"""
58
+ Configuration for running the agent.
59
+
60
+ See `LangModelInferConfig` and `KnowledgeConfig` for more details.
61
+ """
62
+ @property
63
+ def inference(self) -> typing.Optional[LangModelInferConfig]: ...
64
+ @inference.setter
65
+ def inference(self, value: typing.Optional[LangModelInferConfig]) -> None: ...
66
+ @property
67
+ def knowledge(self) -> typing.Optional[KnowledgeConfig]: ...
68
+ @knowledge.setter
69
+ def knowledge(self, value: typing.Optional[KnowledgeConfig]) -> None: ...
70
+ def __new__(cls, inference: typing.Optional[LangModelInferConfig] = None, knowledge: typing.Optional[KnowledgeConfig] = None) -> AgentConfig: ...
71
+ @classmethod
72
+ def from_dict(cls, config: dict) -> AgentConfig: ...
73
+
74
+ @typing.final
75
+ class CacheProgress:
76
+ @property
77
+ def comment(self) -> builtins.str: ...
78
+ @property
79
+ def current(self) -> builtins.int: ...
80
+ @property
81
+ def total(self) -> builtins.int: ...
82
+ def __repr__(self) -> builtins.str: ...
83
+
84
+ @typing.final
85
+ class Document:
86
+ @property
87
+ def id(self) -> builtins.str: ...
88
+ @property
89
+ def title(self) -> typing.Optional[builtins.str]: ...
90
+ @property
91
+ def text(self) -> builtins.str: ...
92
+ def __eq__(self, other: builtins.object) -> builtins.bool: ...
93
+ def __new__(cls, id: builtins.str, text: builtins.str, title: typing.Optional[builtins.str] = None) -> Document: ...
94
+
95
+ @typing.final
96
+ class DocumentPolyfill:
97
+ r"""
98
+ Provides a polyfill for LLMs that do not natively support the Document feature.
99
+ """
100
+ @property
101
+ def system_message_template(self) -> typing.Optional[builtins.str]: ...
102
+ @system_message_template.setter
103
+ def system_message_template(self, value: typing.Optional[builtins.str]) -> None: ...
104
+ @property
105
+ def query_message_template(self) -> typing.Optional[builtins.str]: ...
106
+ @query_message_template.setter
107
+ def query_message_template(self, value: typing.Optional[builtins.str]) -> None: ...
108
+ def __new__(cls, system_message_template: typing.Optional[builtins.str] = None, query_message_template: typing.Optional[builtins.str] = None) -> DocumentPolyfill: ...
109
+ @classmethod
110
+ def get(cls, kind: typing.Literal["Qwen3"]) -> DocumentPolyfill: ...
111
+
112
+ @typing.final
113
+ class EmbeddingModel:
114
+ @classmethod
115
+ def new_local(cls, model_name: builtins.str, device_id: typing.Optional[builtins.int] = None, validate_checksum: typing.Optional[builtins.bool] = None, progress_callback: typing.Callable[[CacheProgress], None] = None) -> typing.Awaitable[EmbeddingModel]: ...
116
+ @classmethod
117
+ def new_local_sync(cls, model_name: builtins.str, device_id: typing.Optional[builtins.int] = None, validate_checksum: typing.Optional[builtins.bool] = None, progress_callback: typing.Callable[[CacheProgress], None] = None) -> EmbeddingModel: ...
118
+ @classmethod
119
+ def download(cls, model_name: builtins.str, progress_callback: typing.Callable[[CacheProgress], None] = None) -> None: ...
120
+ @classmethod
121
+ def remove(cls, model_name: builtins.str) -> None: ...
122
+ async def infer(self, text: builtins.str) -> builtins.list[float]: ...
123
+ def infer_sync(self, text: builtins.str) -> builtins.list[float]: ...
124
+
125
+ class Grammar:
126
+ @typing.final
127
+ class Plain(Grammar):
128
+ __match_args__ = ()
129
+ def __new__(cls) -> Grammar.Plain: ...
130
+
131
+ @typing.final
132
+ class JSON(Grammar):
133
+ __match_args__ = ()
134
+ def __new__(cls) -> Grammar.JSON: ...
135
+
136
+ @typing.final
137
+ class JSONSchema(Grammar):
138
+ __match_args__ = ("schema",)
139
+ @property
140
+ def schema(self) -> builtins.str: ...
141
+ def __new__(cls, schema: builtins.str) -> Grammar.JSONSchema: ...
142
+
143
+ @typing.final
144
+ class Regex(Grammar):
145
+ __match_args__ = ("regex",)
146
+ @property
147
+ def regex(self) -> builtins.str: ...
148
+ def __new__(cls, regex: builtins.str) -> Grammar.Regex: ...
149
+
150
+ @typing.final
151
+ class CFG(Grammar):
152
+ __match_args__ = ("cfg",)
153
+ @property
154
+ def cfg(self) -> builtins.str: ...
155
+ def __new__(cls, cfg: builtins.str) -> Grammar.CFG: ...
156
+
157
+ ...
158
+
159
+ @typing.final
160
+ class KVCacheConfig:
161
+ @property
162
+ def context_window_size(self) -> typing.Optional[builtins.int]: ...
163
+ @context_window_size.setter
164
+ def context_window_size(self, value: typing.Optional[builtins.int]) -> None: ...
165
+ @property
166
+ def prefill_chunk_size(self) -> typing.Optional[builtins.int]: ...
167
+ @prefill_chunk_size.setter
168
+ def prefill_chunk_size(self, value: typing.Optional[builtins.int]) -> None: ...
169
+ @property
170
+ def sliding_window_size(self) -> typing.Optional[builtins.int]: ...
171
+ @sliding_window_size.setter
172
+ def sliding_window_size(self, value: typing.Optional[builtins.int]) -> None: ...
173
+ def __new__(cls, context_window_size: typing.Optional[builtins.int], prefill_chunk_size: typing.Optional[builtins.int], sliding_window_size: typing.Optional[builtins.int]) -> KVCacheConfig: ...
174
+
175
+ @typing.final
176
+ class Knowledge:
177
+ @classmethod
178
+ def new_vector_store(cls, store: VectorStore, embedding_model: EmbeddingModel) -> Knowledge: ...
179
+ async def retrieve(self, query: builtins.str, config: KnowledgeConfig) -> builtins.list[Document]: ...
180
+ def as_tool(self) -> Tool: ...
181
+
182
+ @typing.final
183
+ class KnowledgeConfig:
184
+ @property
185
+ def top_k(self) -> typing.Optional[builtins.int]: ...
186
+ @top_k.setter
187
+ def top_k(self, value: typing.Optional[builtins.int]) -> None: ...
188
+ def __new__(cls, top_k: typing.Optional[builtins.int] = None) -> KnowledgeConfig: ...
189
+ @classmethod
190
+ def from_dict(cls, config: dict) -> KnowledgeConfig: ...
191
+
192
+ @typing.final
193
+ class LangModel:
194
+ @classmethod
195
+ def new_local(cls, model_name: builtins.str, device_id: typing.Optional[builtins.int] = None, validate_checksum: typing.Optional[builtins.bool] = None, kv_cache: typing.Optional[KVCacheConfig] = None, progress_callback: typing.Callable[[CacheProgress], None] = None) -> typing.Awaitable[LangModel]: ...
196
+ @classmethod
197
+ def new_local_sync(cls, model_name: builtins.str, device_id: typing.Optional[builtins.int] = None, validate_checksum: typing.Optional[builtins.bool] = None, kv_cache: typing.Optional[KVCacheConfig] = None, progress_callback: typing.Callable[[CacheProgress], None] = None) -> LangModel: ...
198
+ @classmethod
199
+ def new_stream_api(cls, spec: typing.Literal["ChatCompletion", "OpenAI", "Gemini", "Claude", "Responses", "Grok"], model_name: builtins.str, api_key: builtins.str) -> LangModel: ...
200
+ @classmethod
201
+ def download(cls, model_name: builtins.str, progress_callback: typing.Callable[[CacheProgress], None] = None) -> None: ...
202
+ @classmethod
203
+ def remove(cls, model_name: builtins.str) -> None: ...
204
+ def infer_delta(self, messages: str | list[Message], tools: typing.Optional[typing.Sequence[ToolDesc]] = None, documents: typing.Optional[typing.Sequence[Document]] = None, config: typing.Optional[LangModelInferConfig] = None) -> MessageDeltaOutputIterator: ...
205
+ def infer_delta_sync(self, messages: str | list[Message], tools: typing.Optional[typing.Sequence[ToolDesc]] = None, documents: typing.Optional[typing.Sequence[Document]] = None, config: typing.Optional[LangModelInferConfig] = None) -> MessageDeltaOutputSyncIterator: ...
206
+ def infer(self, messages: str | list[Message], tools: typing.Optional[typing.Sequence[ToolDesc]] = None, documents: typing.Optional[typing.Sequence[Document]] = None, config: typing.Optional[LangModelInferConfig] = None) -> typing.Awaitable[MessageOutput]: ...
207
+ def infer_sync(self, messages: str | list[Message], tools: typing.Optional[typing.Sequence[ToolDesc]] = None, documents: typing.Optional[typing.Sequence[Document]] = None, config: typing.Optional[LangModelInferConfig] = None) -> MessageOutput: ...
208
+ def __repr__(self) -> builtins.str: ...
209
+
210
+ @typing.final
211
+ class LangModelInferConfig:
212
+ r"""
213
+ Configuration parameters that control the behavior of language model inference.
214
+
215
+ # Fields
216
+
217
+ ## `document_polyfill`
218
+ Configuration describing how retrieved documents are embedded into the model input.
219
+ If `None`, it does not perform any polyfill, (ignoring documents).
220
+
221
+ ## `think_effort`
222
+ Controls the model's reasoning intensity.
223
+ In local models, `low`, `medium`, `high` is ignored.
224
+ In API models, it is up to it's API. See API parameters.
225
+
226
+ Possible values: `disable`, `enable`, `low`, `medium`, `high`.
227
+
228
+ ## `temperature`
229
+ Sampling temperature controlling randomness of output.
230
+ Lower values make output more deterministic; higher values increase diversity.
231
+
232
+ ## `top_p`
233
+ Nucleus sampling parameter (probability mass cutoff).
234
+ Limits token sampling to a cumulative probability ≤ `top_p`.`
235
+
236
+ ## `max_tokens`
237
+ Maximum number of tokens to generate for a single inference.
238
+
239
+ ## `grammar`
240
+ Optional grammar constraint that restricts valid output forms.
241
+ Supported types include:
242
+ - `Plain`: unconstrained text
243
+ - `JSON`: ensures valid JSON output
244
+ - `JSONSchema { schema }`: validates JSON against the given schema
245
+ - `Regex { regex }`: constrains generation by a regular expression
246
+ - `CFG { cfg }`: uses a context-free grammar definition
247
+ """
248
+ @property
249
+ def document_polyfill(self) -> typing.Optional[DocumentPolyfill]: ...
250
+ @document_polyfill.setter
251
+ def document_polyfill(self, value: typing.Optional[DocumentPolyfill]) -> None: ...
252
+ @property
253
+ def think_effort(self) -> typing.Optional[typing.Literal["disable", "enable", "low", "medium", "high"]]: ...
254
+ @think_effort.setter
255
+ def think_effort(self, value: typing.Optional[typing.Literal["disable", "enable", "low", "medium", "high"]]) -> None: ...
256
+ @property
257
+ def temperature(self) -> typing.Optional[builtins.float]: ...
258
+ @temperature.setter
259
+ def temperature(self, value: typing.Optional[builtins.float]) -> None: ...
260
+ @property
261
+ def top_p(self) -> typing.Optional[builtins.float]: ...
262
+ @top_p.setter
263
+ def top_p(self, value: typing.Optional[builtins.float]) -> None: ...
264
+ @property
265
+ def max_tokens(self) -> typing.Optional[builtins.int]: ...
266
+ @max_tokens.setter
267
+ def max_tokens(self, value: typing.Optional[builtins.int]) -> None: ...
268
+ @property
269
+ def grammar(self) -> typing.Optional[Grammar]: ...
270
+ @grammar.setter
271
+ def grammar(self, value: typing.Optional[Grammar]) -> None: ...
272
+ def __new__(cls, document_polyfill: typing.Optional[DocumentPolyfill] = None, think_effort: typing.Optional[typing.Literal["disable", "enable", "low", "medium", "high"]] = None, temperature: typing.Optional[builtins.float] = None, top_p: typing.Optional[builtins.float] = None, max_tokens: typing.Optional[builtins.int] = None) -> LangModelInferConfig: ...
273
+ @classmethod
274
+ def from_dict(cls, config: dict) -> LangModelInferConfig: ...
275
+
276
+ @typing.final
277
+ class MCPClient:
278
+ @property
279
+ def tools(self) -> builtins.list[Tool]: ...
280
+ def __repr__(self) -> builtins.str: ...
281
+ @classmethod
282
+ def from_stdio(cls, command: builtins.str, args: typing.Sequence[builtins.str]) -> typing.Awaitable[MCPClient]: ...
283
+ @classmethod
284
+ def from_streamable_http(cls, url: builtins.str) -> typing.Awaitable[MCPClient]: ...
285
+ def get_tool(self, name: builtins.str) -> typing.Optional[Tool]: ...
286
+
287
+ @typing.final
288
+ class Message:
289
+ r"""
290
+ A chat message generated by a user, model, or tool.
291
+
292
+ `Message` is the concrete, non-streaming container used by the application to store, transmit, or feed structured content into models or tools.
293
+ It can represent various kinds of messages, including user input, assistant responses, tool-call outputs, or signed *thinking* metadata.
294
+
295
+ Note that many different kinds of messages can be produced.
296
+ For example, a language model may internally generate a `thinking` trace before emitting its final output, in order to improve reasoning accuracy.
297
+ In other cases, a model may produce *function calls* — structured outputs that instruct external tools to perform specific actions.
298
+
299
+ This struct is designed to handle all of these situations in a unified way.
300
+
301
+ # Example
302
+
303
+ ## Rust
304
+ ```rust
305
+ let msg = Message::new(Role::User).with_contents([Part::text("hello")]);
306
+ assert_eq!(msg.role, Role::User);
307
+ assert_eq!(msg.contents.len(), 1);
308
+ ```
309
+ """
310
+ @property
311
+ def role(self) -> typing.Literal["system", "user", "assistant", "tool"]:
312
+ r"""
313
+ Author of the message.
314
+ """
315
+ @role.setter
316
+ def role(self, value: typing.Literal["system", "user", "assistant", "tool"]) -> None:
317
+ r"""
318
+ Author of the message.
319
+ """
320
+ @property
321
+ def contents(self) -> builtins.list[Part]:
322
+ r"""
323
+ Primary parts of the message (e.g., text, image, value, or function).
324
+ """
325
+ @contents.setter
326
+ def contents(self, value: typing.Optional[str | list[Part]]) -> None: ...
327
+ @property
328
+ def id(self) -> typing.Optional[builtins.str]:
329
+ r"""
330
+ Optional stable identifier for deduplication or threading.
331
+ """
332
+ @id.setter
333
+ def id(self, value: typing.Optional[builtins.str]) -> None:
334
+ r"""
335
+ Optional stable identifier for deduplication or threading.
336
+ """
337
+ @property
338
+ def thinking(self) -> typing.Optional[builtins.str]:
339
+ r"""
340
+ Internal “thinking” text used by some models before producing final output.
341
+ """
342
+ @thinking.setter
343
+ def thinking(self, value: typing.Optional[builtins.str]) -> None:
344
+ r"""
345
+ Internal “thinking” text used by some models before producing final output.
346
+ """
347
+ @property
348
+ def tool_calls(self) -> typing.Optional[builtins.list[Part]]:
349
+ r"""
350
+ Tool-call parts emitted alongside the main contents.
351
+ """
352
+ @tool_calls.setter
353
+ def tool_calls(self, value: typing.Optional[builtins.list[Part]]) -> None:
354
+ r"""
355
+ Tool-call parts emitted alongside the main contents.
356
+ """
357
+ @property
358
+ def signature(self) -> typing.Optional[builtins.str]:
359
+ r"""
360
+ Optional signature for the `thinking` field.
361
+
362
+ This is only applicable to certain LLM APIs that require a signature as part of the `thinking` payload.
363
+ """
364
+ @signature.setter
365
+ def signature(self, value: typing.Optional[builtins.str]) -> None:
366
+ r"""
367
+ Optional signature for the `thinking` field.
368
+
369
+ This is only applicable to certain LLM APIs that require a signature as part of the `thinking` payload.
370
+ """
371
+ def __new__(cls, role: typing.Literal["system", "user", "assistant", "tool"], contents: typing.Optional[str | list[Part]] = None, id: typing.Optional[builtins.str] = None, thinking: typing.Optional[builtins.str] = None, tool_calls: typing.Optional[typing.Sequence[Part]] = None, signature: typing.Optional[builtins.str] = None) -> Message: ...
372
+ def __repr__(self) -> builtins.str: ...
373
+ def append_tool_call(self, part: Part) -> None: ...
374
+
375
+ @typing.final
376
+ class MessageDelta:
377
+ r"""
378
+ A streaming, incremental update to a [`Message`].
379
+
380
+ `MessageDelta` accumulates partial outputs (text chunks, tool-call fragments, IDs, signatures, etc.) until they can be materialized as a full [`Message`].
381
+ It implements [`Delta`] to support accumulation.
382
+
383
+ # Accumulation Rules
384
+ - `role`: merging two distinct roles fails.
385
+ - `thinking`: concatenated in arrival order.
386
+ - `contents`/`tool_calls`: last element is accumulated with the incoming delta when both are compatible (e.g., Text+Text, Function+Function with matching ID policy), otherwise appended as a new fragment.
387
+ - `id`/`signature`: last-writer-wins.
388
+
389
+ # Finalization
390
+ - `finish()` converts the accumulated deltas into a fully-formed [`Message`].
391
+ Fails if required fields (e.g., `role`) are missing or inner deltas cannot be finalized.
392
+
393
+ # Examples
394
+ ```rust
395
+ let d1 = MessageDelta::new().with_role(Role::Assistant).with_contents([PartDelta::Text { text: "Hel".into() }]);
396
+ let d2 = MessageDelta::new().with_contents([PartDelta::Text { text: "lo".into() }]);
397
+
398
+ let merged = d1.accumulate(d2).unwrap();
399
+ let msg = merged.finish().unwrap();
400
+ assert_eq!(msg.contents[0].as_text().unwrap(), "Hello");
401
+ ```
402
+ """
403
+ @property
404
+ def role(self) -> typing.Optional[typing.Literal["system", "user", "assistant", "tool"]]: ...
405
+ @role.setter
406
+ def role(self, value: typing.Optional[typing.Literal["system", "user", "assistant", "tool"]]) -> None: ...
407
+ @property
408
+ def contents(self) -> builtins.list[PartDelta]: ...
409
+ @contents.setter
410
+ def contents(self, value: builtins.list[PartDelta]) -> None: ...
411
+ @property
412
+ def id(self) -> typing.Optional[builtins.str]: ...
413
+ @id.setter
414
+ def id(self, value: typing.Optional[builtins.str]) -> None: ...
415
+ @property
416
+ def thinking(self) -> typing.Optional[builtins.str]: ...
417
+ @thinking.setter
418
+ def thinking(self, value: typing.Optional[builtins.str]) -> None: ...
419
+ @property
420
+ def tool_calls(self) -> builtins.list[PartDelta]: ...
421
+ @tool_calls.setter
422
+ def tool_calls(self, value: builtins.list[PartDelta]) -> None: ...
423
+ @property
424
+ def signature(self) -> typing.Optional[builtins.str]: ...
425
+ @signature.setter
426
+ def signature(self, value: typing.Optional[builtins.str]) -> None: ...
427
+ def __new__(cls, role: typing.Optional[typing.Literal["system", "user", "assistant", "tool"]] = None, contents: typing.Optional[typing.Sequence[PartDelta]] = None, id: typing.Optional[builtins.str] = None, thinking: typing.Optional[builtins.str] = None, tool_calls: typing.Optional[typing.Sequence[PartDelta]] = None, signature: typing.Optional[builtins.str] = None) -> MessageDelta: ...
428
+ def __repr__(self) -> builtins.str: ...
429
+ def __add__(self, other: MessageDelta) -> MessageDelta: ...
430
+ def finish(self) -> Message: ...
431
+
432
+ @typing.final
433
+ class MessageDeltaOutput:
434
+ r"""
435
+ A container for a streamed message delta and its termination signal.
436
+
437
+ During streaming, `delta` carries the incremental payload; once a terminal
438
+ condition is reached, `finish_reason` may be populated to explain why.
439
+
440
+ # Examples
441
+ ```rust
442
+ let mut out = MessageOutput::new();
443
+ out.delta = MessageDelta::new().with_role(Role::Assistant).with_contents([PartDelta::Text { text: "Hi".into() }]);
444
+ assert!(out.finish_reason.is_none());
445
+ ```
446
+
447
+ # Lifecycle
448
+ - While streaming: `finish_reason` is typically `None`.
449
+ - On completion: `finish_reason` is set; callers can then `finish()` the delta to obtain a concrete [`Message`].
450
+ """
451
+ @property
452
+ def delta(self) -> MessageDelta: ...
453
+ @delta.setter
454
+ def delta(self, value: MessageDelta) -> None: ...
455
+ @property
456
+ def finish_reason(self) -> typing.Optional[FinishReason]: ...
457
+ @finish_reason.setter
458
+ def finish_reason(self, value: typing.Optional[FinishReason]) -> None: ...
459
+ def __repr__(self) -> builtins.str: ...
460
+
461
+ @typing.final
462
+ class MessageDeltaOutputIterator:
463
+ def __aiter__(self) -> MessageDeltaOutputIterator: ...
464
+ def __anext__(self) -> typing.Awaitable[MessageDeltaOutput]: ...
465
+
466
+ @typing.final
467
+ class MessageDeltaOutputSyncIterator:
468
+ def __iter__(self) -> MessageDeltaOutputSyncIterator: ...
469
+ def __next__(self) -> MessageDeltaOutput: ...
470
+
471
+ @typing.final
472
+ class MessageOutput:
473
+ @property
474
+ def message(self) -> Message: ...
475
+ @message.setter
476
+ def message(self, value: Message) -> None: ...
477
+ @property
478
+ def finish_reason(self) -> FinishReason: ...
479
+ @finish_reason.setter
480
+ def finish_reason(self, value: FinishReason) -> None: ...
481
+ def __repr__(self) -> builtins.str: ...
482
+
483
+ @typing.final
484
+ class MessageOutputIterator:
485
+ def __aiter__(self) -> MessageOutputIterator: ...
486
+ def __anext__(self) -> typing.Awaitable[MessageOutput]: ...
487
+
488
+ @typing.final
489
+ class MessageOutputSyncIterator:
490
+ def __iter__(self) -> MessageOutputSyncIterator: ...
491
+ def __next__(self) -> MessageOutput: ...
492
+
493
+ class Part:
494
+ r"""
495
+ Represents a semantically meaningful content unit exchanged between the model and the user.
496
+
497
+ Conceptually, each `Part` encapsulates a piece of **data** that contributes
498
+ to a chat message — such as text, a function invocation, or an image.
499
+
500
+ For example, a single message consisting of a sequence like
501
+ `(text..., image, text...)` is represented as a `Message` containing
502
+ an array of three `Part` elements.
503
+
504
+ Note that a `Part` does **not** carry "intent", such as "reasoning" or "tool call".
505
+ These higher-level semantics are determined by the context of a [`Message`].
506
+
507
+ # Example
508
+
509
+ ## Rust
510
+ ```rust
511
+ let part = Part::text("Hello, world!");
512
+ assert!(part.is_text());
513
+ ```
514
+ """
515
+ @property
516
+ def part_type(self) -> builtins.str: ...
517
+ def __repr__(self) -> builtins.str: ...
518
+ @classmethod
519
+ def image_from_bytes(cls, data: bytes) -> Part: ...
520
+ @classmethod
521
+ def image_from_base64(cls, data: builtins.str) -> Part: ...
522
+ @classmethod
523
+ def image_from_url(cls, url: builtins.str) -> Part: ...
524
+ @typing.final
525
+ class Text(Part):
526
+ r"""
527
+ Plain utf-8 encoded text.
528
+ """
529
+ __match_args__ = ("text",)
530
+ @property
531
+ def text(self) -> builtins.str: ...
532
+ def __new__(cls, text: builtins.str) -> Part.Text: ...
533
+
534
+ @typing.final
535
+ class Function(Part):
536
+ r"""
537
+ Represents a structured function call to an external tool.
538
+
539
+ Many language models (LLMs) use a **function calling** mechanism to extend their capabilities.
540
+ When an LLM decides to use external *tools*, it produces a structured output called a `function`.
541
+ A function conventionally consists of two fields: a `name`, and an `arguments` field formatted as JSON.
542
+ This is conceptually similar to making an HTTP POST request, where the request body carries a single JSON object.
543
+
544
+ This struct models that convention, representing a function invocation request
545
+ from an LLM to an external tool or API.
546
+
547
+ # Examples
548
+ ```rust
549
+ let f = PartFunction {
550
+ name: "translate".to_string(),
551
+ arguments: Value::from_json(r#"{"source": "hello", "lang": "cn"}"#).unwrap(),
552
+ };
553
+ ```
554
+ """
555
+ __match_args__ = ("id", "function",)
556
+ @property
557
+ def id(self) -> typing.Optional[builtins.str]: ...
558
+ @property
559
+ def function(self) -> PartFunction: ...
560
+ def __new__(cls, id: typing.Optional[builtins.str], function: PartFunction) -> Part.Function: ...
561
+
562
+ @typing.final
563
+ class Value(Part):
564
+ r"""
565
+ Holds a structured data value, typically considered as a JSON structure.
566
+ """
567
+ __match_args__ = ("value",)
568
+ @property
569
+ def value(self) -> typing.Any: ...
570
+ def __new__(cls, value: typing.Any) -> Part.Value: ...
571
+
572
+ @typing.final
573
+ class Image(Part):
574
+ r"""
575
+ Contains an image payload or reference used within a message part.
576
+ The image may be provided as raw binary data or an encoded format (e.g., PNG, JPEG),
577
+ or as a reference via a URL. Optional metadata can be included alongside the image.
578
+ """
579
+ __match_args__ = ("image",)
580
+ @property
581
+ def image(self) -> PartImage: ...
582
+ def __new__(cls, image: PartImage) -> Part.Image: ...
583
+
584
+
585
+ class PartDelta:
586
+ r"""
587
+ Represents a partial or incremental update (delta) of a [`Part`].
588
+
589
+ This type enables composable, streaming updates to message parts.
590
+ For example, text may be produced token-by-token, or a function call
591
+ may be emitted gradually as its arguments stream in.
592
+
593
+ # Example
594
+
595
+ ## Rust
596
+ ```rust
597
+ let d1 = PartDelta::Text { text: "Hel".into() };
598
+ let d2 = PartDelta::Text { text: "lo".into() };
599
+ let merged = d1.accumulate(d2).unwrap();
600
+ assert_eq!(merged.to_text().unwrap(), "Hello");
601
+ ```
602
+
603
+ # Error Handling
604
+ Accumulation or finalization may return an error if incompatible deltas
605
+ (e.g. mismatched function IDs) are combined or invalid JSON arguments are given.
606
+ """
607
+ @property
608
+ def part_type(self) -> builtins.str: ...
609
+ def __repr__(self) -> builtins.str: ...
610
+ @typing.final
611
+ class Text(PartDelta):
612
+ r"""
613
+ Incremental text fragment.
614
+ """
615
+ __match_args__ = ("text",)
616
+ @property
617
+ def text(self) -> builtins.str: ...
618
+ def __new__(cls, text: builtins.str) -> PartDelta.Text: ...
619
+
620
+ @typing.final
621
+ class Function(PartDelta):
622
+ r"""
623
+ Incremental function call fragment.
624
+ """
625
+ __match_args__ = ("id", "function",)
626
+ @property
627
+ def id(self) -> typing.Optional[builtins.str]: ...
628
+ @property
629
+ def function(self) -> PartDeltaFunction: ...
630
+ def __new__(cls, id: typing.Optional[builtins.str], function: PartDeltaFunction) -> PartDelta.Function: ...
631
+
632
+ @typing.final
633
+ class Value(PartDelta):
634
+ r"""
635
+ JSON-like value update.
636
+ """
637
+ __match_args__ = ("value",)
638
+ @property
639
+ def value(self) -> typing.Any: ...
640
+ def __new__(cls, value: typing.Any) -> PartDelta.Value: ...
641
+
642
+ @typing.final
643
+ class Null(PartDelta):
644
+ r"""
645
+ Placeholder representing no data yet.
646
+ """
647
+ __match_args__ = ()
648
+ def __new__(cls) -> PartDelta.Null: ...
649
+
650
+
651
+ class PartDeltaFunction:
652
+ r"""
653
+ Represents an incremental update (delta) of a function part.
654
+
655
+ This type is used during streaming or partial message generation, when function calls are being streamed as text chunks or partial JSON fragments.
656
+
657
+ # Variants
658
+ * `Verbatim(String)` — Raw text content, typically a partial JSON fragment.
659
+ * `WithStringArgs { name, arguments }` — Function name and its serialized arguments as strings.
660
+ * `WithParsedArgs { name, arguments }` — Function name and parsed arguments as a `Value`.
661
+
662
+ # Use Case
663
+ When the model streams out a function call response (e.g., `"function_call":{"name":...}`),
664
+ the incremental deltas can be accumulated until the full function payload is formed.
665
+
666
+ # Example
667
+ ```rust
668
+ let delta = PartDeltaFunction::WithStringArgs {
669
+ name: "translate".into(),
670
+ arguments: r#"{"text":"hi"}"#.into(),
671
+ };
672
+ ```
673
+ """
674
+ @typing.final
675
+ class Verbatim(PartDeltaFunction):
676
+ __match_args__ = ("text",)
677
+ @property
678
+ def text(self) -> builtins.str: ...
679
+ def __new__(cls, text: builtins.str) -> PartDeltaFunction.Verbatim: ...
680
+
681
+ @typing.final
682
+ class WithStringArgs(PartDeltaFunction):
683
+ __match_args__ = ("name", "arguments",)
684
+ @property
685
+ def name(self) -> builtins.str: ...
686
+ @property
687
+ def arguments(self) -> builtins.str: ...
688
+ def __new__(cls, name: builtins.str, arguments: builtins.str) -> PartDeltaFunction.WithStringArgs: ...
689
+
690
+ @typing.final
691
+ class WithParsedArgs(PartDeltaFunction):
692
+ __match_args__ = ("name", "arguments",)
693
+ @property
694
+ def name(self) -> builtins.str: ...
695
+ @property
696
+ def arguments(self) -> typing.Any: ...
697
+ def __new__(cls, name: builtins.str, arguments: typing.Any) -> PartDeltaFunction.WithParsedArgs: ...
698
+
699
+ ...
700
+
701
+ @typing.final
702
+ class PartFunction:
703
+ r"""
704
+ Represents a function call contained within a message part.
705
+ """
706
+ @property
707
+ def name(self) -> builtins.str: ...
708
+ @property
709
+ def arguments(self) -> dict[str, typing.Any]: ...
710
+ def __eq__(self, other: builtins.object) -> builtins.bool: ...
711
+ def __new__(cls, name: builtins.str, arguments: typing.Any) -> PartFunction: ...
712
+ def __repr__(self) -> builtins.str: ...
713
+
714
+ class PartImage:
715
+ r"""
716
+ Represents the image data contained in a [`Part`].
717
+
718
+ `PartImage` provides structured access to image data.
719
+ Currently, it only implments "binary" types.
720
+
721
+ # Example
722
+ ```rust
723
+ let part = Part::image_binary(640, 480, "rgb", (0..640*480*3).map(|i| (i % 255) as u8)).unwrap();
724
+
725
+ if let Some(img) = part.as_image() {
726
+ assert_eq!(img.height(), 640);
727
+ assert_eq!(img.width(), 480);
728
+ }
729
+ ```
730
+ """
731
+ @typing.final
732
+ class Binary(PartImage):
733
+ __match_args__ = ("height", "width", "colorspace", "data",)
734
+ @property
735
+ def height(self) -> builtins.int: ...
736
+ @property
737
+ def width(self) -> builtins.int: ...
738
+ @property
739
+ def colorspace(self) -> typing.Literal["grayscale", "rgb", "rgba"]: ...
740
+ @property
741
+ def data(self) -> typing.Any: ...
742
+ def __new__(cls, height: builtins.int, width: builtins.int, colorspace: typing.Literal["grayscale", "rgb", "rgba"], data: typing.Any) -> PartImage.Binary: ...
743
+
744
+ @typing.final
745
+ class Url(PartImage):
746
+ __match_args__ = ("url",)
747
+ @property
748
+ def url(self) -> builtins.str: ...
749
+ def __new__(cls, url: builtins.str) -> PartImage.Url: ...
750
+
751
+ ...
752
+
753
+ class Tool:
754
+ @classmethod
755
+ def new_builtin(cls, kind: typing.Literal["terminal", "web_search_duckduckgo", "web_fetch"], **kwargs: typing.Any) -> Tool: ...
756
+ @classmethod
757
+ def new_py_function(cls, func: typing.Any, desc: typing.Optional[ToolDesc] = None) -> Tool: ...
758
+ def __repr__(self) -> builtins.str: ...
759
+ def get_description(self) -> ToolDesc: ...
760
+ def __call__(self, **kwargs: typing.Any) -> typing.Awaitable[typing.Any]: ...
761
+ def call(self, **kwargs: typing.Any) -> typing.Awaitable[typing.Any]: ...
762
+ def call_sync(self, **kwargs: typing.Any) -> typing.Any: ...
763
+
764
+ @typing.final
765
+ class ToolDesc:
766
+ r"""
767
+ Describes a **tool** (or function) that a language model can invoke.
768
+
769
+ `ToolDesc` defines the schema, behavior, and input/output specification of a callable
770
+ external function, allowing an LLM to understand how to use it.
771
+
772
+ The primary role of this struct is to describe to the LLM what a *tool* does,
773
+ how it can be invoked, and what input (`parameters`) and output (`returns`) schemas it expects.
774
+
775
+ The format follows the same **schema conventions** used by Hugging Face’s
776
+ `transformers` library, as well as APIs such as *OpenAI* and *Anthropic*.
777
+ The `parameters` and `returns` fields are typically defined using **JSON Schema**.
778
+
779
+ We provide a builder [`ToolDescBuilder`] helper for convenient and fluent construction.
780
+ Please refer to [`ToolDescBuilder`].
781
+
782
+ # Example
783
+ ```rust
784
+ use crate::value::{ToolDescBuilder, to_value};
785
+
786
+ let desc = ToolDescBuilder::new("temperature")
787
+ .description("Get the current temperature for a given city")
788
+ .parameters(to_value!({
789
+ "type": "object",
790
+ "properties": {
791
+ "location": {
792
+ "type": "string",
793
+ "description": "The city name"
794
+ },
795
+ "unit": {
796
+ "type": "string",
797
+ "description": "Temperature unit (default: Celsius)",
798
+ "enum": ["Celsius", "Fahrenheit"]
799
+ }
800
+ },
801
+ "required": ["location"]
802
+ }))
803
+ .returns(to_value!({
804
+ "type": "number"
805
+ }))
806
+ .build();
807
+
808
+ assert_eq!(desc.name, "temperature");
809
+ ```
810
+ """
811
+ @property
812
+ def name(self) -> builtins.str: ...
813
+ @property
814
+ def description(self) -> typing.Optional[builtins.str]: ...
815
+ @property
816
+ def parameters(self) -> dict: ...
817
+ @property
818
+ def returns(self) -> typing.Optional[dict]: ...
819
+ def __new__(cls, name: builtins.str, description: typing.Optional[builtins.str], parameters: dict, *, returns: typing.Optional[dict] = None) -> ToolDesc: ...
820
+ def __repr__(self) -> builtins.str: ...
821
+
822
+ @typing.final
823
+ class VectorStore:
824
+ @classmethod
825
+ def new_faiss(cls, dim: builtins.int) -> VectorStore: ...
826
+ @classmethod
827
+ def new_chroma(cls, url: builtins.str, collection_name: typing.Optional[builtins.str]) -> VectorStore: ...
828
+ def add_vector(self, input: VectorStoreAddInput) -> builtins.str: ...
829
+ def add_vectors(self, inputs: typing.Sequence[VectorStoreAddInput]) -> builtins.list[builtins.str]: ...
830
+ def get_by_id(self, id: builtins.str) -> typing.Optional[VectorStoreGetResult]: ...
831
+ def get_by_ids(self, ids: typing.Sequence[builtins.str]) -> builtins.list[VectorStoreGetResult]: ...
832
+ def retrieve(self, query_embedding: builtins.list[float], top_k: builtins.int) -> builtins.list[VectorStoreRetrieveResult]: ...
833
+ def batch_retrieve(self, query_embeddings: typing.Sequence[builtins.list[float]], top_k: builtins.int) -> builtins.list[builtins.list[VectorStoreRetrieveResult]]: ...
834
+ def remove_vector(self, id: builtins.str) -> None: ...
835
+ def remove_vectors(self, ids: typing.Sequence[builtins.str]) -> None: ...
836
+ def clear(self) -> None: ...
837
+ def count(self) -> builtins.int: ...
838
+
839
+ @typing.final
840
+ class VectorStoreAddInput:
841
+ @property
842
+ def embedding(self) -> builtins.list[float]: ...
843
+ @embedding.setter
844
+ def embedding(self, value: builtins.list[float]) -> None: ...
845
+ @property
846
+ def document(self) -> builtins.str: ...
847
+ @document.setter
848
+ def document(self, value: builtins.str) -> None: ...
849
+ @property
850
+ def metadata(self) -> typing.Optional[builtins.dict[builtins.str, typing.Any]]: ...
851
+ @metadata.setter
852
+ def metadata(self, value: typing.Optional[builtins.dict[builtins.str, typing.Any]]) -> None: ...
853
+ def __new__(cls, embedding: builtins.list[float], document: builtins.str, metadata: typing.Optional[typing.Mapping[builtins.str, typing.Any]] = None) -> VectorStoreAddInput: ...
854
+
855
+ @typing.final
856
+ class VectorStoreGetResult:
857
+ @property
858
+ def id(self) -> builtins.str: ...
859
+ @id.setter
860
+ def id(self, value: builtins.str) -> None: ...
861
+ @property
862
+ def document(self) -> builtins.str: ...
863
+ @document.setter
864
+ def document(self, value: builtins.str) -> None: ...
865
+ @property
866
+ def metadata(self) -> typing.Optional[builtins.dict[builtins.str, typing.Any]]: ...
867
+ @metadata.setter
868
+ def metadata(self, value: typing.Optional[builtins.dict[builtins.str, typing.Any]]) -> None: ...
869
+ @property
870
+ def embedding(self) -> builtins.list[float]: ...
871
+ @embedding.setter
872
+ def embedding(self, value: builtins.list[float]) -> None: ...
873
+
874
+ @typing.final
875
+ class VectorStoreRetrieveResult:
876
+ @property
877
+ def id(self) -> builtins.str: ...
878
+ @id.setter
879
+ def id(self, value: builtins.str) -> None: ...
880
+ @property
881
+ def document(self) -> builtins.str: ...
882
+ @document.setter
883
+ def document(self, value: builtins.str) -> None: ...
884
+ @property
885
+ def metadata(self) -> typing.Optional[builtins.dict[builtins.str, typing.Any]]: ...
886
+ @metadata.setter
887
+ def metadata(self, value: typing.Optional[builtins.dict[builtins.str, typing.Any]]) -> None: ...
888
+ @property
889
+ def distance(self) -> builtins.float: ...
890
+ @distance.setter
891
+ def distance(self, value: builtins.float) -> None: ...
892
+
893
+ @typing.final
894
+ class FinishReason(enum.Enum):
895
+ r"""
896
+ Explains why a language model's streamed generation finished.
897
+ """
898
+ Stop = ...
899
+ r"""
900
+ The model stopped naturally (e.g., EOS token or stop sequence).
901
+ """
902
+ Length = ...
903
+ r"""
904
+ Hit the maximum token/length limit.
905
+ """
906
+ ToolCall = ...
907
+ r"""
908
+ Stopped because a tool call was produced, waiting for it's execution.
909
+ """
910
+ Refusal = ...
911
+ r"""
912
+ Content was refused/filtered; string provides reason.
913
+ """
914
+
915
+ def __repr__(self) -> builtins.str: ...
916
+
917
+ def ailoy_model_cli() -> None: ...
918
+
ailoy/_patches.py ADDED
@@ -0,0 +1,256 @@
1
+ import inspect
2
+ import json
3
+ import re
4
+ import types
5
+ from typing import (
6
+ Any,
7
+ Callable,
8
+ Literal,
9
+ Optional,
10
+ Union,
11
+ get_args,
12
+ get_origin,
13
+ get_type_hints,
14
+ )
15
+
16
+ from ailoy._core import Tool, ToolDesc
17
+
18
+ description_re = re.compile(r"^(.*?)[\n\s]*(Args:|Returns:|Raises:|\Z)", re.DOTALL)
19
+ # Extracts the Args: block from the docstring
20
+ args_re = re.compile(r"\n\s*Args:\n\s*(.*?)[\n\s]*(Returns:|Raises:|\Z)", re.DOTALL)
21
+ # Splits the Args: block into individual arguments
22
+ args_split_re = re.compile(
23
+ r"""
24
+ (?:^|\n) # Match the start of the args block, or a newline
25
+ \s*(\w+):\s* # Capture the argument name and strip spacing
26
+ (.*?)\s* # Capture the argument description, which can span multiple lines, and strip trailing spacing
27
+ (?=\n\s*\w+:|\Z) # Stop when you hit the next argument or the end of the block
28
+ """,
29
+ re.DOTALL | re.VERBOSE,
30
+ )
31
+ # Extracts the Returns: block from the docstring, if present. Note that most chat templates ignore the return type/doc!
32
+ returns_re = re.compile(r"\n\s*Returns:\n\s*(.*?)[\n\s]*(Raises:|\Z)", re.DOTALL)
33
+
34
+
35
+ class TypeHintParsingException(Exception):
36
+ """Exception raised for errors in parsing type hints to generate JSON schemas"""
37
+
38
+ pass
39
+
40
+
41
+ class DocstringParsingException(Exception):
42
+ """Exception raised for errors in parsing docstrings to generate JSON schemas"""
43
+
44
+ pass
45
+
46
+
47
+ def _get_json_schema_type(param_type: type) -> dict[str, str]:
48
+ type_mapping = {
49
+ int: {"type": "integer"},
50
+ float: {"type": "number"},
51
+ str: {"type": "string"},
52
+ bool: {"type": "boolean"},
53
+ type(None): {"type": "null"},
54
+ Any: {},
55
+ }
56
+ # if is_vision_available():
57
+ # type_mapping[Image] = {"type": "image"}
58
+ # if is_torch_available():
59
+ # type_mapping[Tensor] = {"type": "audio"}
60
+ return type_mapping.get(param_type, {"type": "object"})
61
+
62
+
63
+ def _parse_type_hint(hint: str) -> dict:
64
+ origin = get_origin(hint)
65
+ args = get_args(hint)
66
+
67
+ if origin is None:
68
+ try:
69
+ return _get_json_schema_type(hint)
70
+ except KeyError:
71
+ raise TypeHintParsingException(
72
+ "Couldn't parse this type hint, likely due to a custom class or object: ",
73
+ hint,
74
+ )
75
+
76
+ elif origin is Union or (hasattr(types, "UnionType") and origin is types.UnionType):
77
+ # Recurse into each of the subtypes in the Union, except None, which is handled separately at the end
78
+ subtypes = [_parse_type_hint(t) for t in args if t is not type(None)]
79
+ if len(subtypes) == 1:
80
+ # A single non-null type can be expressed directly
81
+ return_dict = subtypes[0]
82
+ elif all(isinstance(subtype["type"], str) for subtype in subtypes):
83
+ # A union of basic types can be expressed as a list in the schema
84
+ return_dict = {"type": sorted([subtype["type"] for subtype in subtypes])}
85
+ else:
86
+ # A union of more complex types requires "anyOf"
87
+ return_dict = {"anyOf": subtypes}
88
+ if type(None) in args:
89
+ return_dict["nullable"] = True
90
+ return return_dict
91
+
92
+ elif origin is Literal and len(args) > 0:
93
+ LITERAL_TYPES = (int, float, str, bool, type(None))
94
+ args_types = []
95
+ for arg in args:
96
+ if type(arg) not in LITERAL_TYPES:
97
+ raise TypeHintParsingException(
98
+ "Only the valid python literals can be listed in typing.Literal."
99
+ )
100
+ arg_type = _get_json_schema_type(type(arg)).get("type")
101
+ if arg_type is not None and arg_type not in args_types:
102
+ args_types.append(arg_type)
103
+ return {
104
+ "type": args_types.pop() if len(args_types) == 1 else list(args_types),
105
+ "enum": list(args),
106
+ }
107
+
108
+ elif origin is list:
109
+ if not args:
110
+ return {"type": "array"}
111
+ else:
112
+ # Lists can only have a single type argument, so recurse into it
113
+ return {"type": "array", "items": _parse_type_hint(args[0])}
114
+
115
+ elif origin is tuple:
116
+ if not args:
117
+ return {"type": "array"}
118
+ if len(args) == 1:
119
+ raise TypeHintParsingException(
120
+ f"The type hint {str(hint).replace('typing.', '')} is a Tuple with a single element, which "
121
+ "we do not automatically convert to JSON schema as it is rarely necessary. If this input can contain "
122
+ "more than one element, we recommend "
123
+ "using a list[] type instead, or if it really is a single element, remove the tuple[] wrapper and just "
124
+ "pass the element directly."
125
+ )
126
+ if ... in args:
127
+ raise TypeHintParsingException(
128
+ "Conversion of '...' is not supported in Tuple type hints. "
129
+ "Use list[] types for variable-length"
130
+ " inputs instead."
131
+ )
132
+ return {"type": "array", "prefixItems": [_parse_type_hint(t) for t in args]}
133
+
134
+ elif origin is dict:
135
+ # The JSON equivalent to a dict is 'object', which mandates that all keys are strings
136
+ # However, we can specify the type of the dict values with "additionalProperties"
137
+ out = {"type": "object"}
138
+ if len(args) == 2:
139
+ out["additionalProperties"] = _parse_type_hint(args[1])
140
+ return out
141
+
142
+ raise TypeHintParsingException(
143
+ "Couldn't parse this type hint, likely due to a custom class or object: ", hint
144
+ )
145
+
146
+
147
+ def _convert_type_hints_to_json_schema(func: Callable) -> dict:
148
+ type_hints = get_type_hints(func)
149
+ signature = inspect.signature(func)
150
+ required = []
151
+ for param_name, param in signature.parameters.items():
152
+ if param.annotation == inspect.Parameter.empty:
153
+ raise TypeHintParsingException(
154
+ f"Argument {param.name} is missing a type hint in function {func.__name__}"
155
+ )
156
+ if param.default == inspect.Parameter.empty:
157
+ required.append(param_name)
158
+
159
+ properties = {}
160
+ for param_name, param_type in type_hints.items():
161
+ properties[param_name] = _parse_type_hint(param_type)
162
+
163
+ schema = {"type": "object", "properties": properties}
164
+ if required:
165
+ schema["required"] = required
166
+
167
+ return schema
168
+
169
+
170
+ def parse_google_format_docstring(
171
+ docstring: str,
172
+ ) -> tuple[Optional[str], Optional[dict], Optional[str]]:
173
+ """
174
+ Parses a Google-style docstring to extract the function description,
175
+ argument descriptions, and return description.
176
+
177
+ Args:
178
+ docstring (str): The docstring to parse.
179
+
180
+ Returns:
181
+ The function description, arguments, and return description.
182
+ """
183
+
184
+ # Extract the sections
185
+ description_match = description_re.search(docstring)
186
+ args_match = args_re.search(docstring)
187
+ returns_match = returns_re.search(docstring)
188
+
189
+ # Clean and store the sections
190
+ description = description_match.group(1).strip() if description_match else None
191
+ docstring_args = args_match.group(1).strip() if args_match else None
192
+ returns = returns_match.group(1).strip() if returns_match else None
193
+
194
+ # Parsing the arguments into a dictionary
195
+ if docstring_args is not None:
196
+ docstring_args = "\n".join(
197
+ [line for line in docstring_args.split("\n") if line.strip()]
198
+ ) # Remove blank lines
199
+ matches = args_split_re.findall(docstring_args)
200
+ args_dict = {
201
+ match[0]: re.sub(r"\s*\n+\s*", " ", match[1].strip()) for match in matches
202
+ }
203
+ else:
204
+ args_dict = {}
205
+
206
+ return description, args_dict, returns
207
+
208
+
209
+ def get_json_schema(func: Callable) -> dict:
210
+ doc = inspect.getdoc(func)
211
+ if not doc:
212
+ raise DocstringParsingException(
213
+ f"Cannot generate JSON schema for {func.__name__} because it has no docstring!"
214
+ )
215
+ doc = doc.strip()
216
+ main_doc, param_descriptions, return_doc = parse_google_format_docstring(doc)
217
+
218
+ json_schema = _convert_type_hints_to_json_schema(func)
219
+ if (return_dict := json_schema["properties"].pop("return", None)) is not None:
220
+ if (
221
+ return_doc is not None
222
+ ): # We allow a missing return docstring since most templates ignore it
223
+ return_dict["description"] = return_doc
224
+ for arg, schema in json_schema["properties"].items():
225
+ if arg not in param_descriptions:
226
+ raise DocstringParsingException(
227
+ f"Cannot generate JSON schema for {func.__name__} because the docstring has no description for the argument '{arg}'"
228
+ )
229
+ desc = param_descriptions[arg]
230
+ enum_choices = re.search(r"\(choices:\s*(.*?)\)\s*$", desc, flags=re.IGNORECASE)
231
+ if enum_choices:
232
+ schema["enum"] = [c.strip() for c in json.loads(enum_choices.group(1))]
233
+ desc = enum_choices.string[: enum_choices.start()].strip()
234
+ schema["description"] = desc
235
+
236
+ output = {"name": func.__name__, "description": main_doc, "parameters": json_schema}
237
+ if return_dict is not None:
238
+ output["returns"] = return_dict
239
+ return {"type": "function", "function": output}
240
+
241
+
242
+ def new_py_function(
243
+ cls: Tool, func: Callable, tool_desc: Optional[ToolDesc] = None
244
+ ) -> Tool:
245
+ if tool_desc is None:
246
+ try:
247
+ json_schema = get_json_schema(func)
248
+ except (TypeHintParsingException, DocstringParsingException) as e:
249
+ raise ValueError("Failed to parse docstring", e)
250
+
251
+ tool_desc = ToolDesc(**json_schema.get("function"))
252
+
253
+ return cls.__new_py_function__(tool_desc, func)
254
+
255
+
256
+ setattr(Tool, "new_py_function", classmethod(new_py_function))
@@ -0,0 +1,126 @@
1
+ Metadata-Version: 2.4
2
+ Name: ailoy-py
3
+ Version: 0.2.4
4
+ Classifier: Development Status :: 3 - Alpha
5
+ Classifier: Intended Audience :: Developers
6
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
7
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
8
+ Classifier: Programming Language :: Rust
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Python :: 3.10
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: Programming Language :: Python :: 3.14
15
+ Classifier: Programming Language :: Python :: Implementation :: CPython
16
+ Summary: A lightweight library for building AI applications
17
+ Author-email: "Brekkylab Inc." <contact@brekkylab.com>
18
+ License-Expression: Apache-2.0
19
+ Requires-Python: >=3.10
20
+ Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
21
+ Project-URL: Documentation, https://brekkylab.github.io/ailoy
22
+ Project-URL: Homepage, https://brekkylab.github.io/ailoy
23
+ Project-URL: Issues, https://github.com/brekkylab/ailoy/issues
24
+ Project-URL: Repository, https://github.com/brekkylab/ailoy
25
+
26
+ # ailoy-py
27
+
28
+ Ailoy is a lightweight library for building AI applications — such as **agent systems** or **RAG pipelines** — with ease. It is designed to enable AI features effortlessly, one can just import and use.
29
+
30
+ See our [documentation](https://brekkylab.github.io/ailoy) for more details.
31
+
32
+ ## Install
33
+
34
+ ```bash
35
+ pip install ailoy-py
36
+ ```
37
+
38
+ ## Quickstart
39
+
40
+ ### Asynchronous version (recommended)
41
+ ```python
42
+ import asyncio
43
+
44
+ import ailoy as ai
45
+
46
+
47
+ async def main():
48
+ # Create Qwen3-0.6B local LangModel
49
+ model = await ai.LangModel.new_local("Qwen/Qwen3-0.6B")
50
+
51
+ # Create an agent using this model
52
+ agent = ai.Agent(model)
53
+
54
+ # Ask a prompt and iterate over agent's responses
55
+ async for resp in agent.run("What is your name?"):
56
+ print(resp)
57
+
58
+
59
+ if __name__ == "__main__":
60
+ asyncio.run(main())
61
+ ```
62
+
63
+ ### Synchronous version
64
+ ```python
65
+ import ailoy as ai
66
+
67
+
68
+ def main():
69
+ # Create Qwen3-0.6B LocalLanguageModel
70
+ model = ai.LangModel.new_local_sync("Qwen/Qwen3-0.6B")
71
+
72
+ # Create an agent using this model
73
+ agent = ai.Agent(model)
74
+
75
+ # Ask a prompt and iterate over agent's responses
76
+ for resp in agent.run_sync("What is your name?"):
77
+ print(resp)
78
+
79
+
80
+ if __name__ == "__main__":
81
+ main()
82
+ ```
83
+
84
+ ## Building from source
85
+
86
+ ### Prerequisites
87
+
88
+ - Rust >= 1.88
89
+ - Python >= 3.10
90
+ - C/C++ compiler
91
+ (recommended versions are below)
92
+ - GCC >= 13
93
+ - LLVM Clang >= 17
94
+ - Apple Clang >= 15
95
+ - MSVC >= 19.29
96
+ - CMake >= 3.28.0
97
+ - Git
98
+ - OpenMP (required to build Faiss)
99
+ - BLAS (required to build Faiss)
100
+ - LAPACK (required to build Faiss)
101
+ - Vulkan SDK (on Windows and Linux)
102
+
103
+ > [!WARNING]
104
+ > To build binding, you must change the crate type to **`cdylib`** in [Cargo.toml](../../Cargo.toml).
105
+ >
106
+ > ```toml
107
+ > [lib]
108
+ > crate-type = ["dylib"] # <- Change this to ["cdylib"]
109
+ > ```
110
+
111
+
112
+ ### Setup development environment
113
+
114
+ ```bash
115
+ pip install maturin
116
+
117
+ # This generates `_core.cpython-3xx-darwin.so` under `ailoy/`
118
+ maturin develop
119
+ ```
120
+
121
+ ### Generate wheel
122
+
123
+ ```bash
124
+ maturin build --out ./dist
125
+ ```
126
+
@@ -0,0 +1,13 @@
1
+ ailoy/__init__.py,sha256=gjSajwEPDaWJAXvM744fj_wXHYrpRLR38sYWj_xUp-k,96
2
+ ailoy/_core.abi3.so,sha256=wVzbAwMfXsiuURQ8lQfGNFg3D-vBlEmPUwLua0n_EnA,34447561
3
+ ailoy/_core.pyi,sha256=HTPWsidoLyoLH2jBjlg7vt-23biqRXMKhqBtMQnUz30,38704
4
+ ailoy/_patches.py,sha256=bJg-jl64VKWbFbLyxqM-ZY3a3CK8RoNGzMF6CpWU2pI,9602
5
+ ailoy_py.libs/libfaiss-b004fceb.so,sha256=e02JSyUH4KERET9e1rNexUbZSWyImWkKCk7ZN8t4ymg,11257937
6
+ ailoy_py.libs/libgomp-e985bcbb.so.1.0.0,sha256=pDkE5PopcwHUZA3BuzyKNIC0BvmeSY66mxkUtoqrYEo,253289
7
+ ailoy_py.libs/libtvm_ffi-9e4a703d.so,sha256=exaT3IeM7GD5KXIRSUMlb53-6AQlYklHAZuuZefcNhk,1135113
8
+ ailoy_py.libs/libtvm_runtime-b87d417d.so,sha256=CPsHFl8zASSjgwfPAalBv8WkDwhMP2A4xfCaieO9ndI,5927721
9
+ ailoy_py-0.2.4.dist-info/METADATA,sha256=IyNWRcnuFmfYOD1Tghlv5buoNz-TYgPTRz9StoirpYI,3183
10
+ ailoy_py-0.2.4.dist-info/WHEEL,sha256=IFJVkQ8769pcslF3jPXXkSmy4yVHIAHaSOV_0yFIexE,109
11
+ ailoy_py-0.2.4.dist-info/entry_points.txt,sha256=2QfqZEtN7J34DEJaPEf_zmQalbrI3NMDGJT81mdgXjQ,58
12
+ ailoy_py-0.2.4.dist-info/RECORD,,
13
+ ailoy_py-0.2.4.dist-info/sboms/auditwheel.cdx.json,sha256=7d965nwURfbckBSej25GURuVgxaimx51zflOxOd_qPg,1315
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: maturin (1.11.2)
3
+ Root-Is-Purelib: false
4
+ Tag: cp310-abi3-manylinux_2_28_x86_64
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ ailoy-model=ailoy._core:ailoy_model_cli
@@ -0,0 +1 @@
1
+ {"bomFormat": "CycloneDX", "specVersion": "1.4", "version": 1, "metadata": {"component": {"type": "library", "bom-ref": "pkg:pypi/ailoy_py@0.2.4?file_name=ailoy_py-0.2.4-cp310-abi3-manylinux_2_28_x86_64.whl", "name": "ailoy_py", "version": "0.2.4", "purl": "pkg:pypi/ailoy_py@0.2.4?file_name=ailoy_py-0.2.4-cp310-abi3-manylinux_2_28_x86_64.whl"}, "tools": [{"name": "auditwheel", "version": "6.5.0"}]}, "components": [{"type": "library", "bom-ref": "pkg:pypi/ailoy_py@0.2.4?file_name=ailoy_py-0.2.4-cp310-abi3-manylinux_2_28_x86_64.whl", "name": "ailoy_py", "version": "0.2.4", "purl": "pkg:pypi/ailoy_py@0.2.4?file_name=ailoy_py-0.2.4-cp310-abi3-manylinux_2_28_x86_64.whl"}, {"type": "library", "bom-ref": "pkg:rpm/almalinux/libgomp@8.5.0-28.el8_10.alma.1#c61017c9a24eb6e1e1a3cdc9becd004a6419cbda3d54b4848b98f240a4829571", "name": "libgomp", "version": "8.5.0-28.el8_10.alma.1", "purl": "pkg:rpm/almalinux/libgomp@8.5.0-28.el8_10.alma.1"}], "dependencies": [{"ref": "pkg:pypi/ailoy_py@0.2.4?file_name=ailoy_py-0.2.4-cp310-abi3-manylinux_2_28_x86_64.whl", "dependsOn": ["pkg:rpm/almalinux/libgomp@8.5.0-28.el8_10.alma.1#c61017c9a24eb6e1e1a3cdc9becd004a6419cbda3d54b4848b98f240a4829571"]}, {"ref": "pkg:rpm/almalinux/libgomp@8.5.0-28.el8_10.alma.1#c61017c9a24eb6e1e1a3cdc9becd004a6419cbda3d54b4848b98f240a4829571"}]}
Binary file
Binary file
Binary file
Binary file