aixtools 0.1.3__py3-none-any.whl → 0.1.5__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.

Files changed (46) hide show
  1. aixtools/_version.py +2 -2
  2. aixtools/a2a/app.py +1 -1
  3. aixtools/a2a/google_sdk/__init__.py +0 -0
  4. aixtools/a2a/google_sdk/card.py +27 -0
  5. aixtools/a2a/google_sdk/pydantic_ai_adapter/agent_executor.py +199 -0
  6. aixtools/a2a/google_sdk/pydantic_ai_adapter/storage.py +26 -0
  7. aixtools/a2a/google_sdk/remote_agent_connection.py +88 -0
  8. aixtools/a2a/google_sdk/utils.py +59 -0
  9. aixtools/agents/prompt.py +97 -0
  10. aixtools/context.py +5 -0
  11. aixtools/google/client.py +25 -0
  12. aixtools/logging/logging_config.py +45 -0
  13. aixtools/mcp/client.py +274 -0
  14. aixtools/mcp/faulty_mcp.py +7 -7
  15. aixtools/server/utils.py +3 -3
  16. aixtools/utils/config.py +6 -0
  17. aixtools/utils/files.py +17 -0
  18. aixtools/utils/utils.py +7 -0
  19. {aixtools-0.1.3.dist-info → aixtools-0.1.5.dist-info}/METADATA +3 -1
  20. {aixtools-0.1.3.dist-info → aixtools-0.1.5.dist-info}/RECORD +45 -14
  21. {aixtools-0.1.3.dist-info → aixtools-0.1.5.dist-info}/top_level.txt +1 -0
  22. scripts/test.sh +23 -0
  23. tests/__init__.py +0 -0
  24. tests/unit/__init__.py +0 -0
  25. tests/unit/a2a/__init__.py +0 -0
  26. tests/unit/a2a/google_sdk/__init__.py +0 -0
  27. tests/unit/a2a/google_sdk/pydantic_ai_adapter/__init__.py +0 -0
  28. tests/unit/a2a/google_sdk/pydantic_ai_adapter/test_agent_executor.py +188 -0
  29. tests/unit/a2a/google_sdk/pydantic_ai_adapter/test_storage.py +156 -0
  30. tests/unit/a2a/google_sdk/test_card.py +114 -0
  31. tests/unit/a2a/google_sdk/test_remote_agent_connection.py +413 -0
  32. tests/unit/a2a/google_sdk/test_utils.py +208 -0
  33. tests/unit/agents/__init__.py +0 -0
  34. tests/unit/agents/test_prompt.py +363 -0
  35. tests/unit/google/__init__.py +1 -0
  36. tests/unit/google/test_client.py +233 -0
  37. tests/unit/mcp/__init__.py +0 -0
  38. tests/unit/mcp/test_client.py +242 -0
  39. tests/unit/server/__init__.py +0 -0
  40. tests/unit/server/test_path.py +225 -0
  41. tests/unit/server/test_utils.py +362 -0
  42. tests/unit/utils/__init__.py +0 -0
  43. tests/unit/utils/test_files.py +146 -0
  44. aixtools/a2a/__init__.py +0 -5
  45. {aixtools-0.1.3.dist-info → aixtools-0.1.5.dist-info}/WHEEL +0 -0
  46. {aixtools-0.1.3.dist-info → aixtools-0.1.5.dist-info}/entry_points.txt +0 -0
