aixtools 0.1.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.

Potentially problematic release.


This version of aixtools might be problematic. Click here for more details.

Files changed (58) hide show
  1. aixtools/__init__.py +5 -0
  2. aixtools/a2a/__init__.py +5 -0
  3. aixtools/a2a/app.py +126 -0
  4. aixtools/a2a/utils.py +115 -0
  5. aixtools/agents/__init__.py +12 -0
  6. aixtools/agents/agent.py +164 -0
  7. aixtools/agents/agent_batch.py +74 -0
  8. aixtools/app.py +143 -0
  9. aixtools/context.py +12 -0
  10. aixtools/db/__init__.py +17 -0
  11. aixtools/db/database.py +110 -0
  12. aixtools/db/vector_db.py +115 -0
  13. aixtools/log_view/__init__.py +17 -0
  14. aixtools/log_view/app.py +195 -0
  15. aixtools/log_view/display.py +285 -0
  16. aixtools/log_view/export.py +51 -0
  17. aixtools/log_view/filters.py +41 -0
  18. aixtools/log_view/log_utils.py +26 -0
  19. aixtools/log_view/node_summary.py +229 -0
  20. aixtools/logfilters/__init__.py +7 -0
  21. aixtools/logfilters/context_filter.py +67 -0
  22. aixtools/logging/__init__.py +30 -0
  23. aixtools/logging/log_objects.py +227 -0
  24. aixtools/logging/logging_config.py +116 -0
  25. aixtools/logging/mcp_log_models.py +102 -0
  26. aixtools/logging/mcp_logger.py +172 -0
  27. aixtools/logging/model_patch_logging.py +87 -0
  28. aixtools/logging/open_telemetry.py +36 -0
  29. aixtools/mcp/__init__.py +9 -0
  30. aixtools/mcp/example_client.py +30 -0
  31. aixtools/mcp/example_server.py +22 -0
  32. aixtools/mcp/fast_mcp_log.py +31 -0
  33. aixtools/mcp/faulty_mcp.py +320 -0
  34. aixtools/model_patch/model_patch.py +65 -0
  35. aixtools/server/__init__.py +23 -0
  36. aixtools/server/app_mounter.py +90 -0
  37. aixtools/server/path.py +72 -0
  38. aixtools/server/utils.py +70 -0
  39. aixtools/testing/__init__.py +9 -0
  40. aixtools/testing/aix_test_model.py +147 -0
  41. aixtools/testing/mock_tool.py +66 -0
  42. aixtools/testing/model_patch_cache.py +279 -0
  43. aixtools/tools/doctor/__init__.py +3 -0
  44. aixtools/tools/doctor/tool_doctor.py +61 -0
  45. aixtools/tools/doctor/tool_recommendation.py +44 -0
  46. aixtools/utils/__init__.py +35 -0
  47. aixtools/utils/chainlit/cl_agent_show.py +82 -0
  48. aixtools/utils/chainlit/cl_utils.py +168 -0
  49. aixtools/utils/config.py +118 -0
  50. aixtools/utils/config_util.py +69 -0
  51. aixtools/utils/enum_with_description.py +37 -0
  52. aixtools/utils/persisted_dict.py +99 -0
  53. aixtools/utils/utils.py +160 -0
  54. aixtools-0.1.0.dist-info/METADATA +355 -0
  55. aixtools-0.1.0.dist-info/RECORD +58 -0
  56. aixtools-0.1.0.dist-info/WHEEL +5 -0
  57. aixtools-0.1.0.dist-info/entry_points.txt +2 -0
  58. aixtools-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,172 @@
