aixtools 0.2.15__py3-none-any.whl → 0.2.17__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.
Potentially problematic release.
This version of aixtools might be problematic. Click here for more details.
- aixtools/_version.py +2 -2
- aixtools/agents/agent.py +4 -2
- aixtools/agents/nodes_to_md.py +201 -0
- aixtools/agents/nodes_to_message.py +30 -0
- aixtools/agents/nodes_to_str.py +109 -0
- aixtools/logging/log_objects.py +21 -10
- aixtools/logging/mcp_middleware.py +54 -0
- aixtools/testing/agent_mock.py +143 -0
- aixtools/utils/utils.py +35 -3
- {aixtools-0.2.15.dist-info → aixtools-0.2.17.dist-info}/METADATA +35 -15
- {aixtools-0.2.15.dist-info → aixtools-0.2.17.dist-info}/RECORD +14 -9
- {aixtools-0.2.15.dist-info → aixtools-0.2.17.dist-info}/WHEEL +0 -0
- {aixtools-0.2.15.dist-info → aixtools-0.2.17.dist-info}/entry_points.txt +0 -0
- {aixtools-0.2.15.dist-info → aixtools-0.2.17.dist-info}/top_level.txt +0 -0
aixtools/_version.py
CHANGED
|
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
|
|
|
28
28
|
commit_id: COMMIT_ID
|
|
29
29
|
__commit_id__: COMMIT_ID
|
|
30
30
|
|
|
31
|
-
__version__ = version = '0.2.
|
|
32
|
-
__version_tuple__ = version_tuple = (0, 2,
|
|
31
|
+
__version__ = version = '0.2.17'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 2, 17)
|
|
33
33
|
|
|
34
34
|
__commit_id__ = commit_id = None
|
aixtools/agents/agent.py
CHANGED
|
@@ -70,6 +70,7 @@ def _get_model_openai_azure(
|
|
|
70
70
|
azure_openai_api_key=AZURE_OPENAI_API_KEY,
|
|
71
71
|
azure_openai_endpoint=AZURE_OPENAI_ENDPOINT,
|
|
72
72
|
azure_openai_api_version=AZURE_OPENAI_API_VERSION,
|
|
73
|
+
http_client=None,
|
|
73
74
|
):
|
|
74
75
|
assert azure_openai_endpoint, "AZURE_OPENAI_ENDPOINT is not set"
|
|
75
76
|
assert azure_openai_api_key, "AZURE_OPENAI_API_KEY is not set"
|
|
@@ -79,6 +80,7 @@ def _get_model_openai_azure(
|
|
|
79
80
|
azure_endpoint=azure_openai_endpoint,
|
|
80
81
|
api_version=azure_openai_api_version,
|
|
81
82
|
api_key=azure_openai_api_key,
|
|
83
|
+
http_client=http_client,
|
|
82
84
|
)
|
|
83
85
|
return OpenAIChatModel(model_name=model_name, provider=OpenAIProvider(openai_client=client))
|
|
84
86
|
|
|
@@ -101,9 +103,9 @@ def get_model(model_family=MODEL_FAMILY, model_name=None, http_client=None, **kw
|
|
|
101
103
|
assert model_family is not None and model_family != "", f"Model family '{model_family}' is not set"
|
|
102
104
|
match model_family:
|
|
103
105
|
case "azure":
|
|
104
|
-
return _get_model_openai_azure(model_name=model_name or AZURE_MODEL_NAME, **kwargs)
|
|
106
|
+
return _get_model_openai_azure(model_name=model_name or AZURE_MODEL_NAME, http_client=http_client, **kwargs)
|
|
105
107
|
case "bedrock":
|
|
106
|
-
return _get_model_bedrock(model_name=model_name or BEDROCK_MODEL_NAME, **kwargs)
|
|
108
|
+
return _get_model_bedrock(model_name=model_name or BEDROCK_MODEL_NAME, http_client=http_client, **kwargs)
|
|
107
109
|
case "ollama":
|
|
108
110
|
return _get_model_ollama(model_name=model_name or OLLAMA_MODEL_NAME, http_client=http_client, **kwargs)
|
|
109
111
|
case "openai":
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"""Convert Pydantic-AI Nodes to Markdown format."""
|
|
2
|
+
|
|
3
|
+
from pydantic_ai import CallToolsNode, ModelRequestNode, UserPromptNode
|
|
4
|
+
from pydantic_ai.messages import (
|
|
5
|
+
RetryPromptPart,
|
|
6
|
+
SystemPromptPart,
|
|
7
|
+
TextPart,
|
|
8
|
+
ToolCallPart,
|
|
9
|
+
ToolReturnPart,
|
|
10
|
+
UserPromptPart,
|
|
11
|
+
)
|
|
12
|
+
from pydantic_graph.nodes import End
|
|
13
|
+
|
|
14
|
+
from aixtools.agents.nodes_to_message import NodesToMessage
|
|
15
|
+
from aixtools.utils.utils import is_multiline, is_too_long, to_json_pretty_print
|
|
16
|
+
|
|
17
|
+
MAX_TITLE_LENGTH = 30
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class NodesToMarkdown(NodesToMessage):
|
|
21
|
+
"""
|
|
22
|
+
Convert Pydantic-AI Nodes to Markdown
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
user_prompt_node: bool = True,
|
|
28
|
+
user_prompt_part: bool = True,
|
|
29
|
+
system_prompt_part: bool = True,
|
|
30
|
+
title_max_length: int = MAX_TITLE_LENGTH,
|
|
31
|
+
):
|
|
32
|
+
"""
|
|
33
|
+
Initialize the NodeToMarkdown converter.
|
|
34
|
+
Args:
|
|
35
|
+
user_prompt_node: Whether to include UserPromptNode content
|
|
36
|
+
user_prompt_part: Whether to include UserPromptPart content
|
|
37
|
+
system_prompt_part: Whether to include SystemPromptPart content
|
|
38
|
+
"""
|
|
39
|
+
super().__init__(
|
|
40
|
+
user_prompt_node=user_prompt_node,
|
|
41
|
+
user_prompt_part=user_prompt_part,
|
|
42
|
+
system_prompt_part=system_prompt_part,
|
|
43
|
+
)
|
|
44
|
+
self.title_max_length = title_max_length
|
|
45
|
+
|
|
46
|
+
def to_str(self, nodes) -> str | None:
|
|
47
|
+
"""Convert a node to its markdown string representation."""
|
|
48
|
+
_, title, md = self.to_markdown(nodes)
|
|
49
|
+
return f"# {title}\n\n{md}"
|
|
50
|
+
|
|
51
|
+
def to_markdown(self, node) -> tuple[str | None, str | None, str | None]:
|
|
52
|
+
"""
|
|
53
|
+
Get a name, title, and markdown representation of a node.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
name: A short name for the node
|
|
57
|
+
title: A title for the node
|
|
58
|
+
node_md: A markdown representation of the node or None
|
|
59
|
+
|
|
60
|
+
Note: all values can be None if the node should not be converted (e.g., user prompt disabled).
|
|
61
|
+
"""
|
|
62
|
+
node_md = self.node2md(node)
|
|
63
|
+
if node_md is None:
|
|
64
|
+
return None, None, None
|
|
65
|
+
name = self.node2name(node)
|
|
66
|
+
title = self.node2title(node)
|
|
67
|
+
return name, title, node_md
|
|
68
|
+
|
|
69
|
+
def _format_prompt_part(self, label: str, content: str, enabled: bool) -> str | None:
|
|
70
|
+
"""Format UserPromptPart or SystemPromptPart with multiline handling."""
|
|
71
|
+
if not enabled:
|
|
72
|
+
return None
|
|
73
|
+
if is_multiline(content): # type: ignore
|
|
74
|
+
return f"### {label}\n{content}\n"
|
|
75
|
+
return f"{label}: {content}\n"
|
|
76
|
+
|
|
77
|
+
def _part2md(self, p) -> str | None: # noqa: PLR0911 # pylint: disable=too-many-return-statements
|
|
78
|
+
"""Convert a Part to a string representation."""
|
|
79
|
+
match p:
|
|
80
|
+
case ToolCallPart():
|
|
81
|
+
return f"### Tool `{p.tool_name}`\n```json\n{to_json_pretty_print(p.args)}\n```\n"
|
|
82
|
+
case TextPart():
|
|
83
|
+
return f"### Text\n{p.content}\n" if is_multiline(p.content) else f"{p.content}\n"
|
|
84
|
+
case ToolReturnPart():
|
|
85
|
+
if is_multiline(p.content) or is_too_long(p.content):
|
|
86
|
+
return f"### Tool return `{p.tool_name}`\n```json\n{to_json_pretty_print(p.content)}\n```\n"
|
|
87
|
+
return f"Tool return `{p.tool_name}`: `{p.content}`"
|
|
88
|
+
case UserPromptPart():
|
|
89
|
+
return self._format_prompt_part("UserPromptPart", p.content, self.user_prompt_part) # type: ignore
|
|
90
|
+
case SystemPromptPart():
|
|
91
|
+
return self._format_prompt_part("SystemPromptPart", p.content, self.system_prompt_part) # type: ignore
|
|
92
|
+
case RetryPromptPart():
|
|
93
|
+
return f"### RetryPromptPart `{p.tool_name}`\n{p.content}\n"
|
|
94
|
+
case _:
|
|
95
|
+
return f"### Part {type(p)}\n{p}"
|
|
96
|
+
|
|
97
|
+
def _part2title(self, p) -> str: # noqa: PLR0911 # pylint: disable=too-many-return-statements
|
|
98
|
+
"""Convert a Part to a title representation."""
|
|
99
|
+
match p:
|
|
100
|
+
case ToolCallPart():
|
|
101
|
+
return f"Tool `{p.tool_name}`"
|
|
102
|
+
case TextPart():
|
|
103
|
+
return self._to_title_length(p.content)
|
|
104
|
+
case ToolReturnPart():
|
|
105
|
+
return f"Tool return `{p.tool_name}`"
|
|
106
|
+
case UserPromptPart():
|
|
107
|
+
return f"UserPromptPart: {self._to_title_length(p.content)}\n"
|
|
108
|
+
case SystemPromptPart():
|
|
109
|
+
return f"SystemPromptPart: {self._to_title_length(p.content)}\n"
|
|
110
|
+
case RetryPromptPart():
|
|
111
|
+
return f"WARNING: Retry {p.tool_name}"
|
|
112
|
+
case _:
|
|
113
|
+
return f"{type(p)}: {self._to_title_length(p)}"
|
|
114
|
+
|
|
115
|
+
def _parts2md(self, parts) -> str | None:
|
|
116
|
+
"""Convert to string a list of Parts with a given prefix."""
|
|
117
|
+
if len(parts) == 0:
|
|
118
|
+
return None
|
|
119
|
+
|
|
120
|
+
if len(parts) == 1:
|
|
121
|
+
s = self._part2md(parts[0])
|
|
122
|
+
if s is None:
|
|
123
|
+
return None
|
|
124
|
+
return s + "\n"
|
|
125
|
+
|
|
126
|
+
result = ""
|
|
127
|
+
for p in parts:
|
|
128
|
+
s = self._part2md(p)
|
|
129
|
+
if s is not None:
|
|
130
|
+
result += s + "\n"
|
|
131
|
+
return result
|
|
132
|
+
|
|
133
|
+
def _parts2title(self, parts) -> str:
|
|
134
|
+
"""Convert to title a list of Parts with a given prefix."""
|
|
135
|
+
if len(parts) == 0:
|
|
136
|
+
return ""
|
|
137
|
+
|
|
138
|
+
if len(parts) == 1:
|
|
139
|
+
return self._part2title(parts[0])
|
|
140
|
+
|
|
141
|
+
result = ""
|
|
142
|
+
for p in parts:
|
|
143
|
+
s = self._part2title(p)
|
|
144
|
+
# Prefer 'Tool' titles
|
|
145
|
+
if s.startswith("ERROR"):
|
|
146
|
+
return s
|
|
147
|
+
if s.startswith("Tool"):
|
|
148
|
+
return s
|
|
149
|
+
if not result:
|
|
150
|
+
result = s
|
|
151
|
+
|
|
152
|
+
return result
|
|
153
|
+
|
|
154
|
+
def node2md(self, n) -> str | None:
|
|
155
|
+
"""Convert a node in a markdown format"""
|
|
156
|
+
match n:
|
|
157
|
+
case UserPromptNode():
|
|
158
|
+
return f"# UserPrompt\n{n.user_prompt}\n" if self.user_prompt_node else None
|
|
159
|
+
case CallToolsNode():
|
|
160
|
+
return f"# Call tools\n{self._parts2md(n.model_response.parts)}\n"
|
|
161
|
+
case ModelRequestNode():
|
|
162
|
+
return f"# Model request\n{self._parts2md(n.request.parts)}\n"
|
|
163
|
+
case End():
|
|
164
|
+
return f"# End\n{n.data.output}\n"
|
|
165
|
+
case RetryPromptPart():
|
|
166
|
+
return "# Retry tool"
|
|
167
|
+
case _:
|
|
168
|
+
return f"{type(n)}: {n}"
|
|
169
|
+
|
|
170
|
+
def node2title(self, n) -> str | None:
|
|
171
|
+
"""Get a title for a node."""
|
|
172
|
+
match n:
|
|
173
|
+
case UserPromptNode():
|
|
174
|
+
return self._to_title_length(n.user_prompt)
|
|
175
|
+
case CallToolsNode():
|
|
176
|
+
return self._parts2title(n.model_response.parts)
|
|
177
|
+
case ModelRequestNode():
|
|
178
|
+
return self._parts2title(n.request.parts)
|
|
179
|
+
case End():
|
|
180
|
+
return self._to_title_length(n.data.output)
|
|
181
|
+
case _:
|
|
182
|
+
return f"{type(n)}: {n}"
|
|
183
|
+
|
|
184
|
+
def node2name(self, n) -> str | None:
|
|
185
|
+
"""Get a short name for a node."""
|
|
186
|
+
match n:
|
|
187
|
+
case UserPromptNode():
|
|
188
|
+
return "UserPrompt"
|
|
189
|
+
case CallToolsNode():
|
|
190
|
+
return "Call tools"
|
|
191
|
+
case ModelRequestNode():
|
|
192
|
+
return "Model request"
|
|
193
|
+
case End():
|
|
194
|
+
return "End"
|
|
195
|
+
case _:
|
|
196
|
+
return f"{type(n)}: {n}"
|
|
197
|
+
|
|
198
|
+
def _to_title_length(self, s) -> str:
|
|
199
|
+
"""Truncate a string to a maximum length for title display."""
|
|
200
|
+
s = str(s).replace("\n", " ").strip()
|
|
201
|
+
return s[: self.title_max_length] + "..." if len(s) > self.title_max_length else s
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Convert Pydantic-AI Nodes to Markdown format."""
|
|
2
|
+
|
|
3
|
+
from abc import ABC
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class NodesToMessage(ABC): # pylint: disable=too-few-public-methods
|
|
7
|
+
"""
|
|
8
|
+
Convert Pydantic-AI Nodes to Message format
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
def __init__(
|
|
12
|
+
self,
|
|
13
|
+
user_prompt_node: bool = True,
|
|
14
|
+
user_prompt_part: bool = True,
|
|
15
|
+
system_prompt_part: bool = True,
|
|
16
|
+
):
|
|
17
|
+
"""
|
|
18
|
+
Initialize the NodeToMessage converter.
|
|
19
|
+
Args:
|
|
20
|
+
user_prompt_node: Whether to include UserPromptNode content
|
|
21
|
+
user_prompt_part: Whether to include UserPromptPart content
|
|
22
|
+
system_prompt_part: Whether to include SystemPromptPart content
|
|
23
|
+
"""
|
|
24
|
+
self.user_prompt_node = user_prompt_node
|
|
25
|
+
self.user_prompt_part = user_prompt_part
|
|
26
|
+
self.system_prompt_part = system_prompt_part
|
|
27
|
+
|
|
28
|
+
def to_str(self, nodes) -> str | None:
|
|
29
|
+
"""Convert a node to its string representation."""
|
|
30
|
+
raise NotImplementedError("to_str method must be implemented by subclasses")
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""Convert Pydantic-AI Nodes to String format."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Iterable
|
|
4
|
+
|
|
5
|
+
from pydantic_ai import CallToolsNode, ModelRequestNode, UserPromptNode
|
|
6
|
+
from pydantic_ai.messages import SystemPromptPart, TextPart, ToolCallPart, ToolReturnPart, UserPromptPart
|
|
7
|
+
from pydantic_graph.nodes import End
|
|
8
|
+
|
|
9
|
+
from aixtools.agents.nodes_to_message import NodesToMessage
|
|
10
|
+
from aixtools.utils.utils import is_multiline, tabit
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def print_nodes(nodes):
|
|
14
|
+
"""Convert a list of nodes in a readable format."""
|
|
15
|
+
n2s = NodesToString()
|
|
16
|
+
print(n2s.to_str(nodes))
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class NodesToString(NodesToMessage): # pylint: disable=too-few-public-methods
|
|
20
|
+
"""
|
|
21
|
+
Convert Pydantic-AI Nodes to String
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(self, user_prompt_node: bool = True, user_prompt_part: bool = True, system_prompt_part: bool = True):
|
|
25
|
+
"""
|
|
26
|
+
Initialize the NodeToMarkdown converter.
|
|
27
|
+
Args:
|
|
28
|
+
user_prompt_node: Whether to include UserPromptNode content
|
|
29
|
+
user_prompt_part: Whether to include UserPromptPart content
|
|
30
|
+
system_prompt_part: Whether to include SystemPromptPart content
|
|
31
|
+
"""
|
|
32
|
+
super().__init__(
|
|
33
|
+
user_prompt_node=user_prompt_node,
|
|
34
|
+
user_prompt_part=user_prompt_part,
|
|
35
|
+
system_prompt_part=system_prompt_part,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
def _format_content(self, label: str, content: str, prefix: str) -> str:
|
|
39
|
+
"""Format content with optional multiline handling."""
|
|
40
|
+
if is_multiline(content):
|
|
41
|
+
pre = f"{prefix}\t|"
|
|
42
|
+
return f"{prefix}{label}:\n{tabit(content, pre)}"
|
|
43
|
+
return f"{prefix}{label}: {content}"
|
|
44
|
+
|
|
45
|
+
def _part2str(self, p, prefix: str = "\t") -> str | None:
|
|
46
|
+
"""Convert a Part to a string representation."""
|
|
47
|
+
match p:
|
|
48
|
+
case ToolCallPart():
|
|
49
|
+
return f"{prefix}Tool: {p.tool_name}, args: {p.args}"
|
|
50
|
+
case TextPart():
|
|
51
|
+
return self._format_content("Text", p.content, prefix)
|
|
52
|
+
case ToolReturnPart():
|
|
53
|
+
return f"{prefix}Tool return: {p.tool_name}, content: {p.content}"
|
|
54
|
+
case UserPromptPart():
|
|
55
|
+
return None if not self.user_prompt_part else self._format_content("UserPromptPart", p.content, prefix) # type: ignore # pylint: disable=line-too-long
|
|
56
|
+
case SystemPromptPart():
|
|
57
|
+
return (
|
|
58
|
+
None if not self.system_prompt_part else self._format_content("SystemPromptPart", p.content, prefix)
|
|
59
|
+
) # type: ignore
|
|
60
|
+
case _:
|
|
61
|
+
return f"{prefix}Part {type(p)}: {p}"
|
|
62
|
+
|
|
63
|
+
def _parts2str(self, parts, prefix: str = "") -> str:
|
|
64
|
+
"""Convert to string a list of Parts with a given prefix."""
|
|
65
|
+
if len(parts) == 0:
|
|
66
|
+
return f"{prefix}No parts\n"
|
|
67
|
+
|
|
68
|
+
if len(parts) == 1:
|
|
69
|
+
s = self._part2str(parts[0], prefix=prefix)
|
|
70
|
+
return s + "\n" if s else ""
|
|
71
|
+
|
|
72
|
+
result = ""
|
|
73
|
+
for i, p in enumerate(parts):
|
|
74
|
+
s = self._part2str(p, prefix=f"{prefix}{i}: ")
|
|
75
|
+
result += f"{s}\n" if s else ""
|
|
76
|
+
return result
|
|
77
|
+
|
|
78
|
+
def _node2str(self, n) -> str:
|
|
79
|
+
"""Convert a node in a readable format."""
|
|
80
|
+
match n:
|
|
81
|
+
case UserPromptNode():
|
|
82
|
+
assert n.user_prompt is not None
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
f"UserPrompt:\n{tabit(n.user_prompt)}\n"
|
|
86
|
+
if n.user_prompt is not None and self.user_prompt_node
|
|
87
|
+
else ""
|
|
88
|
+
)
|
|
89
|
+
case CallToolsNode():
|
|
90
|
+
parts_str = self._parts2str(n.model_response.parts, prefix="\t")
|
|
91
|
+
return f"Call tools:\n{parts_str}\n"
|
|
92
|
+
case ModelRequestNode():
|
|
93
|
+
parts_str = self._parts2str(n.request.parts, prefix="\t")
|
|
94
|
+
return f"Model request:\n{parts_str}\n"
|
|
95
|
+
case End():
|
|
96
|
+
return f"End:\n{tabit(n.data.output)}\n"
|
|
97
|
+
case _:
|
|
98
|
+
return f"{type(n)}: {n}"
|
|
99
|
+
|
|
100
|
+
def to_str(self, nodes) -> str:
|
|
101
|
+
"""Convert a list of nodes in a readable format."""
|
|
102
|
+
out = ""
|
|
103
|
+
if isinstance(nodes, Iterable) and not isinstance(nodes, (str, bytes)):
|
|
104
|
+
for n in nodes:
|
|
105
|
+
out += self._node2str(n)
|
|
106
|
+
else:
|
|
107
|
+
# Assume it's a single node
|
|
108
|
+
out += self._node2str(nodes)
|
|
109
|
+
return out
|
aixtools/logging/log_objects.py
CHANGED
|
@@ -38,10 +38,13 @@ class ExceptionWrapper: # pylint: disable=too-few-public-methods
|
|
|
38
38
|
return f"{self.exc_type}: {self.exc_value}\n{self.exc_traceback}"
|
|
39
39
|
|
|
40
40
|
|
|
41
|
-
def is_pickleable(obj):
|
|
41
|
+
def is_pickleable(obj, use_cache: bool = False):
|
|
42
42
|
"""
|
|
43
43
|
Check if an object is pickleable.
|
|
44
|
-
|
|
44
|
+
|
|
45
|
+
use_cache: If True, use the cache to store results of previous checks.
|
|
46
|
+
Why? Some complex objects may have lists of multiple types
|
|
47
|
+
inside, so whether an object is pickleable may depend on its contents.
|
|
45
48
|
"""
|
|
46
49
|
obj_type = type(obj)
|
|
47
50
|
module_name = getattr(obj_type, "__module__", "")
|
|
@@ -50,7 +53,7 @@ def is_pickleable(obj):
|
|
|
50
53
|
if module_name == "fastmcp.utilities.json_schema_type":
|
|
51
54
|
return False
|
|
52
55
|
|
|
53
|
-
if obj_type not in _is_pickleable_cache:
|
|
56
|
+
if not use_cache or obj_type not in _is_pickleable_cache:
|
|
54
57
|
try:
|
|
55
58
|
pickle.loads(pickle.dumps(obj))
|
|
56
59
|
_is_pickleable_cache[obj_type] = True
|
|
@@ -76,32 +79,40 @@ def load_from_log(log_file: Path):
|
|
|
76
79
|
return objects
|
|
77
80
|
|
|
78
81
|
|
|
79
|
-
def safe_deepcopy(obj):
|
|
82
|
+
def safe_deepcopy(obj, use_cache: bool = False):
|
|
80
83
|
"""
|
|
81
84
|
A safe deepcopy function that handles unpickleable objects.
|
|
82
85
|
It uses 'is_pickleable' to check if the object is serializable and
|
|
83
86
|
performs a shallow copy for unpickleable objects.
|
|
87
|
+
|
|
88
|
+
Note: If the object is complex (e.g. has a list or dict of varying objects), using the
|
|
89
|
+
cache may lead to bad results.
|
|
90
|
+
For example you analyze an object with an empty list first, then another object which is non-pickable
|
|
91
|
+
is added, but you use the cache result, resulting in the wrong assumption that the object is pickleable
|
|
92
|
+
So by default the cache is disabled.
|
|
84
93
|
"""
|
|
85
94
|
if isinstance(obj, Exception):
|
|
86
95
|
# Wrap exceptions to make them pickleable
|
|
87
96
|
obj = ExceptionWrapper(obj)
|
|
88
97
|
|
|
89
|
-
if is_pickleable(obj):
|
|
98
|
+
if is_pickleable(obj, use_cache=use_cache):
|
|
90
99
|
return pickle.loads(pickle.dumps(obj)) # Fast path
|
|
91
100
|
|
|
92
101
|
if isinstance(obj, Mapping):
|
|
93
|
-
return {
|
|
102
|
+
return {
|
|
103
|
+
k: safe_deepcopy(v, use_cache=use_cache) for k, v in obj.items() if is_pickleable(k, use_cache=use_cache)
|
|
104
|
+
}
|
|
94
105
|
|
|
95
106
|
if isinstance(obj, Sequence) and not isinstance(obj, str):
|
|
96
|
-
return [safe_deepcopy(item) for item in obj]
|
|
107
|
+
return [safe_deepcopy(item, use_cache=use_cache) for item in obj]
|
|
97
108
|
|
|
98
109
|
if hasattr(obj, "__dict__"):
|
|
99
110
|
copy_obj = copy(obj)
|
|
100
111
|
for attr, value in vars(obj).items():
|
|
101
|
-
if is_pickleable(value):
|
|
102
|
-
setattr(copy_obj, attr,
|
|
112
|
+
if is_pickleable(value, use_cache=use_cache):
|
|
113
|
+
setattr(copy_obj, attr, value)
|
|
103
114
|
else:
|
|
104
|
-
setattr(copy_obj, attr,
|
|
115
|
+
setattr(copy_obj, attr, safe_deepcopy(value, use_cache=use_cache))
|
|
105
116
|
return copy_obj
|
|
106
117
|
|
|
107
118
|
return None # fallback for non-serializable, non-introspectable objects
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Custom middleware for MCP servers
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import traceback
|
|
6
|
+
|
|
7
|
+
from fastmcp.server.middleware.error_handling import ErrorHandlingMiddleware
|
|
8
|
+
from fastmcp.server.middleware.middleware import MiddlewareContext
|
|
9
|
+
|
|
10
|
+
from aixtools.mcp import AixToolError
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AixErrorHandlingMiddleware(ErrorHandlingMiddleware):
|
|
14
|
+
"""Custom middleware class for handling errors in MCP servers."""
|
|
15
|
+
|
|
16
|
+
def log_as_warn_with_traceback(
|
|
17
|
+
self, *, error: Exception, original_error: Exception, context: MiddlewareContext
|
|
18
|
+
) -> None:
|
|
19
|
+
"""Logs provided error as warning.
|
|
20
|
+
original_error is an 'unwrapped' error if applicable, otherwise can be the same as error param.
|
|
21
|
+
"""
|
|
22
|
+
error_type = type(original_error).__name__
|
|
23
|
+
method = context.method or "unknown"
|
|
24
|
+
error_key = f"{error_type}:{method}"
|
|
25
|
+
self.error_counts[error_key] = self.error_counts.get(error_key, 0) + 1
|
|
26
|
+
base_message = f"{method} resulted in {error_type}: {str(error)}"
|
|
27
|
+
if self.include_traceback:
|
|
28
|
+
self.logger.warning(f"{base_message}\n{traceback.format_exc()}")
|
|
29
|
+
if self.error_callback:
|
|
30
|
+
try:
|
|
31
|
+
self.error_callback(error, context)
|
|
32
|
+
except Exception as callback_error: # pylint: disable=broad-exception-caught
|
|
33
|
+
self.logger.warning("Callback failed spectacularly: %s", callback_error)
|
|
34
|
+
|
|
35
|
+
def handle_error(self, error: Exception, context: MiddlewareContext) -> bool:
|
|
36
|
+
"""Custom error logging"""
|
|
37
|
+
if isinstance(error, AixToolError):
|
|
38
|
+
self.log_as_warn_with_traceback(error=error, original_error=error, context=context)
|
|
39
|
+
return True
|
|
40
|
+
|
|
41
|
+
inner_error = error
|
|
42
|
+
while hasattr(inner_error, "__cause__") and inner_error.__cause__ is not None:
|
|
43
|
+
inner_error = inner_error.__cause__
|
|
44
|
+
if isinstance(inner_error, AixToolError):
|
|
45
|
+
self.log_as_warn_with_traceback(error=error, original_error=inner_error, context=context)
|
|
46
|
+
return True
|
|
47
|
+
|
|
48
|
+
return False
|
|
49
|
+
|
|
50
|
+
def _log_error(self, error: Exception, context: MiddlewareContext) -> None:
|
|
51
|
+
"""Override original _log_error method."""
|
|
52
|
+
if self.handle_error(error, context):
|
|
53
|
+
return
|
|
54
|
+
super()._log_error(error, context)
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""Mock agent that replays previously recorded nodes for testing purposes."""
|
|
2
|
+
|
|
3
|
+
import pickle
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from aixtools.logging.log_objects import safe_deepcopy
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class AgentMock:
|
|
11
|
+
"""
|
|
12
|
+
Mock agent that replays previously recorded nodes.
|
|
13
|
+
Used for testing or replaying agent runs without executing the actual agent.
|
|
14
|
+
|
|
15
|
+
Example:
|
|
16
|
+
|
|
17
|
+
# Run an agent and save its nodes
|
|
18
|
+
agent = get_agent(...)
|
|
19
|
+
ret, nodes = await run_agent(agent, prompt, ...)
|
|
20
|
+
|
|
21
|
+
# Use AgentMock to replay the nodes
|
|
22
|
+
agent_mock = AgentMock(nodes=nodes, result_output=ret)
|
|
23
|
+
|
|
24
|
+
# Now we can use agent_mock in place of the original agent
|
|
25
|
+
ret, nodes = await run_agent(agent_mock, prompt, ...)
|
|
26
|
+
|
|
27
|
+
# Save the mock agent to a file
|
|
28
|
+
agent_mock.save(Path("agent_mock.pkl"))
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(self, nodes: list[Any], result_output: str | None = None):
|
|
32
|
+
"""
|
|
33
|
+
Initialize the mock agent with pre-recorded nodes.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
nodes: List of nodes from a previous agent run
|
|
37
|
+
result_output: Optional output to return as the final result
|
|
38
|
+
"""
|
|
39
|
+
self.nodes = nodes
|
|
40
|
+
self.result_output = result_output
|
|
41
|
+
|
|
42
|
+
def save(self, path: Path) -> None:
|
|
43
|
+
"""
|
|
44
|
+
Save the mock agent to a file.
|
|
45
|
+
Uses safe_deepcopy to ensure nodes are serializable.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
path: Path to save the agent mock data
|
|
49
|
+
"""
|
|
50
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
51
|
+
data = {"nodes": safe_deepcopy(self.nodes, use_cache=False), "result_output": self.result_output} # pylint: disable=unexpected-keyword-arg
|
|
52
|
+
with open(path, "wb") as f:
|
|
53
|
+
pickle.dump(data, f)
|
|
54
|
+
|
|
55
|
+
@classmethod
|
|
56
|
+
def load(cls, path: Path) -> "AgentMock":
|
|
57
|
+
"""
|
|
58
|
+
Load a mock agent from a file.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
path: Path to load the agent mock data from
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
AgentMock instance with loaded data
|
|
65
|
+
"""
|
|
66
|
+
with open(path, "rb") as f:
|
|
67
|
+
data = pickle.load(f)
|
|
68
|
+
return cls(nodes=data["nodes"], result_output=data["result_output"])
|
|
69
|
+
|
|
70
|
+
async def __aenter__(self):
|
|
71
|
+
"""Async context manager entry."""
|
|
72
|
+
return self
|
|
73
|
+
|
|
74
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
75
|
+
"""Async context manager exit."""
|
|
76
|
+
|
|
77
|
+
def iter(self, **kwargs) -> "AgentRunMock": # pylint: disable=unused-argument
|
|
78
|
+
"""
|
|
79
|
+
Create an async iterator that replays the recorded nodes.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
prompt: The prompt (ignored in mock, kept for interface compatibility)
|
|
83
|
+
usage_limits: Usage limits (ignored in mock, kept for interface compatibility)
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
An async iterator that yields the recorded nodes
|
|
87
|
+
"""
|
|
88
|
+
return AgentRunMock(self.nodes, self.result_output)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class AgentRunMock:
|
|
92
|
+
"""
|
|
93
|
+
Mock agent run that yields pre-recorded nodes.
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
def __init__(self, nodes: list[Any], result_output: str | None = None):
|
|
97
|
+
"""
|
|
98
|
+
Initialize the mock agent run.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
nodes: List of nodes to replay
|
|
102
|
+
result_output: Optional output to return as the final result
|
|
103
|
+
"""
|
|
104
|
+
self.nodes = nodes
|
|
105
|
+
self.result = MockResult(result_output)
|
|
106
|
+
self._index = 0
|
|
107
|
+
|
|
108
|
+
async def __aenter__(self):
|
|
109
|
+
"""Async context manager entry."""
|
|
110
|
+
return self
|
|
111
|
+
|
|
112
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
113
|
+
"""Async context manager exit."""
|
|
114
|
+
|
|
115
|
+
def __aiter__(self):
|
|
116
|
+
"""Return self as async iterator."""
|
|
117
|
+
return self
|
|
118
|
+
|
|
119
|
+
async def __anext__(self):
|
|
120
|
+
"""Yield the next node from the recorded list."""
|
|
121
|
+
if self._index >= len(self.nodes):
|
|
122
|
+
raise StopAsyncIteration
|
|
123
|
+
node = self.nodes[self._index]
|
|
124
|
+
self._index += 1
|
|
125
|
+
return node
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class MockResult: # pylint: disable=too-few-public-methods
|
|
129
|
+
"""
|
|
130
|
+
Mock result object that mimics the agent result structure.
|
|
131
|
+
"""
|
|
132
|
+
|
|
133
|
+
def __init__(self, output: str | None = None):
|
|
134
|
+
"""
|
|
135
|
+
Initialize the mock result.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
output: The output to return
|
|
139
|
+
"""
|
|
140
|
+
self.output = output
|
|
141
|
+
|
|
142
|
+
def __str__(self):
|
|
143
|
+
return f"MockResult(output={self.output})"
|
aixtools/utils/utils.py
CHANGED
|
@@ -32,6 +32,18 @@ def find_file(path: Path, glob="*.pdf"):
|
|
|
32
32
|
yield from path.rglob(glob)
|
|
33
33
|
|
|
34
34
|
|
|
35
|
+
def is_multiline(s: str) -> bool:
|
|
36
|
+
"""Check if a string is multiline."""
|
|
37
|
+
s = str(s).rstrip()
|
|
38
|
+
return "\n" in s
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def is_too_long(s: str, max_length: int = 200) -> bool:
|
|
42
|
+
"""Check if a string is too long."""
|
|
43
|
+
s = str(s).rstrip()
|
|
44
|
+
return len(s) > max_length
|
|
45
|
+
|
|
46
|
+
|
|
35
47
|
def prepend_all_lines(msg, prepend="\t", skip_first_line: bool = False) -> str:
|
|
36
48
|
"""Prepend all lines of a message with a prepend."""
|
|
37
49
|
out = ""
|
|
@@ -59,7 +71,7 @@ def remove_quotes(s):
|
|
|
59
71
|
"""
|
|
60
72
|
if s is None:
|
|
61
73
|
return None
|
|
62
|
-
s = s.strip()
|
|
74
|
+
s = str(s).strip()
|
|
63
75
|
while (
|
|
64
76
|
(s.startswith('"') and s.endswith('"'))
|
|
65
77
|
or (s.startswith("'") and s.endswith("'"))
|
|
@@ -76,9 +88,29 @@ def remove_quotes(s):
|
|
|
76
88
|
return s
|
|
77
89
|
|
|
78
90
|
|
|
79
|
-
def tabit(s
|
|
91
|
+
def tabit(s, prefix="\t|") -> str:
|
|
80
92
|
"""Add a prefix to each line of a string for improved readability."""
|
|
81
|
-
|
|
93
|
+
s = str(s)
|
|
94
|
+
return prefix + s.replace("\n", f"\n{prefix}")
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def to_json_pretty_print(obj) -> str:
|
|
98
|
+
"""Convert to a pretty-printed JSON string if possible."""
|
|
99
|
+
if isinstance(obj, str):
|
|
100
|
+
# Already a string, try to parse as JSON, then pretty print
|
|
101
|
+
s = obj
|
|
102
|
+
try:
|
|
103
|
+
obj = json.loads(s)
|
|
104
|
+
return json.dumps(obj, indent=2)
|
|
105
|
+
except Exception as _: # pylint: disable=broad-exception-caught
|
|
106
|
+
# Not a JSON string, return as is
|
|
107
|
+
return s
|
|
108
|
+
# Not a string, convert to pretty JSON
|
|
109
|
+
try:
|
|
110
|
+
return json.dumps(obj, indent=2)
|
|
111
|
+
except Exception as _: # pylint: disable=broad-exception-caught
|
|
112
|
+
# Fallback to str representation
|
|
113
|
+
return str(obj)
|
|
82
114
|
|
|
83
115
|
|
|
84
116
|
def to_str(data) -> str:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aixtools
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.17
|
|
4
4
|
Summary: Tools for AI exploration and debugging
|
|
5
5
|
Requires-Python: >=3.11.2
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -63,6 +63,7 @@ Testing Tools & Evals
|
|
|
63
63
|
- Tool Doctor System - `aixtools/tools/doctor/`
|
|
64
64
|
- Tool Recommendation Engine - `aixtools/tools/doctor/tool_recommendation.py`
|
|
65
65
|
- FaultyMCP - `aixtools/mcp/faulty_mcp.py`
|
|
66
|
+
- Agent Mock - `aixtools/testing/agent_mock.py`
|
|
66
67
|
|
|
67
68
|
Databases
|
|
68
69
|
- Database Integration - `aixtools/db/`
|
|
@@ -524,6 +525,21 @@ The framework includes a custom scoring system with [`average_assertions`](aixto
|
|
|
524
525
|
|
|
525
526
|
AIXtools provides comprehensive testing utilities and diagnostic tools for AI agent development and debugging.
|
|
526
527
|
|
|
528
|
+
### Running Tests
|
|
529
|
+
|
|
530
|
+
Execute the test suite using the provided scripts:
|
|
531
|
+
|
|
532
|
+
```bash
|
|
533
|
+
# Run all tests
|
|
534
|
+
./scripts/test.sh
|
|
535
|
+
|
|
536
|
+
# Run unit tests only
|
|
537
|
+
./scripts/test_unit.sh
|
|
538
|
+
|
|
539
|
+
# Run integration tests only
|
|
540
|
+
./scripts/test_integration.sh
|
|
541
|
+
```
|
|
542
|
+
|
|
527
543
|
### Testing Utilities
|
|
528
544
|
|
|
529
545
|
The testing module provides mock tools, model patching, and test utilities for comprehensive agent testing.
|
|
@@ -544,10 +560,6 @@ cached_response = cache.get_cached_response("test_prompt")
|
|
|
544
560
|
test_model = AixTestModel()
|
|
545
561
|
```
|
|
546
562
|
|
|
547
|
-
### Tool Doctor System
|
|
548
|
-
|
|
549
|
-
Automated tool analysis and recommendation system for optimizing agent tool usage and analyzing MCP servers.
|
|
550
|
-
|
|
551
563
|
#### MCP Tool Doctor
|
|
552
564
|
|
|
553
565
|
Analyze tools from MCP (Model Context Protocol) servers and receive AI-powered recommendations for improvement.
|
|
@@ -586,7 +598,7 @@ tool_doctor_mcp --stdio-command fastmcp --stdio-args run my_server.py --debug
|
|
|
586
598
|
# --debug Enable debug output
|
|
587
599
|
```
|
|
588
600
|
|
|
589
|
-
####
|
|
601
|
+
#### Tool Doctor
|
|
590
602
|
|
|
591
603
|
Analyze tool usage patterns from agent logs and get optimization recommendations.
|
|
592
604
|
|
|
@@ -681,19 +693,27 @@ python -m aixtools.mcp.faulty_mcp \
|
|
|
681
693
|
--prob-on-get-crash 0.1
|
|
682
694
|
```
|
|
683
695
|
|
|
684
|
-
###
|
|
696
|
+
### Agent Mock
|
|
685
697
|
|
|
686
|
-
|
|
698
|
+
Replay previously recorded agent runs without executing the actual agent. Useful for testing, debugging, and creating reproducible test cases.
|
|
687
699
|
|
|
688
|
-
```
|
|
689
|
-
|
|
690
|
-
|
|
700
|
+
```python
|
|
701
|
+
from aixtools.testing.agent_mock import AgentMock
|
|
702
|
+
from aixtools.agents.agent import get_agent, run_agent
|
|
691
703
|
|
|
692
|
-
# Run
|
|
693
|
-
|
|
704
|
+
# Run an agent and capture its execution
|
|
705
|
+
agent = get_agent(system_prompt="You are a helpful assistant.")
|
|
706
|
+
result, nodes = await run_agent(agent, "Explain quantum computing")
|
|
694
707
|
|
|
695
|
-
#
|
|
696
|
-
|
|
708
|
+
# Create a mock agent from the recorded nodes
|
|
709
|
+
agent_mock = AgentMock(nodes=nodes, result_output=result)
|
|
710
|
+
|
|
711
|
+
# Save the mock for later use
|
|
712
|
+
agent_mock.save(Path("test_data/quantum_mock.pkl"))
|
|
713
|
+
|
|
714
|
+
# Load and replay the mock agent
|
|
715
|
+
loaded_mock = AgentMock.load(Path("test_data/quantum_mock.pkl"))
|
|
716
|
+
result, nodes = await run_agent(loaded_mock, "any prompt") # Returns recorded nodes
|
|
697
717
|
```
|
|
698
718
|
|
|
699
719
|
## Chainlit & HTTP Server
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
aixtools/__init__.py,sha256=9NGHm7LjsQmsvjTZvw6QFJexSvAU4bCoN_KBk9SCa00,260
|
|
2
|
-
aixtools/_version.py,sha256=
|
|
2
|
+
aixtools/_version.py,sha256=sRnPbdnyLakHrE7uBPRC_AQNPiFphtVIa4BPaftkqk4,706
|
|
3
3
|
aixtools/app.py,sha256=JzQ0nrv_bjDQokllIlGHOV0HEb-V8N6k_nGQH-TEsVU,5227
|
|
4
4
|
aixtools/chainlit.md,sha256=yC37Ly57vjKyiIvK4oUvf4DYxZCwH7iocTlx7bLeGLU,761
|
|
5
5
|
aixtools/context.py,sha256=I_MD40ZnvRm5WPKAKqBUAdXIf8YaurkYUUHSVVy-QvU,598
|
|
@@ -25,8 +25,11 @@ aixtools/a2a/google_sdk/utils.py,sha256=4VIPV2GtG5IRY-KSZN6iRMS3ntxG2uKgd_d5tQvn
|
|
|
25
25
|
aixtools/a2a/google_sdk/pydantic_ai_adapter/agent_executor.py,sha256=8VuU2WXeSHUK3_rRm_mjX6elqdC9NA2uz1aELzeC8BU,9784
|
|
26
26
|
aixtools/a2a/google_sdk/pydantic_ai_adapter/storage.py,sha256=nGoVL7MPoZJW7iVR71laqpUYP308yFKZIifJtvUgpiU,878
|
|
27
27
|
aixtools/agents/__init__.py,sha256=MAW196S2_G7uGqv-VNjvlOETRfuV44WlU1leO7SiR0A,282
|
|
28
|
-
aixtools/agents/agent.py,sha256=
|
|
28
|
+
aixtools/agents/agent.py,sha256=loBbSrRY-QZH8wec27JjNi_h1jhqkZ9-UrR0lqPNMWI,7725
|
|
29
29
|
aixtools/agents/agent_batch.py,sha256=0Zu9yNCRPAQZPjXQ-dIUAmP1uGTVbxVt7xvnMpoJMjU,2251
|
|
30
|
+
aixtools/agents/nodes_to_md.py,sha256=hAT8dgiZTG4uGoSgeRZkIJ7zgkQUNpdIr8KSFhjWAH0,7515
|
|
31
|
+
aixtools/agents/nodes_to_message.py,sha256=ZqcmxUNf4esiCTRk37wWP1LquhqNsCmydvMr4kjZEjw,1012
|
|
32
|
+
aixtools/agents/nodes_to_str.py,sha256=UkOu5Nry827J4H_ohQU3tPBfJxtr3p6FfCfWoUy5uIs,4325
|
|
30
33
|
aixtools/agents/print_nodes.py,sha256=wVTngNfqM0As845WTRz6G3Rei_Gr3HuBlvu-G_eXuig,1665
|
|
31
34
|
aixtools/agents/prompt.py,sha256=p9OYnyJ4-MyGXwHPrQeJBhZ2a3RV2HqhtdUUCrTMsAQ,3361
|
|
32
35
|
aixtools/auth/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -52,10 +55,11 @@ aixtools/log_view/node_summary.py,sha256=EJjnBqdBWI-_bI-4nfTxwaost3mtiufb5cK7T54
|
|
|
52
55
|
aixtools/logfilters/__init__.py,sha256=pTD8ujCqjPWBCeB7yv7lmCtnA2KXOnkIv0HExDagkXs,129
|
|
53
56
|
aixtools/logfilters/context_filter.py,sha256=zR3Bnv3fCqXLeb7bCFTmlnWhC6dFIvUb-u712tOnUPk,2259
|
|
54
57
|
aixtools/logging/__init__.py,sha256=b5oYyGQDUHHxhRtzqKUaQPv8hQeWw54rzDXSV8lDY1w,613
|
|
55
|
-
aixtools/logging/log_objects.py,sha256=
|
|
58
|
+
aixtools/logging/log_objects.py,sha256=gohsgcfyr8vsY7G_hfmj973-Ek1_PN-bMMLEUA-4u6U,7708
|
|
56
59
|
aixtools/logging/logging_config.py,sha256=LvxV3C75-I0096PpcCIbgM-Cp998LzWXeMM14HYbU20,4985
|
|
57
60
|
aixtools/logging/mcp_log_models.py,sha256=7-H2GJXiiyLhpImuyLLftAGG4skxJal8Swax0ob04MY,3463
|
|
58
61
|
aixtools/logging/mcp_logger.py,sha256=d2I5l4t0d6rQH17w23FpE1IUD8Ax-mSaKfByCH86q4I,6257
|
|
62
|
+
aixtools/logging/mcp_middleware.py,sha256=0kpTAwvz9Fd_mFDP3J9ldH4dPP-bhcUjLJKELGOx0IQ,2257
|
|
59
63
|
aixtools/logging/model_patch_logging.py,sha256=CW5-kKI-zNEgZhNV4vx3EQu6fbrEtX7VjA6fE5loRLQ,2916
|
|
60
64
|
aixtools/logging/open_telemetry.py,sha256=fJjF1ou_8GyfNfbyWDQPGK6JAUrUaPwURYPHhXEtDBE,1121
|
|
61
65
|
aixtools/mcp/__init__.py,sha256=Qp4uD1RtypCYgzWrt_ThBVxa5-CFBgcwMWkHbHFUQ54,232
|
|
@@ -71,6 +75,7 @@ aixtools/server/app_mounter.py,sha256=0tJ0tC140ezAjnYdlhpLJQjY-TO8NVw7D8LseYCCVY
|
|
|
71
75
|
aixtools/server/path.py,sha256=nI4yRQcE6gjKx5GG3PmHf7iT1FelT6Q8Xhw4ol9O1e0,5219
|
|
72
76
|
aixtools/server/utils.py,sha256=tZWITIx6M-luV9yve4j3rPtYGSSA6zWS0JWEAySne_M,2276
|
|
73
77
|
aixtools/testing/__init__.py,sha256=mlmaAR2gmS4SbsYNCxnIprmFpFp-syjgVUkpUszo3mE,166
|
|
78
|
+
aixtools/testing/agent_mock.py,sha256=D4DrRGaeKuWEhq8hdB5s9PdfLAXHMKiC3JA7M8yN13o,4244
|
|
74
79
|
aixtools/testing/aix_test_model.py,sha256=i0xBdmpKoEfJHle6JDmcoJLUENN8Eqt181_WZ7XtDdU,6240
|
|
75
80
|
aixtools/testing/mock_tool.py,sha256=4I0LxxSkLhGIKM2YxCP3cnYI8IYJjdKhfwGZ3dioXsM,2465
|
|
76
81
|
aixtools/testing/model_patch_cache.py,sha256=238gKC_gSpR3BkeejhetObOkpOR1l2Iz3A6B_eUTRNc,10158
|
|
@@ -84,13 +89,13 @@ aixtools/utils/config_util.py,sha256=3Ya4Qqhj1RJ1qtTTykQ6iayf5uxlpigPXgEJlTi1wn4
|
|
|
84
89
|
aixtools/utils/enum_with_description.py,sha256=zjSzWxG74eR4x7dpmb74pLTYCWNSMvauHd7_9LpDYIw,1088
|
|
85
90
|
aixtools/utils/files.py,sha256=8JnxwHJRJcjWCdFpjzWmo0po2fRg8esj4H7sOxElYXU,517
|
|
86
91
|
aixtools/utils/persisted_dict.py,sha256=0jQzV7oF-A6Or-HjcU6V7aMXWQL67SOKpULgmtFwAfg,3110
|
|
87
|
-
aixtools/utils/utils.py,sha256=
|
|
92
|
+
aixtools/utils/utils.py,sha256=EP4q50wuIwgICzv3unMqDS93HuuySxE7SezqxX4HdKg,5840
|
|
88
93
|
aixtools/utils/chainlit/cl_agent_show.py,sha256=vaRuowp4BRvhxEr5hw0zHEJ7iaSF_5bo_9BH7pGPPpw,4398
|
|
89
94
|
aixtools/utils/chainlit/cl_utils.py,sha256=fxaxdkcZg6uHdM8uztxdPowg3a2f7VR7B26VPY4t-3c,5738
|
|
90
95
|
aixtools/vault/__init__.py,sha256=fsr_NuX3GZ9WZ7dGfe0gp_5-z3URxAfwVRXw7Xyc0dU,141
|
|
91
96
|
aixtools/vault/vault.py,sha256=9dZLWdZQk9qN_Q9Djkofw9LUKnJqnrX5H0fGusVLBhA,6037
|
|
92
|
-
aixtools-0.2.
|
|
93
|
-
aixtools-0.2.
|
|
94
|
-
aixtools-0.2.
|
|
95
|
-
aixtools-0.2.
|
|
96
|
-
aixtools-0.2.
|
|
97
|
+
aixtools-0.2.17.dist-info/METADATA,sha256=dCP4ss1Kz-AZWMQcmaHxR-HfqrtlSEVhIdZgff8ncvU,27958
|
|
98
|
+
aixtools-0.2.17.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
99
|
+
aixtools-0.2.17.dist-info/entry_points.txt,sha256=q8412TG4T0S8K0SKeWp2vkVPIDYQs0jNoHqcQ7qxOiA,155
|
|
100
|
+
aixtools-0.2.17.dist-info/top_level.txt,sha256=wBn-rw9bCtxrR4AYEYgjilNCUVmKY0LWby9Zan2PRJM,9
|
|
101
|
+
aixtools-0.2.17.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|