aixtools/mcp/client.py ADDED
@@ -0,0 +1,274 @@
1
+ """MCP server utilities with caching and robust error handling."""
2
+
3
+ import asyncio
4
+ from typing import Any
5
+
6
+ import anyio
7
+ from cachebox import TTLCache
8
+ from mcp import types as mcp_types
9
+ from mcp.shared.exceptions import McpError
10
+ from pydantic_ai import RunContext, exceptions
11
+ from pydantic_ai.mcp import MCPServerStreamableHTTP, ToolResult
12
+ from pydantic_ai.toolsets.abstract import ToolsetTool
13
+
14
+ from aixtools.context import SessionIdTuple
15
+ from aixtools.logging.logging_config import get_logger
16
+
17
+ MCP_TOOL_CACHE_TTL = 300 # 5 minutes
18
+ DEFAULT_MCP_CONNECTION_TIMEOUT = 30
19
+ CACHE_KEY = "TOOL_LIST"
20
+
21
+ logger = get_logger(__name__)
22
+
23
+
24
+ def get_mcp_headers(session_id_tuple: SessionIdTuple) -> dict[str, str] | None:
25
+ """
26
+ Generate headers for MCP server requests.
27
+
28
+ This function creates a dictionary of headers to be used in requests to
29
+ the MCP servers. If a `user_id` or `session_id` is provided, they are
30
+ included in the headers.
31
+
32
+ Args:
33
+ session_id_tuple (SessionIdTuple): user_id and session_id tuple
34
+ Returns:
35
+ dict[str, str] | None: A dictionary of headers for MCP server requests,
36
+ or None if neither user_id nor session_id is
37
+ provided. When None is returned, default headers
38
+ from the client or transport will be used.
39
+ """
40
+ headers = None
41
+ user_id, session_id = session_id_tuple
42
+ if session_id or user_id:
43
+ headers = {}
44
+ if session_id:
45
+ headers["session-id"] = session_id
46
+ if user_id:
47
+ headers["user-id"] = user_id
48
+ return headers
49
+
50
+
51
+ def get_configured_mcp_servers(
52
+ session_id_tuple: SessionIdTuple, mcp_urls: list[str], timeout: int = DEFAULT_MCP_CONNECTION_TIMEOUT
53
+ ):
54
+ """
55
+ Retrieve the configured MCP server instances with optional caching.
56
+
57
+ Context values `user_id` and `session_id` are included in the headers for each server request.
58
+
59
+ Each server is wrapped in a try-except block to isolate them from each other.
60
+ If one server fails, it won't affect the others.
61
+
62
+ Args:
63
+ session_id_tuple (SessionIdTuple): A tuple containing (user_id, session_id).
64
+ mcp_urls: (list[str], optional): A list of MCP server URLs to use.
65
+ timeout (int, optional): Timeout in seconds for MCP server connections. Defaults to 30 seconds.
66
+ Returns:
67
+ list[MCPServerStreamableHTTP]: A list of configured MCP server instances. If
68
+ neither user_id nor session_id is provided, the
69
+ server instances will use default headers defined
70
+ by the underlying HTTP implementation.
71
+ """
72
+ headers = get_mcp_headers(session_id_tuple)
73
+
74
+ return [CachedMCPServerStreamableHTTP(url=url, headers=headers, timeout=timeout) for url in mcp_urls]
75
+
76
+
77
+ class CachedMCPServerStreamableHTTP(MCPServerStreamableHTTP):
78
+ """StreamableHTTP MCP server with cachebox-based TTL caching and robust error handling.
79
+
80
+ This class addresses the cancellation propagation issue by:
81
+ 1. Using complete task isolation to prevent CancelledError propagation
82
+ 2. Implementing comprehensive error handling for all MCP operations
83
+ 3. Using fallback mechanisms when servers become unavailable
84
+ 4. Overriding pydantic_ai methods to fix variable scoping bug
85
+ """
86
+
87
+ def __init__(self, **kwargs):
88
+ super().__init__(**kwargs)
89
+ self._tools_cache = TTLCache(maxsize=1, ttl=MCP_TOOL_CACHE_TTL)
90
+ self._tools_list = None
91
+ self._isolation_lock = asyncio.Lock() # Lock for critical operations
92
+
93
+ async def _run_direct_or_isolated(self, func, fallback, timeout: float | None):
94
+ """Run a coroutine in complete isolation to prevent cancellation propagation.
95
+
96
+ Args:
97
+ func: Function that returns a coroutine to run
98
+ fallback: Function that takes an exception and returns a fallback value
99
+ timeout: Timeout in seconds. If None, then direct run is performed
100
+
101
+ Returns:
102
+ The result of the coroutine on success, or fallback value on any exception
103
+ """
104
+ try:
105
+ if timeout is None:
106
+ return await func()
107
+
108
+ task = asyncio.create_task(func())
109
+
110
+ # Use asyncio.wait to prevent cancellation propagation
111
+ done, pending = await asyncio.wait([task], timeout=timeout)
112
+
113
+ if pending:
114
+ # Cancel pending tasks safely
115
+ for t in pending:
116
+ t.cancel()
117
+ try:
118
+ await t
119
+ except (asyncio.CancelledError, Exception): # pylint: disable=broad-except
120
+ pass
121
+ raise TimeoutError(f"Task timed out after {timeout} seconds")
122
+
123
+ # Get result from completed task
124
+ completed_task = done.pop()
125
+ if exc := completed_task.exception():
126
+ raise exc
127
+ return completed_task.result()
128
+
129
+ except exceptions.ModelRetry as exc:
130
+ logger.warning("MCP %s: %s ModelRetry: %s", self.url, func.__name__, exc)
131
+ raise
132
+ except TimeoutError as exc:
133
+ logger.warning("MCP %s: %s timed out: %s", self.url, func.__name__, exc)
134
+ return fallback(exc)
135
+ except asyncio.CancelledError as exc:
136
+ logger.warning("MCP %s: %s was cancelled", self.url, func.__name__)
137
+ return fallback(exc)
138
+ except anyio.ClosedResourceError as exc:
139
+ logger.warning("MCP %s: %s closed resource.", self.url, func.__name__)
140
+ return fallback(exc)
141
+ except Exception as exc: # pylint: disable=broad-except
142
+ if str(exc) == "Attempted to exit cancel scope in a different task than it was entered in":
143
+ logger.warning("MCP %s: %s enter/exit cancel scope task mismatch.", self.url, func.__name__)
144
+ else:
145
+ logger.warning("MCP %s: %s exception %s: %s", self.url, func.__name__, type(exc), exc)
146
+ return fallback(exc)
147
+
148
+ async def __aenter__(self):
149
+ """Enter the context of the cached MCP server with complete cancellation isolation."""
150
+ async with self._isolation_lock:
151
+
152
+ async def direct_init():
153
+ return await super(CachedMCPServerStreamableHTTP, self).__aenter__() # pylint: disable=super-with-arguments
154
+
155
+ def fallback(_exc):
156
+ self._client = None
157
+ return self
158
+
159
+ return await self._run_direct_or_isolated(direct_init, fallback, timeout=None)
160
+
161
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
162
+ """Exit the context of the cached MCP server with complete cancellation isolation."""
163
+ async with self._isolation_lock:
164
+ # If we're being cancelled, just clean up
165
+ if exc_type is asyncio.CancelledError:
166
+ logger.warning("MCP %s: __aexit__ called with cancellation - cleaning up", self.url)
167
+ self._client = None
168
+ return True
169
+
170
+ # If client is already None, skip cleanup
171
+ if not self._client:
172
+ logger.warning("MCP %s: is uninitialized -> skipping cleanup", self.url)
173
+ return True
174
+
175
+ async def direct_cleanup():
176
+ return await super(CachedMCPServerStreamableHTTP, self).__aexit__(exc_type, exc_val, exc_tb) # pylint: disable=super-with-arguments
177
+
178
+ def fallback(_exc):
179
+ self._client = None
180
+ return True # Suppress exceptions to prevent propagation
181
+
182
+ return await self._run_direct_or_isolated(direct_cleanup, fallback, timeout=None)
183
+
184
+ async def list_tools(self) -> list[mcp_types.Tool]:
185
+ """Override to fix variable scoping bug and add caching with cancellation isolation."""
186
+ # If client is not initialized, return empty list
187
+ if not self._client:
188
+ logger.warning("MCP %s: is uninitialized -> no tools", self.url)
189
+ return []
190
+
191
+ # First, check if we have a valid cached result
192
+ if CACHE_KEY in self._tools_cache:
193
+ logger.info("Using cached tools for %s", self.url)
194
+ return self._tools_cache[CACHE_KEY]
195
+
196
+ # Create isolated task to prevent cancellation propagation
197
+ async def isolated_list_tools():
198
+ """Isolated list_tools with variable scoping bug fix."""
199
+ result = None # Initialize to prevent UnboundLocalError
200
+ async with self: # Ensure server is running
201
+ result = await self._client.list_tools()
202
+ if result:
203
+ self._tools_list = result.tools or []
204
+ self._tools_cache[CACHE_KEY] = self._tools_list
205
+ logger.info("MCP %s: list_tools returned %d tools", self.url, len(self._tools_list))
206
+ else:
207
+ logger.warning("MCP %s: list_tools returned no result", self.url)
208
+ return self._tools_list or []
209
+
210
+ def fallback(_exc):
211
+ return self._tools_list or []
212
+
213
+ return await self._run_direct_or_isolated(isolated_list_tools, fallback, timeout=5.0)
214
+
215
+ async def call_tool(
216
+ self,
217
+ name: str,
218
+ tool_args: dict[str, Any],
219
+ ctx: RunContext[Any],
220
+ tool: ToolsetTool[Any],
221
+ ) -> ToolResult:
222
+ """Call tool with complete isolation from cancellation using patched pydantic_ai."""
223
+ logger.info("MCP %s: call_tool '%s' started.", self.url, name)
224
+
225
+ # Early returns for uninitialized servers
226
+ if not self._client:
227
+ logger.warning("MCP %s: is uninitialized -> cannot call tool", self.url)
228
+ return f"There was an error with calling tool '{name}': MCP connection is uninitialized."
229
+
230
+ # Create isolated task to prevent cancellation propagation
231
+ async def isolated_call_tool():
232
+ """Isolated call_tool using patched pydantic_ai methods."""
233
+ return await super(CachedMCPServerStreamableHTTP, self).call_tool(name, tool_args, ctx, tool) # pylint: disable=super-with-arguments
234
+
235
+ def fallback(exc):
236
+ return f"Exception {type(exc)} when calling tool '{name}': {exc}. Consider alternative approaches."
237
+
238
+ result = await self._run_direct_or_isolated(isolated_call_tool, fallback, timeout=3600.0)
239
+ logger.info("MCP %s: call_tool '%s' completed.", self.url, name)
240
+ return result
241
+
242
+ async def direct_call_tool(
243
+ self, name: str, args: dict[str, Any], metadata: dict[str, Any] | None = None
244
+ ) -> ToolResult:
245
+ """Override to fix variable scoping bug in direct_call_tool."""
246
+ result = None # Initialize to prevent UnboundLocalError
247
+ async with self: # Ensure server is running
248
+ try:
249
+ result = await self._client.send_request(
250
+ mcp_types.ClientRequest(
251
+ mcp_types.CallToolRequest(
252
+ method="tools/call",
253
+ params=mcp_types.CallToolRequestParams(
254
+ name=name,
255
+ arguments=args,
256
+ _meta=mcp_types.RequestParams.Meta(**metadata) if metadata else None,
257
+ ),
258
+ )
259
+ ),
260
+ mcp_types.CallToolResult,
261
+ )
262
+ except McpError as e:
263
+ raise exceptions.ModelRetry(e.error.message)
264
+
265
+ if not result:
266
+ raise exceptions.ModelRetry("No result from MCP server")
267
+
268
+ content = [await self._map_tool_result_part(part) for part in result.content]
269
+
270
+ if result.isError:
271
+ text = "\n".join(str(part) for part in content)
272
+ raise exceptions.ModelRetry(text)
273
+
274
+ return content[0] if len(content) == 1 else content
@@ -128,19 +128,19 @@ class StarletteErrorMiddleware: # pylint: disable=too-few-public-methods
128
128
  logger.info("[StarletteErrorMiddleware] Simulating 404 error on DELETE request")
