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.
Files changed (53) hide show
  1. agentic_testing-0.1.0/LICENSE +21 -0
  2. agentic_testing-0.1.0/PKG-INFO +144 -0
  3. agentic_testing-0.1.0/README.md +110 -0
  4. agentic_testing-0.1.0/agent_test/__init__.py +3 -0
  5. agentic_testing-0.1.0/agent_test/src/agent_utils/__init__.py +0 -0
  6. agentic_testing-0.1.0/agent_test/src/agent_utils/models/agent_info.py +12 -0
  7. agentic_testing-0.1.0/agent_test/src/agent_utils/models/api_mock_type.py +15 -0
  8. agentic_testing-0.1.0/agent_test/src/agent_utils/models/global_metadata.py +55 -0
  9. agentic_testing-0.1.0/agent_test/src/agent_utils/remoterunnable_utils.py +47 -0
  10. agentic_testing-0.1.0/agent_test/src/common/agent_test_logger.py +22 -0
  11. agentic_testing-0.1.0/agent_test/src/fixture/fixture_class.py +229 -0
  12. agentic_testing-0.1.0/agent_test/src/fixture/mock_api/aiohttp_mock.py +17 -0
  13. agentic_testing-0.1.0/agent_test/src/fixture/mock_api/base_mock.py +26 -0
  14. agentic_testing-0.1.0/agent_test/src/fixture/mock_api/custom_mock.py +18 -0
  15. agentic_testing-0.1.0/agent_test/src/fixture/mock_api/db_mock.py +18 -0
  16. agentic_testing-0.1.0/agent_test/src/fixture/mock_api/graphql_mock.py +18 -0
  17. agentic_testing-0.1.0/agent_test/src/fixture/mock_api/grpc_mock.py +18 -0
  18. agentic_testing-0.1.0/agent_test/src/fixture/mock_api/httpx_mock.py +19 -0
  19. agentic_testing-0.1.0/agent_test/src/fixture/mock_api/mock_api_factory.py +1 -0
  20. agentic_testing-0.1.0/agent_test/src/fixture/mock_api/mq_mock.py +18 -0
  21. agentic_testing-0.1.0/agent_test/src/fixture/mock_api/requests_mock.py +25 -0
  22. agentic_testing-0.1.0/agent_test/src/fixture/mock_api/sdk_mock.py +18 -0
  23. agentic_testing-0.1.0/agent_test/src/fixture/mock_api/soap_mock.py +18 -0
  24. agentic_testing-0.1.0/agent_test/src/fixture/mock_api/urllib_mock.py +20 -0
  25. agentic_testing-0.1.0/agent_test/src/fixture/mock_api/utils.py +0 -0
  26. agentic_testing-0.1.0/agent_test/src/fixture/mock_api/websockets_mock.py +17 -0
  27. agentic_testing-0.1.0/agent_test/test/agent_utils/models/test_agent_info.py +12 -0
  28. agentic_testing-0.1.0/agent_test/test/agent_utils/models/test_api_mock_type.py +6 -0
  29. agentic_testing-0.1.0/agent_test/test/agent_utils/models/test_global_metadata.py +11 -0
  30. agentic_testing-0.1.0/agent_test/test/agent_utils/test_remoterunnable_utils.py +23 -0
  31. agentic_testing-0.1.0/agent_test/test/common/test_agent_test_logger.py +13 -0
  32. agentic_testing-0.1.0/agent_test/test/fixture/test_fixture_class.py +14 -0
  33. agentic_testing-0.1.0/agentic_testing.egg-info/PKG-INFO +144 -0
  34. agentic_testing-0.1.0/agentic_testing.egg-info/SOURCES.txt +51 -0
  35. agentic_testing-0.1.0/agentic_testing.egg-info/dependency_links.txt +1 -0
  36. agentic_testing-0.1.0/agentic_testing.egg-info/requires.txt +27 -0
  37. agentic_testing-0.1.0/agentic_testing.egg-info/top_level.txt +3 -0
  38. agentic_testing-0.1.0/examples/__init__.py +0 -0
  39. agentic_testing-0.1.0/examples/common/api1/api_code.py +11 -0
  40. agentic_testing-0.1.0/examples/common/worker1/main.py +24 -0
  41. agentic_testing-0.1.0/examples/common/worker2/main.py +24 -0
  42. agentic_testing-0.1.0/examples/common/worker3/main.py +24 -0
  43. agentic_testing-0.1.0/examples/crewai/orchestrator/orchestrator_code.py +116 -0
  44. agentic_testing-0.1.0/examples/langgraph/prompt_agentic/asynchronous/orchestrator_code.py +143 -0
  45. agentic_testing-0.1.0/examples/langgraph/prompt_agentic/asynchronous/test_orchestrator.py +57 -0
  46. agentic_testing-0.1.0/examples/langgraph/prompt_agentic/synchronous/orchestrator_code.py +137 -0
  47. agentic_testing-0.1.0/examples/langgraph/prompt_agentic/synchronous/test_orchestrator.py +85 -0
  48. agentic_testing-0.1.0/examples/langgraph/simple_graph/asynchronous/orchestrator_code.py +99 -0
  49. agentic_testing-0.1.0/examples/langgraph/simple_graph/asynchronous/test_orchestrator.py +67 -0
  50. agentic_testing-0.1.0/examples/langgraph/simple_graph/synchronous/orchestrator_code.py +99 -0
  51. agentic_testing-0.1.0/examples/langgraph/simple_graph/synchronous/test_orchestrator.py +66 -0
  52. agentic_testing-0.1.0/pyproject.toml +50 -0
  53. 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
@@ -0,0 +1,3 @@
1
+ # agent_test package for agentic flow testing utilities
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,12 @@
1
+ from pydantic import BaseModel
2
+
3
+ class AgentInfo(BaseModel):
4
+ agent_name: str
5
+ agent_path: str
6
+ agent_type: str
7
+ agent_module_path: str = None
8
+
9
+ @property
10
+ def module_path(self):
11
+ """Getter for agent_module_path."""
12
+ return self.agent_path
@@ -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