ai-lib-python 0.5.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ai_lib_python/__init__.py +43 -0
- ai_lib_python/batch/__init__.py +15 -0
- ai_lib_python/batch/collector.py +244 -0
- ai_lib_python/batch/executor.py +224 -0
- ai_lib_python/cache/__init__.py +26 -0
- ai_lib_python/cache/backends.py +380 -0
- ai_lib_python/cache/key.py +237 -0
- ai_lib_python/cache/manager.py +332 -0
- ai_lib_python/client/__init__.py +37 -0
- ai_lib_python/client/builder.py +528 -0
- ai_lib_python/client/cancel.py +368 -0
- ai_lib_python/client/core.py +433 -0
- ai_lib_python/client/response.py +134 -0
- ai_lib_python/embeddings/__init__.py +36 -0
- ai_lib_python/embeddings/client.py +339 -0
- ai_lib_python/embeddings/types.py +234 -0
- ai_lib_python/embeddings/vectors.py +246 -0
- ai_lib_python/errors/__init__.py +41 -0
- ai_lib_python/errors/base.py +316 -0
- ai_lib_python/errors/classification.py +210 -0
- ai_lib_python/guardrails/__init__.py +35 -0
- ai_lib_python/guardrails/base.py +336 -0
- ai_lib_python/guardrails/filters.py +583 -0
- ai_lib_python/guardrails/validators.py +475 -0
- ai_lib_python/pipeline/__init__.py +55 -0
- ai_lib_python/pipeline/accumulate.py +248 -0
- ai_lib_python/pipeline/base.py +240 -0
- ai_lib_python/pipeline/decode.py +281 -0
- ai_lib_python/pipeline/event_map.py +506 -0
- ai_lib_python/pipeline/fan_out.py +284 -0
- ai_lib_python/pipeline/select.py +297 -0
- ai_lib_python/plugins/__init__.py +32 -0
- ai_lib_python/plugins/base.py +294 -0
- ai_lib_python/plugins/hooks.py +296 -0
- ai_lib_python/plugins/middleware.py +285 -0
- ai_lib_python/plugins/registry.py +294 -0
- ai_lib_python/protocol/__init__.py +71 -0
- ai_lib_python/protocol/loader.py +317 -0
- ai_lib_python/protocol/manifest.py +385 -0
- ai_lib_python/protocol/validator.py +460 -0
- ai_lib_python/py.typed +1 -0
- ai_lib_python/resilience/__init__.py +102 -0
- ai_lib_python/resilience/backpressure.py +225 -0
- ai_lib_python/resilience/circuit_breaker.py +318 -0
- ai_lib_python/resilience/executor.py +343 -0
- ai_lib_python/resilience/fallback.py +341 -0
- ai_lib_python/resilience/preflight.py +413 -0
- ai_lib_python/resilience/rate_limiter.py +291 -0
- ai_lib_python/resilience/retry.py +299 -0
- ai_lib_python/resilience/signals.py +283 -0
- ai_lib_python/routing/__init__.py +118 -0
- ai_lib_python/routing/manager.py +593 -0
- ai_lib_python/routing/strategy.py +345 -0
- ai_lib_python/routing/types.py +397 -0
- ai_lib_python/structured/__init__.py +33 -0
- ai_lib_python/structured/json_mode.py +281 -0
- ai_lib_python/structured/schema.py +316 -0
- ai_lib_python/structured/validator.py +334 -0
- ai_lib_python/telemetry/__init__.py +127 -0
- ai_lib_python/telemetry/exporters/__init__.py +9 -0
- ai_lib_python/telemetry/exporters/prometheus.py +111 -0
- ai_lib_python/telemetry/feedback.py +446 -0
- ai_lib_python/telemetry/health.py +409 -0
- ai_lib_python/telemetry/logger.py +389 -0
- ai_lib_python/telemetry/metrics.py +496 -0
- ai_lib_python/telemetry/tracer.py +473 -0
- ai_lib_python/tokens/__init__.py +25 -0
- ai_lib_python/tokens/counter.py +282 -0
- ai_lib_python/tokens/estimator.py +286 -0
- ai_lib_python/transport/__init__.py +34 -0
- ai_lib_python/transport/auth.py +141 -0
- ai_lib_python/transport/http.py +364 -0
- ai_lib_python/transport/pool.py +425 -0
- ai_lib_python/types/__init__.py +41 -0
- ai_lib_python/types/events.py +343 -0
- ai_lib_python/types/message.py +332 -0
- ai_lib_python/types/tool.py +191 -0
- ai_lib_python/utils/__init__.py +21 -0
- ai_lib_python/utils/tool_call_assembler.py +317 -0
- ai_lib_python-0.5.0.dist-info/METADATA +837 -0
- ai_lib_python-0.5.0.dist-info/RECORD +84 -0
- ai_lib_python-0.5.0.dist-info/WHEEL +4 -0
- ai_lib_python-0.5.0.dist-info/licenses/LICENSE-APACHE +201 -0
- ai_lib_python-0.5.0.dist-info/licenses/LICENSE-MIT +21 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tool types for function calling support.
|
|
3
|
+
|
|
4
|
+
Based on AI-Protocol standard_schema for tool definitions and calls.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from enum import Enum
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ToolChoice(str, Enum):
|
|
16
|
+
"""Tool choice policy for requests."""
|
|
17
|
+
|
|
18
|
+
AUTO = "auto"
|
|
19
|
+
NONE = "none"
|
|
20
|
+
REQUIRED = "required"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class FunctionDefinition(BaseModel):
|
|
24
|
+
"""Function definition within a tool.
|
|
25
|
+
|
|
26
|
+
Defines the schema for a callable function including:
|
|
27
|
+
- name: Function identifier
|
|
28
|
+
- description: What the function does
|
|
29
|
+
- parameters: JSON Schema for function parameters
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
model_config = ConfigDict(extra="allow")
|
|
33
|
+
|
|
34
|
+
name: str = Field(description="Function name")
|
|
35
|
+
description: str | None = Field(default=None, description="Function description")
|
|
36
|
+
parameters: dict[str, Any] = Field(
|
|
37
|
+
default_factory=lambda: {"type": "object", "properties": {}},
|
|
38
|
+
description="JSON Schema for parameters",
|
|
39
|
+
)
|
|
40
|
+
strict: bool | None = Field(
|
|
41
|
+
default=None,
|
|
42
|
+
description="Whether to enforce strict schema validation (provider-specific)",
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class ToolDefinition(BaseModel):
|
|
47
|
+
"""Tool definition for function calling.
|
|
48
|
+
|
|
49
|
+
Wraps a FunctionDefinition with the standard tool format.
|
|
50
|
+
|
|
51
|
+
Example:
|
|
52
|
+
>>> tool = ToolDefinition.from_function(
|
|
53
|
+
... name="get_weather",
|
|
54
|
+
... description="Get weather for a city",
|
|
55
|
+
... parameters={
|
|
56
|
+
... "type": "object",
|
|
57
|
+
... "properties": {
|
|
58
|
+
... "city": {"type": "string", "description": "City name"},
|
|
59
|
+
... "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}
|
|
60
|
+
... },
|
|
61
|
+
... "required": ["city"]
|
|
62
|
+
... }
|
|
63
|
+
... )
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
model_config = ConfigDict(extra="allow")
|
|
67
|
+
|
|
68
|
+
type: str = Field(default="function", description="Tool type")
|
|
69
|
+
function: FunctionDefinition = Field(description="Function definition")
|
|
70
|
+
|
|
71
|
+
@classmethod
|
|
72
|
+
def from_function(
|
|
73
|
+
cls,
|
|
74
|
+
name: str,
|
|
75
|
+
description: str | None = None,
|
|
76
|
+
parameters: dict[str, Any] | None = None,
|
|
77
|
+
strict: bool | None = None,
|
|
78
|
+
) -> ToolDefinition:
|
|
79
|
+
"""Create a tool definition from function details.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
name: Function name
|
|
83
|
+
description: Function description
|
|
84
|
+
parameters: JSON Schema for parameters
|
|
85
|
+
strict: Whether to enforce strict validation
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
ToolDefinition instance
|
|
89
|
+
"""
|
|
90
|
+
func_def = FunctionDefinition(
|
|
91
|
+
name=name,
|
|
92
|
+
description=description,
|
|
93
|
+
parameters=parameters or {"type": "object", "properties": {}},
|
|
94
|
+
strict=strict,
|
|
95
|
+
)
|
|
96
|
+
return cls(function=func_def)
|
|
97
|
+
|
|
98
|
+
@property
|
|
99
|
+
def name(self) -> str:
|
|
100
|
+
"""Get the function name."""
|
|
101
|
+
return self.function.name
|
|
102
|
+
|
|
103
|
+
@property
|
|
104
|
+
def description(self) -> str | None:
|
|
105
|
+
"""Get the function description."""
|
|
106
|
+
return self.function.description
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class ToolCall(BaseModel):
|
|
110
|
+
"""A tool call from the model response.
|
|
111
|
+
|
|
112
|
+
Represents a request from the model to invoke a specific tool.
|
|
113
|
+
|
|
114
|
+
Attributes:
|
|
115
|
+
id: Unique identifier for this tool call
|
|
116
|
+
type: Tool type (typically "function")
|
|
117
|
+
function_name: Name of the function to call
|
|
118
|
+
arguments: Parsed arguments for the function
|
|
119
|
+
|
|
120
|
+
Example:
|
|
121
|
+
>>> tool_call = ToolCall(
|
|
122
|
+
... id="call_abc123",
|
|
123
|
+
... function_name="get_weather",
|
|
124
|
+
... arguments={"city": "Beijing", "unit": "celsius"}
|
|
125
|
+
... )
|
|
126
|
+
"""
|
|
127
|
+
|
|
128
|
+
model_config = ConfigDict(extra="allow")
|
|
129
|
+
|
|
130
|
+
id: str = Field(description="Unique tool call identifier")
|
|
131
|
+
type: str = Field(default="function", description="Tool type")
|
|
132
|
+
function_name: str = Field(description="Name of the function to call")
|
|
133
|
+
arguments: dict[str, Any] = Field(
|
|
134
|
+
default_factory=dict,
|
|
135
|
+
description="Parsed function arguments",
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
# Raw arguments string (useful for debugging/logging)
|
|
139
|
+
arguments_raw: str | None = Field(
|
|
140
|
+
default=None,
|
|
141
|
+
description="Raw arguments string before parsing",
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
@classmethod
|
|
145
|
+
def from_openai_format(
|
|
146
|
+
cls,
|
|
147
|
+
id: str,
|
|
148
|
+
function_name: str,
|
|
149
|
+
arguments: str | dict[str, Any],
|
|
150
|
+
) -> ToolCall:
|
|
151
|
+
"""Create from OpenAI-style tool call format.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
id: Tool call ID
|
|
155
|
+
function_name: Function name
|
|
156
|
+
arguments: Arguments (string or dict)
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
ToolCall instance
|
|
160
|
+
"""
|
|
161
|
+
import json
|
|
162
|
+
|
|
163
|
+
if isinstance(arguments, str):
|
|
164
|
+
try:
|
|
165
|
+
parsed_args = json.loads(arguments) if arguments else {}
|
|
166
|
+
except json.JSONDecodeError:
|
|
167
|
+
parsed_args = {}
|
|
168
|
+
return cls(
|
|
169
|
+
id=id,
|
|
170
|
+
function_name=function_name,
|
|
171
|
+
arguments=parsed_args,
|
|
172
|
+
arguments_raw=arguments,
|
|
173
|
+
)
|
|
174
|
+
return cls(
|
|
175
|
+
id=id,
|
|
176
|
+
function_name=function_name,
|
|
177
|
+
arguments=arguments,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
def to_content_block(self) -> dict[str, Any]:
|
|
181
|
+
"""Convert to content block format for message construction.
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
Dictionary suitable for use as a tool_use content block.
|
|
185
|
+
"""
|
|
186
|
+
return {
|
|
187
|
+
"type": "tool_use",
|
|
188
|
+
"id": self.id,
|
|
189
|
+
"name": self.function_name,
|
|
190
|
+
"input": self.arguments,
|
|
191
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Utility functions and helpers.
|
|
3
|
+
|
|
4
|
+
This module contains:
|
|
5
|
+
- JSONPath utilities
|
|
6
|
+
- Tool decorator
|
|
7
|
+
- ToolCallAssembler for streaming tool calls
|
|
8
|
+
- Other helper functions
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from ai_lib_python.utils.tool_call_assembler import (
|
|
12
|
+
MultiToolCallAssembler,
|
|
13
|
+
ToolCallAssembler,
|
|
14
|
+
ToolCallFragment,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"MultiToolCallAssembler",
|
|
19
|
+
"ToolCallAssembler",
|
|
20
|
+
"ToolCallFragment",
|
|
21
|
+
]
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tool call assembler for streaming responses.
|
|
3
|
+
|
|
4
|
+
Collects tool call fragments from streaming events and assembles
|
|
5
|
+
them into complete ToolCall objects.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from ai_lib_python.types.tool import ToolCall
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class ToolCallFragment:
|
|
19
|
+
"""Fragment of a tool call being assembled.
|
|
20
|
+
|
|
21
|
+
Attributes:
|
|
22
|
+
id: Tool call identifier
|
|
23
|
+
name: Tool/function name
|
|
24
|
+
arguments_buffer: Accumulated arguments string
|
|
25
|
+
index: Position in the tool calls array
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
id: str
|
|
29
|
+
name: str = ""
|
|
30
|
+
arguments_buffer: str = ""
|
|
31
|
+
index: int = 0
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class ToolCallAssembler:
|
|
35
|
+
"""Assembles tool calls from streaming fragments.
|
|
36
|
+
|
|
37
|
+
Collects tool call started events and argument fragments,
|
|
38
|
+
then finalizes them into complete ToolCall objects.
|
|
39
|
+
|
|
40
|
+
This is intentionally tolerant: if JSON parsing fails,
|
|
41
|
+
it keeps the raw string as arguments.
|
|
42
|
+
|
|
43
|
+
Example:
|
|
44
|
+
>>> assembler = ToolCallAssembler()
|
|
45
|
+
>>>
|
|
46
|
+
>>> # Process streaming events
|
|
47
|
+
>>> assembler.on_started("call_123", "get_weather")
|
|
48
|
+
>>> assembler.on_partial("call_123", '{"loc')
|
|
49
|
+
>>> assembler.on_partial("call_123", 'ation": "NYC"}')
|
|
50
|
+
>>>
|
|
51
|
+
>>> # Finalize
|
|
52
|
+
>>> tool_calls = assembler.finalize()
|
|
53
|
+
>>> print(tool_calls[0].arguments)
|
|
54
|
+
{'location': 'NYC'}
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
def __init__(self) -> None:
|
|
58
|
+
"""Initialize assembler."""
|
|
59
|
+
self._fragments: dict[str, ToolCallFragment] = {}
|
|
60
|
+
self._order: list[str] = [] # Maintain insertion order
|
|
61
|
+
|
|
62
|
+
def on_started(
|
|
63
|
+
self,
|
|
64
|
+
tool_call_id: str,
|
|
65
|
+
tool_name: str,
|
|
66
|
+
index: int | None = None,
|
|
67
|
+
) -> None:
|
|
68
|
+
"""Handle tool call started event.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
tool_call_id: Unique tool call identifier
|
|
72
|
+
tool_name: Name of the tool/function
|
|
73
|
+
index: Optional index in tool calls array
|
|
74
|
+
"""
|
|
75
|
+
if tool_call_id in self._fragments:
|
|
76
|
+
# Update existing fragment with name if empty
|
|
77
|
+
if tool_name and not self._fragments[tool_call_id].name:
|
|
78
|
+
self._fragments[tool_call_id].name = tool_name
|
|
79
|
+
return
|
|
80
|
+
|
|
81
|
+
fragment = ToolCallFragment(
|
|
82
|
+
id=tool_call_id,
|
|
83
|
+
name=tool_name,
|
|
84
|
+
index=index if index is not None else len(self._order),
|
|
85
|
+
)
|
|
86
|
+
self._fragments[tool_call_id] = fragment
|
|
87
|
+
self._order.append(tool_call_id)
|
|
88
|
+
|
|
89
|
+
def on_partial(
|
|
90
|
+
self,
|
|
91
|
+
tool_call_id: str,
|
|
92
|
+
arguments_fragment: str,
|
|
93
|
+
) -> None:
|
|
94
|
+
"""Handle partial tool call arguments.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
tool_call_id: Tool call identifier
|
|
98
|
+
arguments_fragment: Partial arguments string
|
|
99
|
+
"""
|
|
100
|
+
if tool_call_id not in self._fragments:
|
|
101
|
+
# Create fragment if it doesn't exist
|
|
102
|
+
self.on_started(tool_call_id, "")
|
|
103
|
+
|
|
104
|
+
self._fragments[tool_call_id].arguments_buffer += arguments_fragment
|
|
105
|
+
|
|
106
|
+
def on_name(self, tool_call_id: str, name_fragment: str) -> None:
|
|
107
|
+
"""Handle partial tool name (some APIs stream the name too).
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
tool_call_id: Tool call identifier
|
|
111
|
+
name_fragment: Partial name string
|
|
112
|
+
"""
|
|
113
|
+
if tool_call_id not in self._fragments:
|
|
114
|
+
self.on_started(tool_call_id, name_fragment)
|
|
115
|
+
else:
|
|
116
|
+
self._fragments[tool_call_id].name += name_fragment
|
|
117
|
+
|
|
118
|
+
def finalize(self) -> list[ToolCall]:
|
|
119
|
+
"""Finalize all tool calls.
|
|
120
|
+
|
|
121
|
+
Parses accumulated argument strings as JSON and creates
|
|
122
|
+
ToolCall objects. If JSON parsing fails, uses empty dict.
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
List of assembled ToolCall objects
|
|
126
|
+
"""
|
|
127
|
+
tool_calls: list[ToolCall] = []
|
|
128
|
+
|
|
129
|
+
for tool_call_id in self._order:
|
|
130
|
+
fragment = self._fragments[tool_call_id]
|
|
131
|
+
|
|
132
|
+
# Try to parse arguments as JSON
|
|
133
|
+
raw_arguments = fragment.arguments_buffer.strip()
|
|
134
|
+
arguments: dict[str, Any] = {}
|
|
135
|
+
arguments_raw: str | None = None
|
|
136
|
+
|
|
137
|
+
if raw_arguments:
|
|
138
|
+
try:
|
|
139
|
+
parsed = json.loads(raw_arguments)
|
|
140
|
+
if isinstance(parsed, dict):
|
|
141
|
+
arguments = parsed
|
|
142
|
+
else:
|
|
143
|
+
# Non-dict JSON, keep as raw
|
|
144
|
+
arguments_raw = raw_arguments
|
|
145
|
+
except json.JSONDecodeError:
|
|
146
|
+
# Invalid JSON, store as raw
|
|
147
|
+
arguments_raw = raw_arguments
|
|
148
|
+
|
|
149
|
+
tool_call = ToolCall(
|
|
150
|
+
id=fragment.id,
|
|
151
|
+
function_name=fragment.name,
|
|
152
|
+
arguments=arguments,
|
|
153
|
+
arguments_raw=arguments_raw,
|
|
154
|
+
)
|
|
155
|
+
tool_calls.append(tool_call)
|
|
156
|
+
|
|
157
|
+
return tool_calls
|
|
158
|
+
|
|
159
|
+
def finalize_and_reset(self) -> list[ToolCall]:
|
|
160
|
+
"""Finalize tool calls and reset state.
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
List of assembled ToolCall objects
|
|
164
|
+
"""
|
|
165
|
+
result = self.finalize()
|
|
166
|
+
self.reset()
|
|
167
|
+
return result
|
|
168
|
+
|
|
169
|
+
def reset(self) -> None:
|
|
170
|
+
"""Reset assembler state."""
|
|
171
|
+
self._fragments.clear()
|
|
172
|
+
self._order.clear()
|
|
173
|
+
|
|
174
|
+
def has_tool_calls(self) -> bool:
|
|
175
|
+
"""Check if any tool calls have been started.
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
True if any tool calls exist
|
|
179
|
+
"""
|
|
180
|
+
return len(self._fragments) > 0
|
|
181
|
+
|
|
182
|
+
def get_fragment(self, tool_call_id: str) -> ToolCallFragment | None:
|
|
183
|
+
"""Get a specific fragment.
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
tool_call_id: Tool call identifier
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
ToolCallFragment or None
|
|
190
|
+
"""
|
|
191
|
+
return self._fragments.get(tool_call_id)
|
|
192
|
+
|
|
193
|
+
@property
|
|
194
|
+
def count(self) -> int:
|
|
195
|
+
"""Get number of tool calls being assembled."""
|
|
196
|
+
return len(self._fragments)
|
|
197
|
+
|
|
198
|
+
def __len__(self) -> int:
|
|
199
|
+
"""Get number of tool calls."""
|
|
200
|
+
return len(self._fragments)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
class MultiToolCallAssembler:
|
|
204
|
+
"""Assembles tool calls from multiple messages/responses.
|
|
205
|
+
|
|
206
|
+
Manages multiple ToolCallAssembler instances keyed by message/turn ID.
|
|
207
|
+
|
|
208
|
+
Example:
|
|
209
|
+
>>> assembler = MultiToolCallAssembler()
|
|
210
|
+
>>>
|
|
211
|
+
>>> # First turn
|
|
212
|
+
>>> assembler.on_started("turn1", "call_1", "search")
|
|
213
|
+
>>> assembler.on_partial("turn1", "call_1", '{"query": "weather"}')
|
|
214
|
+
>>>
|
|
215
|
+
>>> # Second turn
|
|
216
|
+
>>> assembler.on_started("turn2", "call_2", "calculate")
|
|
217
|
+
>>> assembler.on_partial("turn2", "call_2", '{"expression": "2+2"}')
|
|
218
|
+
>>>
|
|
219
|
+
>>> # Finalize by turn
|
|
220
|
+
>>> turn1_calls = assembler.finalize_turn("turn1")
|
|
221
|
+
>>> turn2_calls = assembler.finalize_turn("turn2")
|
|
222
|
+
"""
|
|
223
|
+
|
|
224
|
+
def __init__(self) -> None:
|
|
225
|
+
"""Initialize multi-assembler."""
|
|
226
|
+
self._assemblers: dict[str, ToolCallAssembler] = {}
|
|
227
|
+
|
|
228
|
+
def _get_assembler(self, turn_id: str) -> ToolCallAssembler:
|
|
229
|
+
"""Get or create assembler for a turn.
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
turn_id: Turn/message identifier
|
|
233
|
+
|
|
234
|
+
Returns:
|
|
235
|
+
ToolCallAssembler instance
|
|
236
|
+
"""
|
|
237
|
+
if turn_id not in self._assemblers:
|
|
238
|
+
self._assemblers[turn_id] = ToolCallAssembler()
|
|
239
|
+
return self._assemblers[turn_id]
|
|
240
|
+
|
|
241
|
+
def on_started(
|
|
242
|
+
self,
|
|
243
|
+
turn_id: str,
|
|
244
|
+
tool_call_id: str,
|
|
245
|
+
tool_name: str,
|
|
246
|
+
index: int | None = None,
|
|
247
|
+
) -> None:
|
|
248
|
+
"""Handle tool call started event.
|
|
249
|
+
|
|
250
|
+
Args:
|
|
251
|
+
turn_id: Turn/message identifier
|
|
252
|
+
tool_call_id: Tool call identifier
|
|
253
|
+
tool_name: Tool name
|
|
254
|
+
index: Optional index
|
|
255
|
+
"""
|
|
256
|
+
self._get_assembler(turn_id).on_started(tool_call_id, tool_name, index)
|
|
257
|
+
|
|
258
|
+
def on_partial(
|
|
259
|
+
self,
|
|
260
|
+
turn_id: str,
|
|
261
|
+
tool_call_id: str,
|
|
262
|
+
arguments_fragment: str,
|
|
263
|
+
) -> None:
|
|
264
|
+
"""Handle partial arguments.
|
|
265
|
+
|
|
266
|
+
Args:
|
|
267
|
+
turn_id: Turn/message identifier
|
|
268
|
+
tool_call_id: Tool call identifier
|
|
269
|
+
arguments_fragment: Arguments fragment
|
|
270
|
+
"""
|
|
271
|
+
self._get_assembler(turn_id).on_partial(tool_call_id, arguments_fragment)
|
|
272
|
+
|
|
273
|
+
def finalize_turn(self, turn_id: str) -> list[ToolCall]:
|
|
274
|
+
"""Finalize tool calls for a turn.
|
|
275
|
+
|
|
276
|
+
Args:
|
|
277
|
+
turn_id: Turn/message identifier
|
|
278
|
+
|
|
279
|
+
Returns:
|
|
280
|
+
List of ToolCall objects
|
|
281
|
+
"""
|
|
282
|
+
if turn_id not in self._assemblers:
|
|
283
|
+
return []
|
|
284
|
+
return self._assemblers[turn_id].finalize()
|
|
285
|
+
|
|
286
|
+
def finalize_all(self) -> dict[str, list[ToolCall]]:
|
|
287
|
+
"""Finalize all turns.
|
|
288
|
+
|
|
289
|
+
Returns:
|
|
290
|
+
Dictionary mapping turn IDs to tool call lists
|
|
291
|
+
"""
|
|
292
|
+
return {
|
|
293
|
+
turn_id: assembler.finalize()
|
|
294
|
+
for turn_id, assembler in self._assemblers.items()
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
def reset(self) -> None:
|
|
298
|
+
"""Reset all assemblers."""
|
|
299
|
+
self._assemblers.clear()
|
|
300
|
+
|
|
301
|
+
def reset_turn(self, turn_id: str) -> None:
|
|
302
|
+
"""Reset a specific turn.
|
|
303
|
+
|
|
304
|
+
Args:
|
|
305
|
+
turn_id: Turn/message identifier
|
|
306
|
+
"""
|
|
307
|
+
if turn_id in self._assemblers:
|
|
308
|
+
del self._assemblers[turn_id]
|
|
309
|
+
|
|
310
|
+
@property
|
|
311
|
+
def turns(self) -> list[str]:
|
|
312
|
+
"""Get list of turn IDs."""
|
|
313
|
+
return list(self._assemblers.keys())
|
|
314
|
+
|
|
315
|
+
def __len__(self) -> int:
|
|
316
|
+
"""Get number of turns."""
|
|
317
|
+
return len(self._assemblers)
|