129
129
  should_inject_404 = True
130
130
 
131
- if http_method == "GET":
132
- random_number = random()
133
- logger.info("[StarletteErrorMiddleware] random number: %f", random_number)
134
- if random_number < config.prob_on_get_crash:
135
- logger.warning("[StarletteErrorMiddleware] Simulating server crash on GET request!")
136
- os.kill(os.getpid(), 9)
137
-
138
131
  async def logging_receive():
139
132
  nonlocal should_inject_404
140
133
  message = await receive()
141
134
  logger.info("[StarletteErrorMiddleware] Received message: %s", str(message))
142
135
 
143
136
  if message["type"] == "http.request": # pylint: disable=too-many-nested-blocks
137
+ if http_method == "GET":
138
+ random_number = random()
139
+ logger.info("[StarletteErrorMiddleware] random number: %f", random_number)
140
+ if random_number < config.prob_on_get_crash:
141
+ logger.warning("[StarletteErrorMiddleware] Simulating server crash on GET request!")
142
+ os.kill(os.getpid(), 9)
143
+
144
144
  body = message.get("body", b"")
145
145
  if body:
146
146
  body_parts.append(body)
aixtools/server/utils.py CHANGED
@@ -8,7 +8,7 @@ from functools import wraps
8
8
  from fastmcp import Context