1
+ """
2
+ Logger implementations for MCP server.
3
+ """
4
+
5
+ import json
6
+ from abc import ABC, abstractmethod
7
+ from datetime import datetime
8
+ from pathlib import Path
9
+
10
+ from aixtools.logging.logging_config import get_logger
11
+
12
+ from .mcp_log_models import CodeLogEntry, CommandLogEntry, LogEntry, ServiceLogEntry, SystemLogEntry
13
+
14
+ logger = get_logger(__name__)
15
+
16
+
17
+ def log_with_default_logger(entry: LogEntry) -> None:
18
+ """
19
+ Formats a log entry into a human-readable string and logs it.
20
+ """
21
+ if isinstance(entry, SystemLogEntry):
22
+ logger.info("%s: %s", entry.event, entry.details or "")
23
+ elif isinstance(entry, ServiceLogEntry):
24
+ logger.info("%s: %s", entry.event, entry.details or "")
25
+ elif isinstance(entry, CodeLogEntry):
26
+ logger.info("%s code: %s", entry.language, entry.code)
27
+ elif isinstance(entry, CommandLogEntry):
28
+ logger.info("%s, CWD: %s", entry.command, entry.working_directory)
29
+ else:
30
+ logger.debug("Logging entry: %s", entry.model_dump_json(indent=2))
31
+
32
+
33
+ class McpLogger(ABC):
34
+ """Abstract base class for loggers."""
35
+
36
+ @abstractmethod
37
+ def log(self, entry: LogEntry) -> None:
38
+ """Log an entry."""
39
+
40
+ @abstractmethod
41
+ def get_logs( # noqa: PLR0913 # pylint: disable=too-many-arguments,too-many-positional-arguments
42
+ self,
43
+ user_id: str | None = None,
44
+ session_id: str | None = None,
45
+ container_id: str | None = None,
46
+ start_time: datetime | None = None,
47
+ end_time: datetime | None = None,
48
+ limit: int = 100,
49
+ ) -> list[LogEntry]:
50
+ """Get logs with optional filters."""
51
+
52
+
53
+ class JSONFileMcpLogger(McpLogger):
54
+ """Logger that stores logs in a single JSON file."""
55
+
56
+ def __init__(self, log_dir: str | Path):
57
+ """Initialize the logger."""
58
+ self.log_dir = Path(log_dir)
59
+ self.log_dir.mkdir(parents=True, exist_ok=True)
60
+ self.log_file_path = self.log_dir / "mcp_logs.jsonl"
61
+ self.log_file = open(self.log_file_path, "a", encoding="utf-8") # pylint: disable=consider-using-with
62
+
63
+ def __del__(self):
64
+ """Ensure file is closed when the logger is destroyed."""
65
+ if hasattr(self, "log_file") and self.log_file and not self.log_file.closed:
66
+ self.log_file.close()
67
+
68
+ def _get_log_file_path(self) -> Path:
69
+ """Get the path to the log file."""
70
+ return self.log_file_path
71
+
72
+ def log(self, entry: LogEntry) -> None:
73
+ """Log an entry to the JSON file."""
74
+ log_with_default_logger(entry)
75
+ # Convert the entry to a JSON string
76
+ entry_json = entry.model_dump_json()
77
+ # Append the entry to the log file and flush to ensure it's written
78
+ self.log_file.write(entry_json + "\n")
79
+ self.log_file.flush()
80
+
81
+ def get_logs( # noqa: PLR0913, PLR0912 # pylint: disable=too-many-arguments,too-many-positional-arguments,too-many-branches
82
+ self,
83
+ user_id: str | None = None,
84
+ session_id: str | None = None,
85
+ container_id: str | None = None,
86
+ start_time: datetime | None = None,
87
+ end_time: datetime | None = None,
88
+ limit: int = 100,
89
+ ) -> list[LogEntry]:
90
+ """Get logs with optional filters.
91
+
92
+ Args:
93
+ user_id: Filter by user ID.
94
+ session_id: Filter by session ID.
95
+ container_id: Filter by container ID.
96
+ start_time: Filter by start time.
97
+ end_time: Filter by end time.
98
+ limit: Maximum number of logs to return.
99
+
100
+ Returns:
101
+ List of log entries.
102
+ """
103
+ logs: list[LogEntry] = []
104
+
105
+ # Ensure any pending writes are flushed to disk
106
+ self.log_file.flush()
107
+
108
+ # Read from the single log file
109
+ if self.log_file_path.exists():
110
+ with open(self.log_file_path, "r", encoding="utf-8") as f:
111
+ for line in f:
112
+ try:
113
+ # Parse the JSON entry
114
+ entry_dict = json.loads(line)
115
+
116
+ # Convert timestamp string to datetime
117
+ entry_dict["timestamp"] = datetime.fromisoformat(entry_dict["timestamp"])
118
+
119
+ # Apply filters
120
+ if user_id and entry_dict.get("user_id") != user_id:
121
+ continue
122
+ if session_id and entry_dict.get("session_id") != session_id:
123
+ continue
124
+ if container_id and entry_dict.get("container_id") != container_id:
125
+ continue
126
+ if start_time and entry_dict["timestamp"] < start_time:
127
+ continue
128
+ if end_time and entry_dict["timestamp"] > end_time:
129
+ continue
130
+
131
+ # Create the appropriate log entry object based on log_type
132
+ log_type = entry_dict["log_type"]
133
+ if log_type == "command":
134
+ entry = CommandLogEntry(**entry_dict)
135
+ elif log_type == "code":
136
+ entry = CodeLogEntry(**entry_dict)
137
+ elif log_type == "system":
138
+ entry = SystemLogEntry(**entry_dict)
139
+ else:
140
+ continue
141
+
142
+ logs.append(entry)
143
+ except (json.JSONDecodeError, KeyError) as e:
144
+ logger.error("Error parsing log entry: %s", e)
145
+ continue
146
+
147
+ # Check if we've reached the limit
148
+ if len(logs) >= limit:
149
+ break
150
+
151
+ # Sort logs by timestamp (newest first)
152
+ logs.sort(key=lambda x: x.timestamp, reverse=True)
153
+
154
+ return logs[:limit]
155
+
156
+
157
+ # Global logger instance
158
+ _mcp_logger: McpLogger | None = None
159
+
160
+
161
+ def initialize_mcp_logger(mcp_logger: McpLogger) -> None:
162
+ """Initialize the MCP logger"""
163
+ global _mcp_logger # noqa: PLW0603, pylint: disable=global-statement
164
+ _mcp_logger = mcp_logger
165
+
166
+
167
+ def get_mcp_logger() -> McpLogger:
168
+ """Get the global logger for MCP server."""
169
+ if _mcp_logger is None:
170
+ raise RuntimeError("Logger not initialized")
171
+
172
+ return _mcp_logger
@@ -0,0 +1,87 @@
1
+ """
2
+ Logging utilities for model patching and request/response tracking.
3
+ """
4
+
5
+ import functools
6
+ from contextlib import asynccontextmanager
7
+ from uuid import uuid4
8
+
9
+ from aixtools.logging.logging_config import get_logger
10
+ from aixtools.model_patch.model_patch import (
11
+ ModelRawRequest,
12
+ ModelRawRequestResult,
13
+ ModelRawRequestYieldItem,
14
+ get_request_fn,
15
+ get_request_stream_fn,
16
+ model_patch,
17
+ )
18
+
19
+ logger = get_logger(__name__)
20
+
21
+
22
+ def log_async_method(fn, agent_logger):
23
+ """Log async method calls"""
24
+
25
+ @functools.wraps(fn)
26
+ async def model_request_logger_wrapper(*args, **kwargs):
27
+ # Log request
28
+ uuid = str(uuid4()) # Create a unique ID for this request
29
+ log_object = ModelRawRequest(method_name=fn.__name__, request_id=uuid, args=args, kwargs=kwargs)
30
+ agent_logger.log(log_object)
31
+ # Invoke the original method
32
+ try:
33
+ result = await fn(*args, **kwargs)
34
+ # Log results
35
+ log_object = ModelRawRequestResult(method_name=fn.__name__, request_id=uuid, result=result)
36
+ agent_logger.log(log_object)
37
+ except Exception as e:
38
+ # Log exception
39
+ agent_logger.log(e)
40
+ raise e
41
+ return result
42
+
43
+ return model_request_logger_wrapper
44
+
45
+
46
+ def log_async_stream(fn, agent_logger):
47
+ """Log async streaming method calls with individual item tracking."""
48
+
49
+ @functools.wraps(fn)
50
+ @asynccontextmanager
51
+ async def model_request_stream_logger_wrapper(*args, **kwargs):
52
+ # Log request
53
+ uuid = str(uuid4()) # Create a unique ID for this request
54
+ log_object = ModelRawRequest(method_name=fn.__name__, request_id=uuid, args=args, kwargs=kwargs)
55
+ agent_logger.log(log_object)
56
+ # Invoke the original method
57
+ async with fn(*args, **kwargs) as stream:
58
+
59
+ async def gen():
60
+ item_num = 0
61
+ try:
62
+ async for item in stream:
63
+ # Log yielded items
64
+ log_object = ModelRawRequestYieldItem(
65
+ method_name=fn.__name__, request_id=uuid, item=item, item_num=item_num
66
+ )
67
+ agent_logger.log(log_object)
68
+ item_num += 1
69
+ yield item
70
+ except Exception as e:
71
+ # Log exception
72
+ agent_logger.log(e)
73
+ raise e
74
+
75
+ yield gen()
76
+
77
+ return model_request_stream_logger_wrapper
78
+
79
+
80
+ def model_patch_logging(model, agent_logger):
81
+ """Patch model with logging methods"""
82
+ logger.debug("Patching model with logging")
83
+ return model_patch(
84
+ model,
85
+ request_method=log_async_method(get_request_fn(model), agent_logger),
86
+ request_stream_method=log_async_stream(get_request_stream_fn(model), agent_logger),
87
+ )
@@ -0,0 +1,36 @@
1
+ """
2
+ OpenTelemetry integration for logging and tracing agent operations.
3
+ """
4
+
5
+ import os
6
+
7
+ import logfire # pylint: disable=import-error
8
+ from pydantic_ai import Agent
9
+
10
+ from aixtools.utils.config import LOGFIRE_TOKEN, LOGFIRE_TRACES_ENDPOINT
11
+
12
+
13
+ def open_telemetry_on():
14
+ """Configure and enable OpenTelemetry tracing with LogFire integration."""
15
+ service_name = "agent_poc"
16
+
17
+ if LOGFIRE_TRACES_ENDPOINT:
18
+ os.environ["OTEL_EXPORTER_OTLP_TRACES_ENDPOINT"] = LOGFIRE_TRACES_ENDPOINT
19
+ logfire.configure(
20
+ service_name=service_name,
21
+ # Sending to Logfire is on by default regardless of the OTEL env vars.
22
+ # Keep this line here if you don't want to send to both Jaeger and Logfire.
23
+ send_to_logfire=False,
24
+ )
25
+ Agent.instrument_all(True)
26
+ return
27
+
28
+ if LOGFIRE_TOKEN:
29
+ logfire.configure(
30
+ token=LOGFIRE_TOKEN,
31
+ service_name=service_name,
32
+ )
33
+ Agent.instrument_all(True)
34
+ return
35
+
36
+ print("OpenTelemetry is not enabled. Set the LOGFIRE_TOKEN or LOGFIRE_TRACES_ENDPOINT environment variable.")
@@ -0,0 +1,9 @@
1
+ """
2
+ Model Context Protocol (MCP) implementation for AI agent communication.
3
+ """
4
+
5
+ from aixtools.mcp.fast_mcp_log import FastMcpLog
6
+
7
+ __all__ = [
8
+ "FastMcpLog",
9
+ ]
@@ -0,0 +1,30 @@
1
+ """
2
+ Example client implementation for Model Context Protocol (MCP) servers.
3
+ """
4
+
5
+ import asyncio
6
+
7
+ from pydantic_ai.mcp import MCPServerSSE, MCPServerStdio
8
+
9
+ from aixtools.agents import get_agent, run_agent
10
+
11
+ USE_SEE = False
12
+
13
+
14
+ if USE_SEE:
15
+ server = MCPServerSSE(url="http://127.0.0.1:8000/sse")
16
+ else:
17
+ server = MCPServerStdio(command="fastmcp", args=["run", "aixtools/mcp/example_server.py"])
18
+
19
+
20
+ async def main(agent, prompt): # pylint: disable=redefined-outer-name
21
+ """Run an agent with MCP servers and display the result."""
22
+ async with agent:
23
+ ret = await run_agent(agent, prompt)
24
+ print(f"Agent returned: {ret}")
25
+
26
+
27
+ if __name__ == "__main__":
28
+ agent = get_agent(mcp_servers=[server]) # pylint: disable=unexpected-keyword-arg
29
+ print(f"Agent created: {agent}")
30
+ asyncio.run(main(agent, "What is the add of 923048502345 and 795467090123481926349123941 ?"))
@@ -0,0 +1,22 @@
1
+ """
2
+ Example server implementation for Model Context Protocol (MCP).
3
+ """
4
+
5
+ from aixtools.mcp.fast_mcp_log import FastMcpLog
6
+
7
+ mcp = FastMcpLog("Demo")
8
+ # mcp = FastMCP("Demo")
9
+
10
+
11
+ # Add an addition tool
12
+ @mcp.tool()
13
+ def add(a: int, b: int) -> int:
14
+ """Add two numbers"""
15
+ return a + b
16
+
17
+
18
+ # Add a dynamic greeting resource
19
+ @mcp.resource("greeting://{name}")
20
+ def get_greeting(name: str) -> str:
21
+ """Get a personalized greeting"""
22
+ return f"Hello, {name}!"
@@ -0,0 +1,31 @@
1
+ """
2
+ FastMCP logging implementation for Model Context Protocol.
3
+ """
4
+
5
+ import sys
6
+ from typing import Any
7
+
8
+ from fastmcp import FastMCP
9
+ from pydantic import AnyUrl
10
+
11
+
12
+ class FastMcpLog(FastMCP):
13
+ """A FastMCP with hooks for logging."""
14
+
15
+ async def _call_tool(self, key: str, arguments: dict[str, Any]):
16
+ print(f"Calling tool: {key} with arguments: {arguments}", file=sys.stderr)
17
+ ret = await super()._call_tool(key, arguments)
18
+ print(f"Tool returned: {ret}", file=sys.stderr)
19
+ return ret
20
+
21
+ async def _read_resource(self, uri: AnyUrl | str):
22
+ print(f"Reading resource: {uri}", file=sys.stderr)
23
+ ret = await super()._read_resource(uri)
24
+ print(f"Resource contents: {ret}", file=sys.stderr)
25
+ return ret
26
+
27
+ async def get_prompt(self, key: str, arguments: dict[str, Any] | None = None):
28
+ print(f"Getting prompt: {key} with arguments: {arguments}", file=sys.stderr)
29
+ ret = await super()._get_prompt(key, arguments)
30
+ print(f"Prompt result: {ret}", file=sys.stderr)
31
+ return ret