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.
- aixtools/__init__.py +5 -0
- aixtools/a2a/__init__.py +5 -0
- aixtools/a2a/app.py +126 -0
- aixtools/a2a/utils.py +115 -0
- aixtools/agents/__init__.py +12 -0
- aixtools/agents/agent.py +164 -0
- aixtools/agents/agent_batch.py +74 -0
- aixtools/app.py +143 -0
- aixtools/context.py +12 -0
- aixtools/db/__init__.py +17 -0
- aixtools/db/database.py +110 -0
- aixtools/db/vector_db.py +115 -0
- aixtools/log_view/__init__.py +17 -0
- aixtools/log_view/app.py +195 -0
- aixtools/log_view/display.py +285 -0
- aixtools/log_view/export.py +51 -0
- aixtools/log_view/filters.py +41 -0
- aixtools/log_view/log_utils.py +26 -0
- aixtools/log_view/node_summary.py +229 -0
- aixtools/logfilters/__init__.py +7 -0
- aixtools/logfilters/context_filter.py +67 -0
- aixtools/logging/__init__.py +30 -0
- aixtools/logging/log_objects.py +227 -0
- aixtools/logging/logging_config.py +116 -0
- aixtools/logging/mcp_log_models.py +102 -0
- aixtools/logging/mcp_logger.py +172 -0
- aixtools/logging/model_patch_logging.py +87 -0
- aixtools/logging/open_telemetry.py +36 -0
- aixtools/mcp/__init__.py +9 -0
- aixtools/mcp/example_client.py +30 -0
- aixtools/mcp/example_server.py +22 -0
- aixtools/mcp/fast_mcp_log.py +31 -0
- aixtools/mcp/faulty_mcp.py +320 -0
- aixtools/model_patch/model_patch.py +65 -0
- aixtools/server/__init__.py +23 -0
- aixtools/server/app_mounter.py +90 -0
- aixtools/server/path.py +72 -0
- aixtools/server/utils.py +70 -0
- aixtools/testing/__init__.py +9 -0
- aixtools/testing/aix_test_model.py +147 -0
- aixtools/testing/mock_tool.py +66 -0
- aixtools/testing/model_patch_cache.py +279 -0
- aixtools/tools/doctor/__init__.py +3 -0
- aixtools/tools/doctor/tool_doctor.py +61 -0
- aixtools/tools/doctor/tool_recommendation.py +44 -0
- aixtools/utils/__init__.py +35 -0
- aixtools/utils/chainlit/cl_agent_show.py +82 -0
- aixtools/utils/chainlit/cl_utils.py +168 -0
- aixtools/utils/config.py +118 -0
- aixtools/utils/config_util.py +69 -0
- aixtools/utils/enum_with_description.py +37 -0
- aixtools/utils/persisted_dict.py +99 -0
- aixtools/utils/utils.py +160 -0
- aixtools-0.1.0.dist-info/METADATA +355 -0
- aixtools-0.1.0.dist-info/RECORD +58 -0
- aixtools-0.1.0.dist-info/WHEEL +5 -0
- aixtools-0.1.0.dist-info/entry_points.txt +2 -0
- 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.")
|
aixtools/mcp/__init__.py
ADDED
|
@@ -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
|