9
9
  from fastmcp.server import dependencies
10
10
 
11
- from ..context import session_id_var, user_id_var
11
+ from ..context import DEFAULT_SESSION_ID, DEFAULT_USER_ID, session_id_var, user_id_var
12
12
 
13
13
 
14
14
  def get_session_id_tuple(ctx: Context | None = None) -> tuple[str, str]:
@@ -18,9 +18,9 @@ def get_session_id_tuple(ctx: Context | None = None) -> tuple[str, str]:
18
18
  Returns: Tuple of (user_id, session_id).
19
19
  """
20
20
  user_id = get_user_id_from_request(ctx)
21
- user_id = user_id or user_id_var.get("default_user")
21
+ user_id = user_id or user_id_var.get(DEFAULT_USER_ID)
22
22
  session_id = get_session_id_from_request(ctx)
23
- session_id = session_id or session_id_var.get("default_session")
23
+ session_id = session_id or session_id_var.get(DEFAULT_SESSION_ID)
24
24
  return user_id, session_id
25
25
 
26
26
 
aixtools/utils/config.py CHANGED
@@ -9,6 +9,7 @@ from pathlib import Path
9
9
  from dotenv import dotenv_values, load_dotenv
10
10
 
11
11
  from aixtools.utils.config_util import find_env_file, get_project_root, get_variable_env
12
+ from aixtools.utils.utils import str2bool
12
13
 
13
14
  # Debug mode
14
15
  LOG_LEVEL = logging.DEBUG
@@ -116,3 +117,8 @@ BEDROCK_MODEL_NAME = get_variable_env("BEDROCK_MODEL_NAME", allow_empty=True)
116
117
  # LogFire
117
118
  LOGFIRE_TOKEN = get_variable_env("LOGFIRE_TOKEN", True, "")
118
119
  LOGFIRE_TRACES_ENDPOINT = get_variable_env("LOGFIRE_TRACES_ENDPOINT", True, "")
120
+
121
+ # Google Vertex AI
122
+ GOOGLE_GENAI_USE_VERTEXAI = str2bool(get_variable_env("GOOGLE_GENAI_USE_VERTEXAI", True, True))
123
+ GOOGLE_CLOUD_PROJECT = get_variable_env("GOOGLE_CLOUD_PROJECT", True)
124
+ GOOGLE_CLOUD_LOCATION = get_variable_env("GOOGLE_CLOUD_LOCATION", True)
@@ -0,0 +1,17 @@
1
+ """File utilities"""
2
+
3
+
4
+ def is_text_content(data: bytes, mime_type: str) -> bool:
5
+ """Check if content is text based on mime type and content analysis."""
6
+ # Check mime type first
7
+ if mime_type and (
8
+ mime_type.startswith("text/") or mime_type in ["application/json", "application/xml", "application/javascript"]
9
+ ):
10
+ return True
11
+
12
+ # Try to decode as UTF-8 to check if it's text
13
+ try:
14
+ data.decode("utf-8")
15
+ return True
16
+ except UnicodeDecodeError:
17
+ return False
aixtools/utils/utils.py CHANGED
@@ -154,6 +154,13 @@ def timestamp_uuid_tuple() -> tuple[str, str, str]:
154
154
  return (now.strftime("%Y-%m-%d"), now.strftime("%H:%M:%S"), str(uuid.uuid4()))
155
155
 
156
156
 
157
+ def str2bool(v: str | None) -> bool:
158
+ """Convert a string to a boolean value."""
159
+ if not v:
160
+ return False
161
+ return str(v).lower() in ("yes", "true", "on", "1")
162
+
163
+
157
164
  async def async_iter(items):
158
165
  """Asynchronously iterate over items."""
159
166
  for item in items:
@@ -1,9 +1,11 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aixtools
3
- Version: 0.1.3
3
+ Version: 0.1.5
4
4
  Summary: Tools for AI exploration and debugging
5
5
  Requires-Python: >=3.11.2
6
6
  Description-Content-Type: text/markdown
7
+ Requires-Dist: a2a-sdk>=0.3.1
8
+ Requires-Dist: cachebox>=5.0.1
7
9
  Requires-Dist: chainlit>=2.5.5
8
10
  Requires-Dist: colorlog>=6.9.0
9
11
  Requires-Dist: fasta2a>=0.5.0
@@ -1,8 +1,8 @@
1
1
  aixtools/__init__.py,sha256=9NGHm7LjsQmsvjTZvw6QFJexSvAU4bCoN_KBk9SCa00,260
2
- aixtools/_version.py,sha256=q5nF98G8SoVeJqaknL0xdyxtv0egsqb0fK06_84Izu8,704
2
+ aixtools/_version.py,sha256=rdxBMYpwzYxiWk08QbPLHSAxHoDfeKWwyaJIAM0lSic,704
3
3
  aixtools/app.py,sha256=JzQ0nrv_bjDQokllIlGHOV0HEb-V8N6k_nGQH-TEsVU,5227
4
4
  aixtools/chainlit.md,sha256=yC37Ly57vjKyiIvK4oUvf4DYxZCwH7iocTlx7bLeGLU,761
5
- aixtools/context.py,sha256=XuvSGjG8f-QHBJlI_yCPdjYo4rm_WrswmUE8GjLoRqI,491
5
+ aixtools/context.py,sha256=I_MD40ZnvRm5WPKAKqBUAdXIf8YaurkYUUHSVVy-QvU,598
6
6
  aixtools/.chainlit/config.toml,sha256=jxpzXQ9l2IRqBKTFLdvpDG4DunUcOko6kIEb3dQw4HU,3788
7
7
  aixtools/.chainlit/translations/bn.json,sha256=eB36bL3SggncVGejh29UDnGYFd_MMDtQGBiMxQ9w1fM,16432
8
8
  aixtools/.chainlit/translations/en-US.json,sha256=V5pTnHxsR0dkzhUwlkxqKe0qvR-T39N9tJN0c1CLsAw,7118
@@ -17,15 +17,22 @@ aixtools/.chainlit/translations/nl.json,sha256=R3e-WxkQXAiuQgnnXjFWhwzpn1EA9xJ8g
17
17
  aixtools/.chainlit/translations/ta.json,sha256=pxa2uLEEDjiGiT6MFcCJ_kNh5KoFViHFptcJjc79Llc,17224
18
18
  aixtools/.chainlit/translations/te.json,sha256=0qGj-ODEHVOcxfVVX5IszS1QBCKSXuU1okANP_EbvBQ,16885
19
19
  aixtools/.chainlit/translations/zh-CN.json,sha256=EWxhT2_6CW9z0F6SI2llr3RsaL2omH1QZWHVG2n5POA,8664
20
- aixtools/a2a/__init__.py,sha256=UGeU-1JoE-QmgT3A_9r4Gzw7WQU2cZr4JxNGqm5ZZrg,191
21
- aixtools/a2a/app.py,sha256=VQAKJHYJJr5-qJRLXvRgc2pZtH_SPXvB293nJaEDXTw,5071
20
+ aixtools/a2a/app.py,sha256=p18G7fAInl9dcNYq6RStBjv1C3aD6oilQq3WXtBuk30,5069
22
21
  aixtools/a2a/utils.py,sha256=EHr3IyyBJn23ni-JcfAf6i3VpQmPs0g1TSnAZazvY_8,4039
22
+ aixtools/a2a/google_sdk/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
23
+ aixtools/a2a/google_sdk/card.py,sha256=P0L3bKbm28HaRkcIxIvjuSGUKOOc0ymyRAFHKm3a5GQ,996
24
+ aixtools/a2a/google_sdk/remote_agent_connection.py,sha256=oDCRSN3gfONY1Ibp8BrtysIVqfQQ-lWe5N7lr1ymHxY,2819
25
+ aixtools/a2a/google_sdk/utils.py,sha256=hjrNRZywJEUxHHaOttJFQU0FLzteg0Ggtm3qAeXMSVw,2430
26
+ aixtools/a2a/google_sdk/pydantic_ai_adapter/agent_executor.py,sha256=MMbhbEnUL6NwSYnisJrDdHW8zJoSyJ3Pzzkt8jqwNdI,7066
27
+ aixtools/a2a/google_sdk/pydantic_ai_adapter/storage.py,sha256=nGoVL7MPoZJW7iVR71laqpUYP308yFKZIifJtvUgpiU,878
23
28
  aixtools/agents/__init__.py,sha256=MAW196S2_G7uGqv-VNjvlOETRfuV44WlU1leO7SiR0A,282
24
29
  aixtools/agents/agent.py,sha256=E1zu70t53RqIbcLI_R09wUtsiYZR1bTnElCQ5PrsrKw,6127
25
30
  aixtools/agents/agent_batch.py,sha256=0Zu9yNCRPAQZPjXQ-dIUAmP1uGTVbxVt7xvnMpoJMjU,2251
31
+ aixtools/agents/prompt.py,sha256=VCOVSnhNKsPIT347ouzwM1PH4I9UTm2cSnTh3ZpjRwk,3391
26
32
  aixtools/db/__init__.py,sha256=b8vRhme3egV-aUZbAntnOaDkSXB8UT0Xy5oqQhU_z0Q,399
27
33
  aixtools/db/database.py,sha256=caWe95GlxZYlxn2ubDmR-_cQUW0ulkpR3BHunKIaOsw,3369
28
34
  aixtools/db/vector_db.py,sha256=be4JGyXj3o8VEfy9L6SO1aAoDET_zazMJkYfjlYHTYQ,4133
35
+ aixtools/google/client.py,sha256=8yuv_zEZKlmUTI-zRxAb3vjLUrfiwrBhcpNe0hYsO0g,1078
29
36
  aixtools/log_view/__init__.py,sha256=0fWLCq9BMo8GoH3Z5WDgvf0-J2TP0XWqtef0f28SHBA,405
30
37
  aixtools/log_view/app.py,sha256=DZp3PUM_iS3DpMHqHfFXVACvbZ9PItbOCNMkDjIOfTc,6595
31
38
  aixtools/log_view/display.py,sha256=8ygvtfUjieD8JKov_cnAi5KNMsGoOCE4AeZmDwAa8DU,9971
@@ -37,21 +44,22 @@ aixtools/logfilters/__init__.py,sha256=pTD8ujCqjPWBCeB7yv7lmCtnA2KXOnkIv0HExDagk
37
44
  aixtools/logfilters/context_filter.py,sha256=zR3Bnv3fCqXLeb7bCFTmlnWhC6dFIvUb-u712tOnUPk,2259
38
45
  aixtools/logging/__init__.py,sha256=b5oYyGQDUHHxhRtzqKUaQPv8hQeWw54rzDXSV8lDY1w,613
39
46
  aixtools/logging/log_objects.py,sha256=hKMLIsWZEw9Q0JT3GCn9mS8O7DJCzsgMZTL7mlMObpM,6828
40
- aixtools/logging/logging_config.py,sha256=LBUjR5aDUh_G5U2EA2yjzw9IuOZHHUJli1y-TVpj9lA,3267
47
+ aixtools/logging/logging_config.py,sha256=LvxV3C75-I0096PpcCIbgM-Cp998LzWXeMM14HYbU20,4985
41
48
  aixtools/logging/mcp_log_models.py,sha256=7-H2GJXiiyLhpImuyLLftAGG4skxJal8Swax0ob04MY,3463
42
49
  aixtools/logging/mcp_logger.py,sha256=d2I5l4t0d6rQH17w23FpE1IUD8Ax-mSaKfByCH86q4I,6257
43
50
  aixtools/logging/model_patch_logging.py,sha256=MY2EvR7ZSctC4hJxNMe8iACeVayUJ2V5In2GAnKdgOo,2880
44
51
  aixtools/logging/open_telemetry.py,sha256=fJjF1ou_8GyfNfbyWDQPGK6JAUrUaPwURYPHhXEtDBE,1121
45
52
  aixtools/mcp/__init__.py,sha256=tLo2KZ1Ojo-rgEEJBGtZfUw-iOoopWoHDnYQTq3IzfE,163
53
+ aixtools/mcp/client.py,sha256=yUcurbNQ8bRaPc8-gK_ESpcPb26d5ckDOEDQrG_YW0c,11823
46
54
  aixtools/mcp/example_client.py,sha256=QCFGP3NCNJMOKWjUOnFwjnbJhUSb879IA1ZYmwjRnmc,889
47
55
  aixtools/mcp/example_server.py,sha256=1SWCyrLWsAnOa81HC4QbPJo_lBVu0b3SZBWI-qDh1vQ,458
48
56
  aixtools/mcp/fast_mcp_log.py,sha256=XYOS406dVjn5YTHyGRsRvVNQ0SKlRObfrKj6EeLFjHg,1057
49
- aixtools/mcp/faulty_mcp.py,sha256=IzayVeElQZRw-sw_QwOwHHh2PuK2T9NKikEi_tt8SPU,12759
57
+ aixtools/mcp/faulty_mcp.py,sha256=uU9vlNGCS_i2k20wocVMaDHTlYjMQxuzjILad9O1cjA,12807
50
58
  aixtools/model_patch/model_patch.py,sha256=JT-oHubIn2LeoSwWbzEQ5vLH7crJmFUecHyQfaAFHa0,1813
51
59
  aixtools/server/__init__.py,sha256=rwPx020YpOzCnrxA80Lc4yLLcIp-Mpe9hNqVO9wDPv0,448
52
60
  aixtools/server/app_mounter.py,sha256=0tJ0tC140ezAjnYdlhpLJQjY-TO8NVw7D8LseYCCVY8,3336
53
61
  aixtools/server/path.py,sha256=SaIJxvmhJy3kzx5zJ6d4cKP6kKu2wFFciQkOLGTA4gg,3056
54
- aixtools/server/utils.py,sha256=3kIkwpqvc3KsLr5Ja2aGaQ6xy13UaG6K75s2NJVjj-o,2237
62
+ aixtools/server/utils.py,sha256=tZWITIx6M-luV9yve4j3rPtYGSSA6zWS0JWEAySne_M,2276
55
63
  aixtools/testing/__init__.py,sha256=mlmaAR2gmS4SbsYNCxnIprmFpFp-syjgVUkpUszo3mE,166
56
64
  aixtools/testing/aix_test_model.py,sha256=dlI3sdyvmu4fUs_K4-oazs_a7cE6V-gnI6RQ0_fPVxg,5925
57
65
  aixtools/testing/mock_tool.py,sha256=4I0LxxSkLhGIKM2YxCP3cnYI8IYJjdKhfwGZ3dioXsM,2465
@@ -60,11 +68,12 @@ aixtools/tools/doctor/__init__.py,sha256=FPwYzC1eJyw8IH0-BP0wgxSprLy6Y_4yXCek749
60
68
  aixtools/tools/doctor/tool_doctor.py,sha256=flp00mbFwVI0-Ii_xC4YDW6Vrn-EAExA1TtQkY6cOZE,2583
61
69
  aixtools/tools/doctor/tool_recommendation.py,sha256=t-l5bm6kwnXs1NH-ZZVTWhVrEAmWa460M44bi_Bip4g,1463
62
70
  aixtools/utils/__init__.py,sha256=xT6almZBQYMfj4h7Hq9QXDHyVXbOOTxqLsmJsxYYnSw,757
63
- aixtools/utils/config.py,sha256=1_sGqjj9qahu2thdjJUo6RGi6W2b79UB10sefyfZNMs,4146
71
+ aixtools/utils/config.py,sha256=x6zfDDAwD0dQglRJnlENxYXZKo_IMUaSLo9Ed6XKboc,4446
64
72
  aixtools/utils/config_util.py,sha256=3Ya4Qqhj1RJ1qtTTykQ6iayf5uxlpigPXgEJlTi1wn4,2229
65
73
  aixtools/utils/enum_with_description.py,sha256=zjSzWxG74eR4x7dpmb74pLTYCWNSMvauHd7_9LpDYIw,1088
74
+ aixtools/utils/files.py,sha256=8JnxwHJRJcjWCdFpjzWmo0po2fRg8esj4H7sOxElYXU,517
66
75
  aixtools/utils/persisted_dict.py,sha256=0jQzV7oF-A6Or-HjcU6V7aMXWQL67SOKpULgmtFwAfg,3110
67
- aixtools/utils/utils.py,sha256=4AkAPPnymYt4faThWz0QYSrAeQVys-l-YO8Kw302CcA,4703
76
+ aixtools/utils/utils.py,sha256=5911Ej1ES2NU_FKIWA3CWKhKnwgjvi1aDR2aiD6Xv3E,4880
68
77
  aixtools/utils/chainlit/cl_agent_show.py,sha256=vaRuowp4BRvhxEr5hw0zHEJ7iaSF_5bo_9BH7pGPPpw,4398
69
78
  aixtools/utils/chainlit/cl_utils.py,sha256=fxaxdkcZg6uHdM8uztxdPowg3a2f7VR7B26VPY4t-3c,5738
70
79
  docker/mcp-base/Dockerfile,sha256=sSpbt0sasSBHHeGwPIpJpiEQMU5HGeXzerK8biVSt7Q,1547
@@ -78,8 +87,30 @@ scripts/log_view.sh,sha256=bp8oXFRRbbHpyvHAN85wfDHTVK7vMJOYsBx_-bgECQc,511
78
87
  scripts/run_example_mcp_server.sh,sha256=f7m7h7O_wo6-nAsYlOXVWIASCOh3Qbuu0XWizlxMhl8,355
79
88
  scripts/run_faulty_mcp_server.sh,sha256=u_-8NbPDnJQt6IinNSjh8tc2ed-_MjGyipJXrUXaGR8,291
80
89
  scripts/run_server.sh,sha256=5iiB9bB5M2MuOgxVQqu7Oa_tBVtJpt0uB4z9uLu2J50,720
81
- aixtools-0.1.3.dist-info/METADATA,sha256=EjMCZ1tNjNpNF95J0h08jI3Rb19Phe9-fDei7S1Syg0,10109
82
- aixtools-0.1.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
83
- aixtools-0.1.3.dist-info/entry_points.txt,sha256=dHoutULEZx7xXSqJrZdViSVjfInJibfLibi2nRXL3SE,56
84
- aixtools-0.1.3.dist-info/top_level.txt,sha256=IPyw70hj9gVDyugaIr3LRlanLYXzostW4azlTgdlALo,34
85
- aixtools-0.1.3.dist-info/RECORD,,
90
+ scripts/test.sh,sha256=5akwfnX7P-98898WCXcQRFQt84UBgms5Y9fXC_ym1a4,662
91
+ tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
92
+ tests/unit/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
93
+ tests/unit/a2a/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
94
+ tests/unit/a2a/google_sdk/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
95
+ tests/unit/a2a/google_sdk/test_card.py,sha256=g8OUIX9BCtApM9y8l_nL1Q7sm3yezxu5yBxrpy76mo4,4359
96
+ tests/unit/a2a/google_sdk/test_remote_agent_connection.py,sha256=nIY8eg32w96BAddaQ25mT-lr0ozPb6UrG-_Vpqx5RMY,17492
97
+ tests/unit/a2a/google_sdk/test_utils.py,sha256=-eHmIk2GJH57W2bAdTzfRrUUb5jnd9Pf-QSXJogN3g8,8312
98
+ tests/unit/a2a/google_sdk/pydantic_ai_adapter/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
99
+ tests/unit/a2a/google_sdk/pydantic_ai_adapter/test_agent_executor.py,sha256=PcyCw0N3y-txu2KJzufzbCjs7ZfoBBCVjpZuRBqTmOw,7722
100
+ tests/unit/a2a/google_sdk/pydantic_ai_adapter/test_storage.py,sha256=tb67pFfvyWSaDfKaiPDNBQfl6-o17WtCMZh3lQHrYxY,5468
101
+ tests/unit/agents/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
102
+ tests/unit/agents/test_prompt.py,sha256=YWFZdH_F774hxw79gsWoTWBPVs8UjOAtJOgNXJ8N9gs,15384
103
+ tests/unit/google/__init__.py,sha256=eRYHldBi5cFWL7oo2_t5TErI8ESmIjNvBZIcp-w8hSA,45
104
+ tests/unit/google/test_client.py,sha256=fXR4Cozea7bdL2prM-1s9IqUQ9AheklQnHpN-4YM3gg,11005
105
+ tests/unit/mcp/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
106
+ tests/unit/mcp/test_client.py,sha256=n9sZvmzNzJfozvxoHweAg4M5ZLNhEizq16IjcZHGdj0,8838
107
+ tests/unit/server/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
108
+ tests/unit/server/test_path.py,sha256=1QKiKLLRga9GNxmaUEt_wEZ9U14yzB-7PIhAOgB4wwo,9523
109
+ tests/unit/server/test_utils.py,sha256=kvhzdgNfsJl5tqcRBWg2yTR5GPpyrFCOmEIOuHb3904,14848
110
+ tests/unit/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
111
+ tests/unit/utils/test_files.py,sha256=AKFmXQqXstyKd2PreE4EmQyhQYeqOmu1Sp80MwHrf_Q,5782
112
+ aixtools-0.1.5.dist-info/METADATA,sha256=QbJnhcWkwh9yMwKh5e95XmVuLKVAltsOx2bce11oKbs,10170
113
+ aixtools-0.1.5.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
114
+ aixtools-0.1.5.dist-info/entry_points.txt,sha256=dHoutULEZx7xXSqJrZdViSVjfInJibfLibi2nRXL3SE,56
115
+ aixtools-0.1.5.dist-info/top_level.txt,sha256=ee4eF-0pqu45zCUVml0mWIhnXQgqMQper2-49BBVHLY,40
116
+ aixtools-0.1.5.dist-info/RECORD,,
@@ -2,3 +2,4 @@ aixtools
2
2
  docker
3
3
  notebooks
4
4
  scripts
5
+ tests
scripts/test.sh ADDED
@@ -0,0 +1,23 @@
1
+ #!/bin/bash -eu
2
+ set -o pipefail
3
+
4
+ # Get the directory of this script
5
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
6
+ source "${SCRIPT_DIR}/config.sh"
7
+
8
+ # Install required dependencies
9
+ echo "Installing test dependencies..."
10
+ uv pip install pytest pytest-asyncio pytest-mock pytest-cov
11
+
12
+ # Ensure .env exists
13
+ touch .env
14
+
15
+ # Add the project root to PYTHONPATH so imports work correctly
16
+ export PYTHONPATH="${PROJECT_DIR}:${PYTHONPATH:-}"
17
+
18
+ # Run all tests using pytest with coverage
19
+ echo "Running tests with coverage..."
20
+ pytest tests/ -v --cov=aixtools --cov-report=term-missing
21
+
22
+ # Generate HTML coverage report
23
+ # pytest tests/ --cov=aixtools --cov-report=html
tests/__init__.py ADDED
File without changes
tests/unit/__init__.py ADDED
File without changes
File without changes
File without changes