agentic-testing 0.1.0__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.
- agentic_testing-0.1.0/LICENSE +21 -0
- agentic_testing-0.1.0/PKG-INFO +144 -0
- agentic_testing-0.1.0/README.md +110 -0
- agentic_testing-0.1.0/agent_test/__init__.py +3 -0
- agentic_testing-0.1.0/agent_test/src/agent_utils/__init__.py +0 -0
- agentic_testing-0.1.0/agent_test/src/agent_utils/models/agent_info.py +12 -0
- agentic_testing-0.1.0/agent_test/src/agent_utils/models/api_mock_type.py +15 -0
- agentic_testing-0.1.0/agent_test/src/agent_utils/models/global_metadata.py +55 -0
- agentic_testing-0.1.0/agent_test/src/agent_utils/remoterunnable_utils.py +47 -0
- agentic_testing-0.1.0/agent_test/src/common/agent_test_logger.py +22 -0
- agentic_testing-0.1.0/agent_test/src/fixture/fixture_class.py +229 -0
- agentic_testing-0.1.0/agent_test/src/fixture/mock_api/aiohttp_mock.py +17 -0
- agentic_testing-0.1.0/agent_test/src/fixture/mock_api/base_mock.py +26 -0
- agentic_testing-0.1.0/agent_test/src/fixture/mock_api/custom_mock.py +18 -0
- agentic_testing-0.1.0/agent_test/src/fixture/mock_api/db_mock.py +18 -0
- agentic_testing-0.1.0/agent_test/src/fixture/mock_api/graphql_mock.py +18 -0
- agentic_testing-0.1.0/agent_test/src/fixture/mock_api/grpc_mock.py +18 -0
- agentic_testing-0.1.0/agent_test/src/fixture/mock_api/httpx_mock.py +19 -0
- agentic_testing-0.1.0/agent_test/src/fixture/mock_api/mock_api_factory.py +1 -0
- agentic_testing-0.1.0/agent_test/src/fixture/mock_api/mq_mock.py +18 -0
- agentic_testing-0.1.0/agent_test/src/fixture/mock_api/requests_mock.py +25 -0
- agentic_testing-0.1.0/agent_test/src/fixture/mock_api/sdk_mock.py +18 -0
- agentic_testing-0.1.0/agent_test/src/fixture/mock_api/soap_mock.py +18 -0
- agentic_testing-0.1.0/agent_test/src/fixture/mock_api/urllib_mock.py +20 -0
- agentic_testing-0.1.0/agent_test/src/fixture/mock_api/utils.py +0 -0
- agentic_testing-0.1.0/agent_test/src/fixture/mock_api/websockets_mock.py +17 -0
- agentic_testing-0.1.0/agent_test/test/agent_utils/models/test_agent_info.py +12 -0
- agentic_testing-0.1.0/agent_test/test/agent_utils/models/test_api_mock_type.py +6 -0
- agentic_testing-0.1.0/agent_test/test/agent_utils/models/test_global_metadata.py +11 -0
- agentic_testing-0.1.0/agent_test/test/agent_utils/test_remoterunnable_utils.py +23 -0
- agentic_testing-0.1.0/agent_test/test/common/test_agent_test_logger.py +13 -0
- agentic_testing-0.1.0/agent_test/test/fixture/test_fixture_class.py +14 -0
- agentic_testing-0.1.0/agentic_testing.egg-info/PKG-INFO +144 -0
- agentic_testing-0.1.0/agentic_testing.egg-info/SOURCES.txt +51 -0
- agentic_testing-0.1.0/agentic_testing.egg-info/dependency_links.txt +1 -0
- agentic_testing-0.1.0/agentic_testing.egg-info/requires.txt +27 -0
- agentic_testing-0.1.0/agentic_testing.egg-info/top_level.txt +3 -0
- agentic_testing-0.1.0/examples/__init__.py +0 -0
- agentic_testing-0.1.0/examples/common/api1/api_code.py +11 -0
- agentic_testing-0.1.0/examples/common/worker1/main.py +24 -0
- agentic_testing-0.1.0/examples/common/worker2/main.py +24 -0
- agentic_testing-0.1.0/examples/common/worker3/main.py +24 -0
- agentic_testing-0.1.0/examples/crewai/orchestrator/orchestrator_code.py +116 -0
- agentic_testing-0.1.0/examples/langgraph/prompt_agentic/asynchronous/orchestrator_code.py +143 -0
- agentic_testing-0.1.0/examples/langgraph/prompt_agentic/asynchronous/test_orchestrator.py +57 -0
- agentic_testing-0.1.0/examples/langgraph/prompt_agentic/synchronous/orchestrator_code.py +137 -0
- agentic_testing-0.1.0/examples/langgraph/prompt_agentic/synchronous/test_orchestrator.py +85 -0
- agentic_testing-0.1.0/examples/langgraph/simple_graph/asynchronous/orchestrator_code.py +99 -0
- agentic_testing-0.1.0/examples/langgraph/simple_graph/asynchronous/test_orchestrator.py +67 -0
- agentic_testing-0.1.0/examples/langgraph/simple_graph/synchronous/orchestrator_code.py +99 -0
- agentic_testing-0.1.0/examples/langgraph/simple_graph/synchronous/test_orchestrator.py +66 -0
- agentic_testing-0.1.0/pyproject.toml +50 -0
- agentic_testing-0.1.0/setup.cfg +4 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Anant Gupta
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: agentic-testing
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Multi-agent orchestration and testing framework with LangGraph, CrewAI, and FastAPI.
|
|
5
|
+
Author-email: Anant Gupta <anantguptadbl@gmail.com>, Ritesh Jena <riteshjena@gmail.com>
|
|
6
|
+
Requires-Python: >=3.13
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Dist: fastapi>=0.110.0
|
|
10
|
+
Requires-Dist: uvicorn>=0.29.0
|
|
11
|
+
Requires-Dist: httpx>=0.27.0
|
|
12
|
+
Requires-Dist: langchain>=0.1.0
|
|
13
|
+
Requires-Dist: langgraph>=1.0.7
|
|
14
|
+
Requires-Dist: crewai
|
|
15
|
+
Requires-Dist: pytest
|
|
16
|
+
Requires-Dist: pytest-asyncio
|
|
17
|
+
Requires-Dist: langserve
|
|
18
|
+
Provides-Extra: test
|
|
19
|
+
Requires-Dist: pytest; extra == "test"
|
|
20
|
+
Requires-Dist: httpx; extra == "test"
|
|
21
|
+
Requires-Dist: pytest-asyncio; extra == "test"
|
|
22
|
+
Provides-Extra: langgraph
|
|
23
|
+
Requires-Dist: langgraph>=1.0.7; extra == "langgraph"
|
|
24
|
+
Requires-Dist: langchain>=0.1.0; extra == "langgraph"
|
|
25
|
+
Requires-Dist: fastapi>=0.110.0; extra == "langgraph"
|
|
26
|
+
Requires-Dist: uvicorn>=0.29.0; extra == "langgraph"
|
|
27
|
+
Requires-Dist: httpx>=0.27.0; extra == "langgraph"
|
|
28
|
+
Provides-Extra: crewai
|
|
29
|
+
Requires-Dist: crewai; extra == "crewai"
|
|
30
|
+
Requires-Dist: fastapi>=0.110.0; extra == "crewai"
|
|
31
|
+
Requires-Dist: uvicorn>=0.29.0; extra == "crewai"
|
|
32
|
+
Requires-Dist: httpx>=0.27.0; extra == "crewai"
|
|
33
|
+
Dynamic: license-file
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# Agent Testing Library
|
|
37
|
+
|
|
38
|
+
This repository provides a comprehensive framework for building, testing, and orchestrating agentic workflows in Python. It includes utilities, fixtures, logging, and a rich set of examples demonstrating integration with various agentic frameworks and orchestration patterns.
|
|
39
|
+
|
|
40
|
+
## Project Structure
|
|
41
|
+
|
|
42
|
+
- **agent_test/**: Core testing library for agents
|
|
43
|
+
- **src/agent_utils/**: Utilities for agent orchestration and metadata
|
|
44
|
+
- `remoterunnable_utils.py`, `models/agent_info.py`, `models/api_mock_type.py`, `models/global_metadata.py`
|
|
45
|
+
- **common/**: Logging and shared utilities
|
|
46
|
+
- `agent_test_logger.py`
|
|
47
|
+
- **fixture/**: Test fixtures and API mocks
|
|
48
|
+
- `fixture_class.py`, `mock_api/` (various protocol mocks)
|
|
49
|
+
- **test/**: Unit tests for all modules
|
|
50
|
+
- `test_remoterunnable_utils.py`, `test_agent_info.py`, `test_api_mock_type.py`, `test_global_metadata.py`, `test_agent_test_logger.py`, `test_fixture_class.py`
|
|
51
|
+
|
|
52
|
+
- **examples/**: Practical agentic workflow examples
|
|
53
|
+
- **common/**: Shared example code
|
|
54
|
+
- `api1/api_code.py`, `worker1/main.py`, `worker2/main.py`, `worker3/main.py`
|
|
55
|
+
- **crewai/**: CrewAI orchestration example
|
|
56
|
+
- `orchestrator/orchestrator_code.py`
|
|
57
|
+
- **langgraph/**: LangGraph agentic workflows
|
|
58
|
+
- `prompt_agentic/asynchronous/orchestrator_code.py`, `prompt_agentic/asynchronous/test_orchestrator.py`
|
|
59
|
+
- `prompt_agentic/synchronous/orchestrator_code.py`, `prompt_agentic/synchronous/test_orchestrator.py`
|
|
60
|
+
- `simple_graph/orchestrator_code.py`, `simple_graph/test_orchestrator.py`
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
## Examples Section
|
|
64
|
+
|
|
65
|
+
The `examples/` directory demonstrates how to build and orchestrate agentic workflows using different frameworks and patterns. Below are direct code snippets from key example files:
|
|
66
|
+
|
|
67
|
+
### common/api1/api_code.py
|
|
68
|
+
```python
|
|
69
|
+
def api_call(input_data):
|
|
70
|
+
# Simulate API logic
|
|
71
|
+
return {"result": f"Processed {input_data}"}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### common/worker1/main.py
|
|
75
|
+
```python
|
|
76
|
+
from common.api1.api_code import api_call
|
|
77
|
+
|
|
78
|
+
def worker1_task(data):
|
|
79
|
+
response = api_call(data)
|
|
80
|
+
print("Worker1 received:", response)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### crewai/orchestrator/orchestrator_code.py
|
|
84
|
+
```python
|
|
85
|
+
def orchestrate_agents(agent_list, task):
|
|
86
|
+
results = []
|
|
87
|
+
for agent in agent_list:
|
|
88
|
+
result = agent.run(task)
|
|
89
|
+
results.append(result)
|
|
90
|
+
return results
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### langgraph/prompt_agentic/asynchronous/orchestrator_code.py
|
|
94
|
+
```python
|
|
95
|
+
import asyncio
|
|
96
|
+
|
|
97
|
+
async def async_orchestrate(agents, task):
|
|
98
|
+
tasks = [agent.run_async(task) for agent in agents]
|
|
99
|
+
return await asyncio.gather(*tasks)
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### langgraph/prompt_agentic/synchronous/orchestrator_code.py
|
|
103
|
+
```python
|
|
104
|
+
def sync_orchestrate(agents, task):
|
|
105
|
+
return [agent.run(task) for agent in agents]
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### langgraph/simple_graph/orchestrator_code.py
|
|
109
|
+
```python
|
|
110
|
+
def simple_graph_orchestrate(nodes, input_data):
|
|
111
|
+
output = input_data
|
|
112
|
+
for node in nodes:
|
|
113
|
+
output = node.process(output)
|
|
114
|
+
return output
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Testing
|
|
118
|
+
|
|
119
|
+
Unit tests are provided in the `agent_test/test/` directory. Run all tests with:
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
pytest agent_test/test
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## License
|
|
126
|
+
|
|
127
|
+
See LICENSE for details.
|
|
128
|
+
|
|
129
|
+
## Future Work
|
|
130
|
+
|
|
131
|
+
Agentic Framework Alternatives (besides langgraph):
|
|
132
|
+
|
|
133
|
+
CrewAI (multi-agent orchestration)
|
|
134
|
+
Autogen (Microsoft’s multi-agent framework)
|
|
135
|
+
LangChain (chains, agents, tools)
|
|
136
|
+
Haystack (for RAG and agent pipelines)
|
|
137
|
+
OpenAI Function Calling (tool-using agents)
|
|
138
|
+
Semantic Kernel (Microsoft’s orchestration)
|
|
139
|
+
Custom agent frameworks (your own classes)
|
|
140
|
+
HuggingFace Transformers Agents
|
|
141
|
+
LlamaIndex (for agentic workflows)
|
|
142
|
+
DSPy (for programmatic LLM pipelines)
|
|
143
|
+
PromptChainer (open-source agentic framework)
|
|
144
|
+
Direct function/callback-based agents
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
|
|
2
|
+
# Agent Testing Library
|
|
3
|
+
|
|
4
|
+
This repository provides a comprehensive framework for building, testing, and orchestrating agentic workflows in Python. It includes utilities, fixtures, logging, and a rich set of examples demonstrating integration with various agentic frameworks and orchestration patterns.
|
|
5
|
+
|
|
6
|
+
## Project Structure
|
|
7
|
+
|
|
8
|
+
- **agent_test/**: Core testing library for agents
|
|
9
|
+
- **src/agent_utils/**: Utilities for agent orchestration and metadata
|
|
10
|
+
- `remoterunnable_utils.py`, `models/agent_info.py`, `models/api_mock_type.py`, `models/global_metadata.py`
|
|
11
|
+
- **common/**: Logging and shared utilities
|
|
12
|
+
- `agent_test_logger.py`
|
|
13
|
+
- **fixture/**: Test fixtures and API mocks
|
|
14
|
+
- `fixture_class.py`, `mock_api/` (various protocol mocks)
|
|
15
|
+
- **test/**: Unit tests for all modules
|
|
16
|
+
- `test_remoterunnable_utils.py`, `test_agent_info.py`, `test_api_mock_type.py`, `test_global_metadata.py`, `test_agent_test_logger.py`, `test_fixture_class.py`
|
|
17
|
+
|
|
18
|
+
- **examples/**: Practical agentic workflow examples
|
|
19
|
+
- **common/**: Shared example code
|
|
20
|
+
- `api1/api_code.py`, `worker1/main.py`, `worker2/main.py`, `worker3/main.py`
|
|
21
|
+
- **crewai/**: CrewAI orchestration example
|
|
22
|
+
- `orchestrator/orchestrator_code.py`
|
|
23
|
+
- **langgraph/**: LangGraph agentic workflows
|
|
24
|
+
- `prompt_agentic/asynchronous/orchestrator_code.py`, `prompt_agentic/asynchronous/test_orchestrator.py`
|
|
25
|
+
- `prompt_agentic/synchronous/orchestrator_code.py`, `prompt_agentic/synchronous/test_orchestrator.py`
|
|
26
|
+
- `simple_graph/orchestrator_code.py`, `simple_graph/test_orchestrator.py`
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
## Examples Section
|
|
30
|
+
|
|
31
|
+
The `examples/` directory demonstrates how to build and orchestrate agentic workflows using different frameworks and patterns. Below are direct code snippets from key example files:
|
|
32
|
+
|
|
33
|
+
### common/api1/api_code.py
|
|
34
|
+
```python
|
|
35
|
+
def api_call(input_data):
|
|
36
|
+
# Simulate API logic
|
|
37
|
+
return {"result": f"Processed {input_data}"}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### common/worker1/main.py
|
|
41
|
+
```python
|
|
42
|
+
from common.api1.api_code import api_call
|
|
43
|
+
|
|
44
|
+
def worker1_task(data):
|
|
45
|
+
response = api_call(data)
|
|
46
|
+
print("Worker1 received:", response)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### crewai/orchestrator/orchestrator_code.py
|
|
50
|
+
```python
|
|
51
|
+
def orchestrate_agents(agent_list, task):
|
|
52
|
+
results = []
|
|
53
|
+
for agent in agent_list:
|
|
54
|
+
result = agent.run(task)
|
|
55
|
+
results.append(result)
|
|
56
|
+
return results
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### langgraph/prompt_agentic/asynchronous/orchestrator_code.py
|
|
60
|
+
```python
|
|
61
|
+
import asyncio
|
|
62
|
+
|
|
63
|
+
async def async_orchestrate(agents, task):
|
|
64
|
+
tasks = [agent.run_async(task) for agent in agents]
|
|
65
|
+
return await asyncio.gather(*tasks)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### langgraph/prompt_agentic/synchronous/orchestrator_code.py
|
|
69
|
+
```python
|
|
70
|
+
def sync_orchestrate(agents, task):
|
|
71
|
+
return [agent.run(task) for agent in agents]
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### langgraph/simple_graph/orchestrator_code.py
|
|
75
|
+
```python
|
|
76
|
+
def simple_graph_orchestrate(nodes, input_data):
|
|
77
|
+
output = input_data
|
|
78
|
+
for node in nodes:
|
|
79
|
+
output = node.process(output)
|
|
80
|
+
return output
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Testing
|
|
84
|
+
|
|
85
|
+
Unit tests are provided in the `agent_test/test/` directory. Run all tests with:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
pytest agent_test/test
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## License
|
|
92
|
+
|
|
93
|
+
See LICENSE for details.
|
|
94
|
+
|
|
95
|
+
## Future Work
|
|
96
|
+
|
|
97
|
+
Agentic Framework Alternatives (besides langgraph):
|
|
98
|
+
|
|
99
|
+
CrewAI (multi-agent orchestration)
|
|
100
|
+
Autogen (Microsoft’s multi-agent framework)
|
|
101
|
+
LangChain (chains, agents, tools)
|
|
102
|
+
Haystack (for RAG and agent pipelines)
|
|
103
|
+
OpenAI Function Calling (tool-using agents)
|
|
104
|
+
Semantic Kernel (Microsoft’s orchestration)
|
|
105
|
+
Custom agent frameworks (your own classes)
|
|
106
|
+
HuggingFace Transformers Agents
|
|
107
|
+
LlamaIndex (for agentic workflows)
|
|
108
|
+
DSPy (for programmatic LLM pipelines)
|
|
109
|
+
PromptChainer (open-source agentic framework)
|
|
110
|
+
Direct function/callback-based agents
|
|
File without changes
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from enum import Enum, auto
|
|
2
|
+
|
|
3
|
+
class APIMockType(Enum):
|
|
4
|
+
REQUESTS = auto()
|
|
5
|
+
HTTPX = auto()
|
|
6
|
+
URLLIB = auto()
|
|
7
|
+
AIOHTTP = auto()
|
|
8
|
+
GRPC = auto()
|
|
9
|
+
WEBSOCKETS = auto()
|
|
10
|
+
CUSTOM = auto()
|
|
11
|
+
SDK = auto()
|
|
12
|
+
DB = auto()
|
|
13
|
+
MQ = auto()
|
|
14
|
+
GRAPHQL = auto()
|
|
15
|
+
SOAP = auto()
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
|
|
2
|
+
from abc import abstractmethod
|
|
3
|
+
|
|
4
|
+
from agent_test.src.agent_utils.models.api_mock_type import APIMockType
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class GlobalMetadata:
|
|
8
|
+
_api_patcher_registry = {}
|
|
9
|
+
|
|
10
|
+
@classmethod
|
|
11
|
+
def identify_patcher_type(cls, api_type: APIMockType = APIMockType.REQUESTS):
|
|
12
|
+
"""
|
|
13
|
+
Identify the patcher class based on the payload using the factory module.
|
|
14
|
+
"""
|
|
15
|
+
if not cls._api_patcher_registry:
|
|
16
|
+
cls._api_patcher_registry = cls._build_api_patcher_registry()
|
|
17
|
+
# print(f"GlobalMetadata: identify_patcher_type called with api_type={api_type} and the registry={cls._api_patcher_registry} ")
|
|
18
|
+
return cls._api_patcher_registry.get(api_type, None)
|
|
19
|
+
|
|
20
|
+
def _build_api_patcher_registry():
|
|
21
|
+
import importlib
|
|
22
|
+
import pkgutil
|
|
23
|
+
from agent_test.src.fixture.mock_api import base_mock
|
|
24
|
+
from agent_test.src.fixture.mock_api.base_mock import BaseAPIMock
|
|
25
|
+
import agent_test.src.fixture.mock_api
|
|
26
|
+
|
|
27
|
+
# Dynamically import all modules in the mock_api package
|
|
28
|
+
package = agent_test.src.fixture.mock_api
|
|
29
|
+
for _, module_name, is_pkg in pkgutil.iter_modules(package.__path__, package.__name__ + "."):
|
|
30
|
+
if not is_pkg and not module_name.endswith("base_mock"):
|
|
31
|
+
try:
|
|
32
|
+
importlib.import_module(module_name)
|
|
33
|
+
except Exception as e:
|
|
34
|
+
print(f"GlobalMetadata: Failed to import {module_name}: {e}")
|
|
35
|
+
|
|
36
|
+
# print("GlobalMetadata: Building API patcher registry...")
|
|
37
|
+
# print("GlobalMetadata: Found subclasses of BaseAPIMock:", BaseAPIMock.__subclasses__())
|
|
38
|
+
registry = {}
|
|
39
|
+
for cls in BaseAPIMock.__subclasses__():
|
|
40
|
+
# print(f"GlobalMetadata: Registering API patcher class {cls} ")
|
|
41
|
+
instance = cls()
|
|
42
|
+
api_type = instance.get_api_type()
|
|
43
|
+
# print(f"GlobalMetadata: Identified api_type={api_type} for class {cls} ")
|
|
44
|
+
if api_type:
|
|
45
|
+
registry[api_type] = cls
|
|
46
|
+
return registry
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def get_api_patcher_class(api_type: str):
|
|
50
|
+
"""
|
|
51
|
+
Factory function to select the correct API patcher class based on payload.
|
|
52
|
+
Default is RequestsAPIMock.
|
|
53
|
+
"""
|
|
54
|
+
from agent_test.fixture.mock_api.base_mock import BaseAPIMock
|
|
55
|
+
return BaseAPIMock._api_patcher_registry.get(api_type)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
import pkgutil
|
|
3
|
+
import importlib
|
|
4
|
+
import inspect
|
|
5
|
+
from langserve import RemoteRunnable
|
|
6
|
+
from langchain_core.runnables import Runnable, RunnableLambda
|
|
7
|
+
from agent_test.src.agent_utils.models.agent_info import AgentInfo
|
|
8
|
+
|
|
9
|
+
# List of supported runnable types
|
|
10
|
+
RUNNABLE_TYPES = [RemoteRunnable, Runnable, RunnableLambda]
|
|
11
|
+
|
|
12
|
+
def find_all_remoterunnables(package_name):
|
|
13
|
+
"""
|
|
14
|
+
Scans the given package and returns a dict of agent_name to RemoteRunnableInfo
|
|
15
|
+
for all RemoteRunnable objects found in the package and its submodules.
|
|
16
|
+
"""
|
|
17
|
+
remoterunnables = {}
|
|
18
|
+
try:
|
|
19
|
+
package = importlib.import_module(package_name)
|
|
20
|
+
except Exception as e:
|
|
21
|
+
print(f"find_all_remoterunnables: Could not import package '{package_name}': {e}")
|
|
22
|
+
return remoterunnables
|
|
23
|
+
for _, modname, _ in pkgutil.walk_packages(package.__path__, package.__name__ + "."):
|
|
24
|
+
try:
|
|
25
|
+
module = importlib.import_module(modname)
|
|
26
|
+
for name, obj in inspect.getmembers(module):
|
|
27
|
+
if isinstance(obj, tuple(RUNNABLE_TYPES)):
|
|
28
|
+
info = AgentInfo(
|
|
29
|
+
agent_name=name,
|
|
30
|
+
agent_path=f"{modname}.{name}",
|
|
31
|
+
agent_type=type(obj).__name__,
|
|
32
|
+
agent_module_path=modname
|
|
33
|
+
)
|
|
34
|
+
remoterunnables[name] = info
|
|
35
|
+
except Exception:
|
|
36
|
+
pass
|
|
37
|
+
print(f"find_all_remoterunnables: Found remoterunnables: {remoterunnables}")
|
|
38
|
+
return remoterunnables
|
|
39
|
+
|
|
40
|
+
def find_async_nodes_in_graph(graph):
|
|
41
|
+
"""Returns a list of node names that are async in the given LangGraph graph object."""
|
|
42
|
+
async_nodes = []
|
|
43
|
+
for node_name, node_func in getattr(graph, 'nodes', {}).items():
|
|
44
|
+
if inspect.iscoroutinefunction(node_func):
|
|
45
|
+
async_nodes.append(node_name)
|
|
46
|
+
print(f"find_async_nodes_in_graph: Found async nodes: {async_nodes}")
|
|
47
|
+
return async_nodes
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
class AgentTestLogger:
|
|
4
|
+
_logger = None
|
|
5
|
+
|
|
6
|
+
@staticmethod
|
|
7
|
+
def get_logger(name: str = "agent_test_logger"):
|
|
8
|
+
if AgentTestLogger._logger is None:
|
|
9
|
+
logger = logging.getLogger(name)
|
|
10
|
+
logger.setLevel(logging.DEBUG)
|
|
11
|
+
if not logger.handlers:
|
|
12
|
+
handler = logging.StreamHandler()
|
|
13
|
+
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
|
14
|
+
handler.setFormatter(formatter)
|
|
15
|
+
logger.addHandler(handler)
|
|
16
|
+
AgentTestLogger._logger = logger
|
|
17
|
+
return AgentTestLogger._logger
|
|
18
|
+
|
|
19
|
+
@staticmethod
|
|
20
|
+
def set_logger(logger):
|
|
21
|
+
"""Set a custom logger instance."""
|
|
22
|
+
AgentTestLogger._logger = logger
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
# FixtureLibrary: Chainable test fixture builder for agent orchestration
|
|
2
|
+
|
|
3
|
+
from agent_test.src.agent_utils.models.api_mock_type import APIMockType
|
|
4
|
+
from agent_test.src.agent_utils.models.global_metadata import GlobalMetadata
|
|
5
|
+
from agent_test.src.common.agent_test_logger import AgentTestLogger
|
|
6
|
+
from unittest.mock import AsyncMock, Mock, patch
|
|
7
|
+
from agent_test.src.agent_utils.remoterunnable_utils import find_all_remoterunnables
|
|
8
|
+
from agent_test.src.agent_utils.models.agent_info import AgentInfo
|
|
9
|
+
|
|
10
|
+
logger = AgentTestLogger.get_logger()
|
|
11
|
+
|
|
12
|
+
class FixtureLibrary:
|
|
13
|
+
|
|
14
|
+
def __init__(self, root_path: str = None):
|
|
15
|
+
if root_path is None:
|
|
16
|
+
# Use current package path if available, else fallback to 'orchestrator'
|
|
17
|
+
root_path = __package__ if __package__ else "orchestrator"
|
|
18
|
+
self._input_state = None
|
|
19
|
+
self._api_mocks = []
|
|
20
|
+
self._agent_invocations = []
|
|
21
|
+
self._agent_responses = []
|
|
22
|
+
self._patchers = []
|
|
23
|
+
self.results = []
|
|
24
|
+
self._root_path = root_path
|
|
25
|
+
# Load all agent info dict at initialization
|
|
26
|
+
self.agent_info_dict = find_all_remoterunnables(self._root_path)
|
|
27
|
+
logger.debug(f"__init__: Initialized FixtureLibrary with members: "
|
|
28
|
+
f"_input_state={self._input_state}, "
|
|
29
|
+
f"_api_mocks={self._api_mocks}, "
|
|
30
|
+
f"_agent_invocations={self._agent_invocations}, "
|
|
31
|
+
f"_agent_responses={self._agent_responses}, "
|
|
32
|
+
f"_patchers={self._patchers}, "
|
|
33
|
+
f"agent_info_dict keys={list(self.agent_info_dict.keys())}")
|
|
34
|
+
|
|
35
|
+
def when_input_state(self, state):
|
|
36
|
+
logger.debug(f"when_input_state: called with state={state}")
|
|
37
|
+
self._input_state = state
|
|
38
|
+
logger.debug(f"when_input_state: _input_state set to {self._input_state}")
|
|
39
|
+
return self
|
|
40
|
+
|
|
41
|
+
def mock_api_call(self, api_path, payload, return_value, api_type:APIMockType=APIMockType.REQUESTS):
|
|
42
|
+
logger.debug(f"mock_api_call: called with api_path={api_path}, payload={payload}, return_value={return_value}")
|
|
43
|
+
patcher_class = GlobalMetadata.identify_patcher_type(api_type)
|
|
44
|
+
patcher = patcher_class().create_patcher(api_path, payload, return_value)
|
|
45
|
+
self._patchers.append((patcher, None, None))
|
|
46
|
+
self._api_mocks.append((api_path, payload, return_value))
|
|
47
|
+
logger.debug(f"mock_api_call: _patchers updated: {self._patchers}")
|
|
48
|
+
logger.debug(f"mock_api_call: _api_mocks updated: {self._api_mocks}")
|
|
49
|
+
return self
|
|
50
|
+
|
|
51
|
+
def expect_agent_invocation(self, agent_name, state, agent_type="invoke", ntimes=1):
|
|
52
|
+
logger.debug(
|
|
53
|
+
f"expect_agent_invocation: called with agent_name={agent_name}, state={state}"
|
|
54
|
+
)
|
|
55
|
+
if (
|
|
56
|
+
self.was_agent_method_called(
|
|
57
|
+
agent_name, agent_type, ntimes, input_args=state
|
|
58
|
+
)
|
|
59
|
+
== False
|
|
60
|
+
):
|
|
61
|
+
raise AssertionError(
|
|
62
|
+
f"Expected agent '{agent_name}' to be invoked with state {state}, but it was not."
|
|
63
|
+
)
|
|
64
|
+
return self
|
|
65
|
+
# self._agent_invocations.append((agent_name, state))
|
|
66
|
+
# print(f"_agent_invocations updated: {self._agent_invocations}")
|
|
67
|
+
|
|
68
|
+
def mock_agent_response(self, agent_name, response_state):
|
|
69
|
+
logger.debug(
|
|
70
|
+
f"mock_agent_response: called with agent_name={agent_name}, response_state={response_state}"
|
|
71
|
+
)
|
|
72
|
+
# Use agent_info_dict to infer the patch path
|
|
73
|
+
agent_info = self._get_agent_info(agent_name)
|
|
74
|
+
module_path = agent_info.agent_path
|
|
75
|
+
for method in ["invoke", "ainvoke", "batch"]:
|
|
76
|
+
patcher = self._create_agent_patcher(module_path, method, response_state)
|
|
77
|
+
self._patchers.append((patcher, agent_name, method))
|
|
78
|
+
self._agent_responses.append((agent_name, response_state))
|
|
79
|
+
# logger.debug(f"mock_agent_response: _patchers updated: {self._patchers}")
|
|
80
|
+
# logger.debug(f"mock_agent_response: _agent_responses updated: {self._agent_responses}")
|
|
81
|
+
return self
|
|
82
|
+
|
|
83
|
+
def _get_agent_info(self, agent_name):
|
|
84
|
+
agent_info: AgentInfo = self.agent_info_dict.get(agent_name)
|
|
85
|
+
if agent_info is None:
|
|
86
|
+
raise ValueError(f"Agent '{agent_name}' not found in agent_info_dict.")
|
|
87
|
+
# logger.debug(f"_get_agent_info: Found agent_info: {agent_info} for agent_name: {agent_name}")
|
|
88
|
+
return agent_info
|
|
89
|
+
|
|
90
|
+
def _create_agent_patcher(self, module_path, method, response_state):
|
|
91
|
+
patch_path = f"{module_path}.{method}"
|
|
92
|
+
if method in ["ainvoke"]:
|
|
93
|
+
# Patch async methods to return a coroutine resolved to response_state
|
|
94
|
+
async_mock = AsyncMock()
|
|
95
|
+
async_mock.return_value = response_state
|
|
96
|
+
return patch(patch_path, new=async_mock)
|
|
97
|
+
else:
|
|
98
|
+
return patch(patch_path, return_value=response_state)
|
|
99
|
+
|
|
100
|
+
def __enter__(self):
|
|
101
|
+
# logger.debug("__enter__: called. Starting all patchers.")
|
|
102
|
+
self._started_patches = self._start_all_patchers()
|
|
103
|
+
# logger.debug(f"__enter__: _started_patches: {self._started_patches}")
|
|
104
|
+
return self
|
|
105
|
+
|
|
106
|
+
def _start_all_patchers(self):
|
|
107
|
+
return [patcher.start() for patcher, _, _ in self._patchers]
|
|
108
|
+
|
|
109
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
110
|
+
# logger.debug("__exit__: called. Stopping all patchers.")
|
|
111
|
+
self._stop_all_patchers()
|
|
112
|
+
# logger.debug("__exit__: All patchers stopped.")
|
|
113
|
+
|
|
114
|
+
def _stop_all_patchers(self):
|
|
115
|
+
for patcher, _, _ in self._patchers:
|
|
116
|
+
patcher.stop()
|
|
117
|
+
|
|
118
|
+
def run(self, test_func):
|
|
119
|
+
# logger.debug(f"run: called with test_func={test_func}")
|
|
120
|
+
with self:
|
|
121
|
+
result = test_func(self._input_state)
|
|
122
|
+
# logger.debug(f"run: result: {result}")
|
|
123
|
+
return result
|
|
124
|
+
|
|
125
|
+
def invoke_function(self, func):
|
|
126
|
+
# logger.debug(f"invoke_function: called with func={func}")
|
|
127
|
+
# logger.debug(f"invoke_function: Using _input_state: {self._input_state}")
|
|
128
|
+
# logger.debug(f"invoke_function: Type of input_state: {type(self._input_state)}")
|
|
129
|
+
# logger.debug(f"invoke_function: Type of func: {type(func)}")
|
|
130
|
+
with self:
|
|
131
|
+
result = func(self._input_state)
|
|
132
|
+
# logger.debug(f"invoke_function: result: {result}")
|
|
133
|
+
self.results.append(result)
|
|
134
|
+
return self
|
|
135
|
+
|
|
136
|
+
def invoke_graph(self, graph):
|
|
137
|
+
# logger.debug(f"invoke_graph: called with graph={graph}")
|
|
138
|
+
# logger.debug(f"invoke_graph: Using _input_state: {self._input_state}")
|
|
139
|
+
# logger.debug(f"invoke_graph: Type of input_state: {type(self._input_state)}")
|
|
140
|
+
# logger.debug(f"invoke_graph: Type of graph: {type(graph)}")
|
|
141
|
+
with self:
|
|
142
|
+
result = graph.invoke(self._input_state)
|
|
143
|
+
# logger.debug(f"invoke_graph: result: {result}")
|
|
144
|
+
self.results.append(result)
|
|
145
|
+
return self
|
|
146
|
+
|
|
147
|
+
async def ainvoke_graph(self, graph):
|
|
148
|
+
# logger.debug(f"ainvoke_graph: called with graph={graph}")
|
|
149
|
+
# logger.debug(f"ainvoke_graph: Using _input_state: {self._input_state}")
|
|
150
|
+
# logger.debug(f"ainvoke_graph: Type of input_state: {type(self._input_state)}")
|
|
151
|
+
# logger.debug(f"ainvoke_graph: Type of graph: {type(graph)}")
|
|
152
|
+
with self:
|
|
153
|
+
result = await graph.ainvoke(self._input_state)
|
|
154
|
+
# logger.debug(f"ainvoke_graph: result: {result}")
|
|
155
|
+
self.results.append(result)
|
|
156
|
+
return self
|
|
157
|
+
|
|
158
|
+
def cleanup(self):
|
|
159
|
+
self._stop_all_patchers()
|
|
160
|
+
|
|
161
|
+
def _get_patch_index_by_agent(self, agent_name, method=None):
|
|
162
|
+
"""
|
|
163
|
+
Returns the index of the patch for a given agent and method (e.g., 'invoke', 'ainvoke', 'batch').
|
|
164
|
+
If method is None, returns all indices for the agent.
|
|
165
|
+
"""
|
|
166
|
+
indices = []
|
|
167
|
+
for i, patch_info in enumerate(self._patchers):
|
|
168
|
+
# patch_info is (patcher, agent_name, method) for agent patches, or just patcher for others
|
|
169
|
+
if isinstance(patch_info, tuple) and len(patch_info) == 3:
|
|
170
|
+
_, patch_agent_name, patch_method = patch_info
|
|
171
|
+
if patch_agent_name == agent_name:
|
|
172
|
+
if method is None or patch_method == method:
|
|
173
|
+
indices.append(i)
|
|
174
|
+
return indices if method is None else (indices[0] if indices else None)
|
|
175
|
+
|
|
176
|
+
def _was_patch_called(self, patch_index):
|
|
177
|
+
"""
|
|
178
|
+
Returns True if the patch at patch_index was called during the test run.
|
|
179
|
+
"""
|
|
180
|
+
if not hasattr(self, '_started_patches'):
|
|
181
|
+
raise RuntimeError("Patches have not been started. Use within a context manager.")
|
|
182
|
+
mock_obj = self._started_patches[patch_index]
|
|
183
|
+
return getattr(mock_obj, 'called', False)
|
|
184
|
+
|
|
185
|
+
def was_agent_method_called(self, agent_name, method, expected_count=1, input_args=None):
|
|
186
|
+
"""
|
|
187
|
+
Asserts the patch for the given agent and method was called expected_count times.
|
|
188
|
+
If input_args is provided, only counts calls with matching args.
|
|
189
|
+
Returns True if assertion passes, else raises AssertionError.
|
|
190
|
+
"""
|
|
191
|
+
# logger.debug(f"was_agent_method_called: called with agent_name={agent_name}, method={method}, expected_count={expected_count}, input_args={input_args}")
|
|
192
|
+
# logger.debug(f"was_agent_method_called: The self._started_patches are: {self._started_patches}")
|
|
193
|
+
idx = self._get_patch_index_by_agent(agent_name, method)
|
|
194
|
+
if idx is None:
|
|
195
|
+
raise ValueError(f"No patch found for agent '{agent_name}' and method '{method}'")
|
|
196
|
+
if not hasattr(self, '_started_patches'):
|
|
197
|
+
raise RuntimeError("Patches have not been started. Use within a context manager.")
|
|
198
|
+
mock_obj = self._started_patches[idx]
|
|
199
|
+
# logger.debug(f"was_agent_method_called: The mock_obj for agent '{agent_name}' and method '{method}' is: {mock_obj}")
|
|
200
|
+
# If input_args is None, count all calls
|
|
201
|
+
if input_args is None:
|
|
202
|
+
call_count = mock_obj.call_count
|
|
203
|
+
else:
|
|
204
|
+
# Filter calls by input_args
|
|
205
|
+
# logger.debug(f"was_agent_method_called: Counting calls with specific input_args: {input_args} and agent_name: {agent_name}, method: {method}")
|
|
206
|
+
call_count = 0
|
|
207
|
+
for call in getattr(mock_obj, 'call_args_list', []):
|
|
208
|
+
args, kwargs = call
|
|
209
|
+
# logger.debug(f"was_agent_method_called: The current call has args: {args}, kwargs: {kwargs}")
|
|
210
|
+
print(f"[DEBUG] Checking call args for agent '{agent_name}' method '{method}': {args} against input_args: {input_args}")
|
|
211
|
+
if(args and len(args) == 1 and args[0] == input_args):
|
|
212
|
+
call_count += 1
|
|
213
|
+
assert call_count == expected_count, (
|
|
214
|
+
f"Expected {expected_count} calls to agent '{agent_name}' method '{method}', but got {call_count} (input_args={input_args})"
|
|
215
|
+
)
|
|
216
|
+
return True
|
|
217
|
+
|
|
218
|
+
def was_api_patch_called(self, api_path):
|
|
219
|
+
"""
|
|
220
|
+
Returns True if the patch for the given API path was called.
|
|
221
|
+
"""
|
|
222
|
+
for i, patcher in enumerate(self._patchers):
|
|
223
|
+
target = getattr(patcher, 'attribute', None) or getattr(patcher, 'target', None)
|
|
224
|
+
if target == api_path:
|
|
225
|
+
return self._was_patch_called(i)
|
|
226
|
+
raise ValueError(f"No patch found for API path '{api_path}'")
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
# Optionally, add more helpers for assertions or reporting as needed
|