aixtools 0.2.14__tar.gz → 0.2.16__tar.gz
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-0.2.14 → aixtools-0.2.16}/PKG-INFO +35 -15
- {aixtools-0.2.14 → aixtools-0.2.16}/README.md +34 -14
- {aixtools-0.2.14 → aixtools-0.2.16}/aixtools/_version.py +3 -3
- {aixtools-0.2.14 → aixtools-0.2.16}/aixtools/agents/agent.py +4 -2
- aixtools-0.2.16/aixtools/agents/nodes_to_md.py +201 -0
- aixtools-0.2.16/aixtools/agents/nodes_to_message.py +30 -0
- aixtools-0.2.16/aixtools/agents/nodes_to_str.py +109 -0
- {aixtools-0.2.14 → aixtools-0.2.16}/aixtools/logging/log_objects.py +27 -15
- {aixtools-0.2.14 → aixtools-0.2.16}/aixtools/logging/model_patch_logging.py +6 -6
- {aixtools-0.2.14 → aixtools-0.2.16}/aixtools/mcp/client.py +2 -2
- aixtools-0.2.16/aixtools/testing/agent_mock.py +143 -0
- {aixtools-0.2.14 → aixtools-0.2.16}/aixtools/utils/utils.py +35 -3
- {aixtools-0.2.14 → aixtools-0.2.16}/aixtools.egg-info/SOURCES.txt +4 -0
- {aixtools-0.2.14 → aixtools-0.2.16}/aixtools/.chainlit/config.toml +0 -0
- {aixtools-0.2.14 → aixtools-0.2.16}/aixtools/.chainlit/translations/bn.json +0 -0
- {aixtools-0.2.14 → aixtools-0.2.16}/aixtools/.chainlit/translations/en-US.json +0 -0
- {aixtools-0.2.14 → aixtools-0.2.16}/aixtools/.chainlit/translations/gu.json +0 -0
- {aixtools-0.2.14 → aixtools-0.2.16}/aixtools/.chainlit/translations/he-IL.json +0 -0
- {aixtools-0.2.14 → aixtools-0.2.16}/aixtools/.chainlit/translations/hi.json +0 -0
- {aixtools-0.2.14 → aixtools-0.2.16}/aixtools/.chainlit/translations/ja.json +0 -0
- {aixtools-0.2.14 → aixtools-0.2.16}/aixtools/.chainlit/translations/kn.json +0 -0
- {aixtools-0.2.14 → aixtools-0.2.16}/aixtools/.chainlit/translations/ml.json +0 -0
- {aixtools-0.2.14 → aixtools-0.2.16}/aixtools/.chainlit/translations/mr.json +0 -0
- {aixtools-0.2.14 → aixtools-0.2.16}/aixtools/.chainlit/translations/nl.json +0 -0
- {aixtools-0.2.14 → aixtools-0.2.16}/aixtools/.chainlit/translations/ta.json +0 -0
- {aixtools-0.2.14 → aixtools-0.2.16}/aixtools/.chainlit/translations/te.json +0 -0
- {aixtools-0.2.14 → aixtools-0.2.16}/aixtools/.chainlit/translations/zh-CN.json +0 -0
- {aixtools-0.2.14 → aixtools-0.2.16}/aixtools/__init__.py +0 -0
- {aixtools-0.2.14 → aixtools-0.2.16}/aixtools/a2a/app.py +0 -0
- {aixtools-0.2.14 → aixtools-0.2.16}/aixtools/a2a/google_sdk/__init__.py +0 -0
- {aixtools-0.2.14 → aixtools-0.2.16}/aixtools/a2a/google_sdk/pydantic_ai_adapter/agent_executor.py +0 -0
- {aixtools-0.2.14 → aixtools-0.2.16}/aixtools/a2a/google_sdk/pydantic_ai_adapter/storage.py +0 -0
- {aixtools-0.2.14 → aixtools-0.2.16}/aixtools/a2a/google_sdk/remote_agent_connection.py +0 -0
- {aixtools-0.2.14 → aixtools-0.2.16}/aixtools/a2a/google_sdk/utils.py +0 -0
- {aixtools-0.2.14 → aixtools-0.2.16}/aixtools/a2a/utils.py +0 -0
- {aixtools-0.2.14 → aixtools-0.2.16}/aixtools/agents/__init__.py +0 -0
- {aixtools-0.2.14 → aixtools-0.2.16}/aixtools/agents/agent_batch.py +0 -0
- {aixtools-0.2.14 → aixtools-0.2.16}/aixtools/agents/print_nodes.py +0 -0
- {aixtools-0.2.14 → aixtools-0.2.16}/aixtools/agents/prompt.py +0 -0
- {aixtools-0.2.14 → aixtools-0.2.16}/aixtools/app.py +0 -0
- {aixtools-0.2.14 → aixtools-0.2.16}/aixtools/auth/__init__.py +0 -0
- {aixtools-0.2.14 → aixtools-0.2.16}/aixtools/auth/auth.py +0 -0
- {aixtools-0.2.14 → aixtools-0.2.16}/aixtools/chainlit.md +0 -0
- {aixtools-0.2.14 → aixtools-0.2.16}/aixtools/compliance/__init__.py +0 -0
- {aixtools-0.2.14 → aixtools-0.2.16}/aixtools/compliance/private_data.py +0 -0
- {aixtools-0.2.14 → aixtools-0.2.16}/aixtools/context.py +0 -0
- {aixtools-0.2.14 → aixtools-0.2.16}/aixtools/db/__init__.py +0 -0
- {aixtools-0.2.14 → aixtools-0.2.16}/aixtools/db/database.py +0 -0
- {aixtools-0.2.14 → aixtools-0.2.16}/aixtools/db/vector_db.py +0 -0
- {aixtools-0.2.14 → aixtools-0.2.16}/aixtools/evals/__init__.py +0 -0
- {aixtools-0.2.14 → aixtools-0.2.16}/aixtools/evals/__main__.py +0 -0
- {aixtools-0.2.14 → aixtools-0.2.16}/aixtools/evals/dataset.py +0 -0
- {aixtools-0.2.14 → aixtools-0.2.16}/aixtools/evals/discovery.py +0 -0
- {aixtools-0.2.14 → aixtools-0.2.16}/aixtools/evals/run_evals.py +0 -0
- {aixtools-0.2.14 → aixtools-0.2.16}/aixtools/google/client.py +0 -0
- {aixtools-0.2.14 → aixtools-0.2.16}/aixtools/log_view/__init__.py +0 -0
- {aixtools-0.2.14 → aixtools-0.2.16}/aixtools/log_view/app.py +0 -0
- {aixtools-0.2.14 → aixtools-0.2.16}/aixtools/log_view/display.py +0 -0
- {aixtools-0.2.14 → aixtools-0.2.16}/aixtools/log_view/export.py +0 -0
- {aixtools-0.2.14 → aixtools-0.2.16}/aixtools/log_view/filters.py +0 -0
- {aixtools-0.2.14 → aixtools-0.2.16}/aixtools/log_view/log_utils.py +0 -0
- {aixtools-0.2.14 → aixtools-0.2.16}/aixtools/log_view/node_summary.py +0 -0
- {aixtools-0.2.14 → aixtools-0.2.16}/aixtools/logfilters/__init__.py +0 -0
- {aixtools-0.2.14 → aixtools-0.2.16}/aixtools/logfilters/context_filter.py +0 -0
- {aixtools-0.2.14 → aixtools-0.2.16}/aixtools/logging/__init__.py +0 -0
- {aixtools-0.2.14 → aixtools-0.2.16}/aixtools/logging/logging_config.py +0 -0
- {aixtools-0.2.14 → aixtools-0.2.16}/aixtools/logging/mcp_log_models.py +0 -0
- {aixtools-0.2.14 → aixtools-0.2.16}/aixtools/logging/mcp_logger.py +0 -0
- {aixtools-0.2.14 → aixtools-0.2.16}/aixtools/logging/open_telemetry.py +0 -0
- {aixtools-0.2.14 → aixtools-0.2.16}/aixtools/mcp/__init__.py +0 -0
- {aixtools-0.2.14 → aixtools-0.2.16}/aixtools/mcp/example_client.py +0 -0
- {aixtools-0.2.14 → aixtools-0.2.16}/aixtools/mcp/example_server.py +0 -0
- {aixtools-0.2.14 → aixtools-0.2.16}/aixtools/mcp/exceptions.py +0 -0
- {aixtools-0.2.14 → aixtools-0.2.16}/aixtools/mcp/fast_mcp_log.py +0 -0
- {aixtools-0.2.14 → aixtools-0.2.16}/aixtools/mcp/faulty_mcp.py +0 -0
- {aixtools-0.2.14 → aixtools-0.2.16}/aixtools/model_patch/model_patch.py +0 -0
- {aixtools-0.2.14 → aixtools-0.2.16}/aixtools/server/__init__.py +0 -0
- {aixtools-0.2.14 → aixtools-0.2.16}/aixtools/server/app_mounter.py +0 -0
- {aixtools-0.2.14 → aixtools-0.2.16}/aixtools/server/path.py +0 -0
- {aixtools-0.2.14 → aixtools-0.2.16}/aixtools/server/utils.py +0 -0
- {aixtools-0.2.14 → aixtools-0.2.16}/aixtools/testing/__init__.py +0 -0
- {aixtools-0.2.14 → aixtools-0.2.16}/aixtools/testing/aix_test_model.py +0 -0
- {aixtools-0.2.14 → aixtools-0.2.16}/aixtools/testing/mock_tool.py +0 -0
- {aixtools-0.2.14 → aixtools-0.2.16}/aixtools/testing/model_patch_cache.py +0 -0
- {aixtools-0.2.14 → aixtools-0.2.16}/aixtools/tools/doctor/__init__.py +0 -0
- {aixtools-0.2.14 → aixtools-0.2.16}/aixtools/tools/doctor/mcp_tool_doctor.py +0 -0
- {aixtools-0.2.14 → aixtools-0.2.16}/aixtools/tools/doctor/tool_doctor.py +0 -0
- {aixtools-0.2.14 → aixtools-0.2.16}/aixtools/tools/doctor/tool_recommendation.py +0 -0
- {aixtools-0.2.14 → aixtools-0.2.16}/aixtools/utils/__init__.py +0 -0
- {aixtools-0.2.14 → aixtools-0.2.16}/aixtools/utils/chainlit/cl_agent_show.py +0 -0
- {aixtools-0.2.14 → aixtools-0.2.16}/aixtools/utils/chainlit/cl_utils.py +0 -0
- {aixtools-0.2.14 → aixtools-0.2.16}/aixtools/utils/config.py +0 -0
- {aixtools-0.2.14 → aixtools-0.2.16}/aixtools/utils/config_util.py +0 -0
- {aixtools-0.2.14 → aixtools-0.2.16}/aixtools/utils/enum_with_description.py +0 -0
- {aixtools-0.2.14 → aixtools-0.2.16}/aixtools/utils/files.py +0 -0
- {aixtools-0.2.14 → aixtools-0.2.16}/aixtools/utils/persisted_dict.py +0 -0
- {aixtools-0.2.14 → aixtools-0.2.16}/aixtools/vault/__init__.py +0 -0
- {aixtools-0.2.14 → aixtools-0.2.16}/aixtools/vault/vault.py +0 -0
- {aixtools-0.2.14 → aixtools-0.2.16}/pyproject.toml +0 -0
- {aixtools-0.2.14 → aixtools-0.2.16}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aixtools
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.16
|
|
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
|
|
@@ -30,6 +30,7 @@ Testing Tools & Evals
|
|
|
30
30
|
- Tool Doctor System - `aixtools/tools/doctor/`
|
|
31
31
|
- Tool Recommendation Engine - `aixtools/tools/doctor/tool_recommendation.py`
|
|
32
32
|
- FaultyMCP - `aixtools/mcp/faulty_mcp.py`
|
|
33
|
+
- Agent Mock - `aixtools/testing/agent_mock.py`
|
|
33
34
|
|
|
34
35
|
Databases
|
|
35
36
|
- Database Integration - `aixtools/db/`
|
|
@@ -491,6 +492,21 @@ The framework includes a custom scoring system with [`average_assertions`](aixto
|
|
|
491
492
|
|
|
492
493
|
AIXtools provides comprehensive testing utilities and diagnostic tools for AI agent development and debugging.
|
|
493
494
|
|
|
495
|
+
### Running Tests
|
|
496
|
+
|
|
497
|
+
Execute the test suite using the provided scripts:
|
|
498
|
+
|
|
499
|
+
```bash
|
|
500
|
+
# Run all tests
|
|
501
|
+
./scripts/test.sh
|
|
502
|
+
|
|
503
|
+
# Run unit tests only
|
|
504
|
+
./scripts/test_unit.sh
|
|
505
|
+
|
|
506
|
+
# Run integration tests only
|
|
507
|
+
./scripts/test_integration.sh
|
|
508
|
+
```
|
|
509
|
+
|
|
494
510
|
### Testing Utilities
|
|
495
511
|
|
|
496
512
|
The testing module provides mock tools, model patching, and test utilities for comprehensive agent testing.
|
|
@@ -511,10 +527,6 @@ cached_response = cache.get_cached_response("test_prompt")
|
|
|
511
527
|
test_model = AixTestModel()
|
|
512
528
|
```
|
|
513
529
|
|
|
514
|
-
### Tool Doctor System
|
|
515
|
-
|
|
516
|
-
Automated tool analysis and recommendation system for optimizing agent tool usage and analyzing MCP servers.
|
|
517
|
-
|
|
518
530
|
#### MCP Tool Doctor
|
|
519
531
|
|
|
520
532
|
Analyze tools from MCP (Model Context Protocol) servers and receive AI-powered recommendations for improvement.
|
|
@@ -553,7 +565,7 @@ tool_doctor_mcp --stdio-command fastmcp --stdio-args run my_server.py --debug
|
|
|
553
565
|
# --debug Enable debug output
|
|
554
566
|
```
|
|
555
567
|
|
|
556
|
-
####
|
|
568
|
+
#### Tool Doctor
|
|
557
569
|
|
|
558
570
|
Analyze tool usage patterns from agent logs and get optimization recommendations.
|
|
559
571
|
|
|
@@ -648,19 +660,27 @@ python -m aixtools.mcp.faulty_mcp \
|
|
|
648
660
|
--prob-on-get-crash 0.1
|
|
649
661
|
```
|
|
650
662
|
|
|
651
|
-
###
|
|
663
|
+
### Agent Mock
|
|
652
664
|
|
|
653
|
-
|
|
665
|
+
Replay previously recorded agent runs without executing the actual agent. Useful for testing, debugging, and creating reproducible test cases.
|
|
654
666
|
|
|
655
|
-
```
|
|
656
|
-
|
|
657
|
-
|
|
667
|
+
```python
|
|
668
|
+
from aixtools.testing.agent_mock import AgentMock
|
|
669
|
+
from aixtools.agents.agent import get_agent, run_agent
|
|
658
670
|
|
|
659
|
-
# Run
|
|
660
|
-
|
|
671
|
+
# Run an agent and capture its execution
|
|
672
|
+
agent = get_agent(system_prompt="You are a helpful assistant.")
|
|
673
|
+
result, nodes = await run_agent(agent, "Explain quantum computing")
|
|
661
674
|
|
|
662
|
-
#
|
|
663
|
-
|
|
675
|
+
# Create a mock agent from the recorded nodes
|
|
676
|
+
agent_mock = AgentMock(nodes=nodes, result_output=result)
|
|
677
|
+
|
|
678
|
+
# Save the mock for later use
|
|
679
|
+
agent_mock.save(Path("test_data/quantum_mock.pkl"))
|
|
680
|
+
|
|
681
|
+
# Load and replay the mock agent
|
|
682
|
+
loaded_mock = AgentMock.load(Path("test_data/quantum_mock.pkl"))
|
|
683
|
+
result, nodes = await run_agent(loaded_mock, "any prompt") # Returns recorded nodes
|
|
664
684
|
```
|
|
665
685
|
|
|
666
686
|
## Chainlit & HTTP Server
|
|
@@ -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.16'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 2, 16)
|
|
33
33
|
|
|
34
|
-
__commit_id__ = commit_id = '
|
|
34
|
+
__commit_id__ = commit_id = 'gbf6656c3c'
|
|
@@ -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
|
|
@@ -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,42 +79,50 @@ 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
|
|
108
119
|
|
|
109
120
|
|
|
110
|
-
def save_objects_to_logfile(objects: list, log_dir=LOGS_DIR):
|
|
121
|
+
async def save_objects_to_logfile(objects: list, log_dir=LOGS_DIR):
|
|
111
122
|
"""Save the objects to a (pickle) log file"""
|
|
112
123
|
with ObjectLogger(log_dir=log_dir) as object_logger:
|
|
113
124
|
for obj in objects:
|
|
114
|
-
object_logger.log(obj)
|
|
125
|
+
await object_logger.log(obj)
|
|
115
126
|
|
|
116
127
|
|
|
117
128
|
class BaseLogger:
|
|
@@ -144,7 +155,7 @@ class ObjectLogger(BaseLogger):
|
|
|
144
155
|
log_dir=LOGS_DIR,
|
|
145
156
|
verbose: bool = True,
|
|
146
157
|
debug: bool | None = None,
|
|
147
|
-
parent_logger: Union["
|
|
158
|
+
parent_logger: Union["BaseLogger", NoneType] = None,
|
|
148
159
|
):
|
|
149
160
|
self.verbose = verbose
|
|
150
161
|
self.debug = (
|
|
@@ -195,8 +206,9 @@ class ObjectLogger(BaseLogger):
|
|
|
195
206
|
elif self.verbose:
|
|
196
207
|
print(obj, flush=True)
|
|
197
208
|
obj_to_save = safe_deepcopy(obj)
|
|
198
|
-
|
|
199
|
-
|
|
209
|
+
if self.file is not None:
|
|
210
|
+
pickle.dump(obj_to_save, self.file)
|
|
211
|
+
self.file.flush() # ensure it's written immediately
|
|
200
212
|
except Exception as e: # pylint: disable=broad-exception-caught
|
|
201
213
|
logger.error("Failed to log object: %s", e)
|
|
202
214
|
logger.error(traceback.format_exc())
|