ragaai-catalyst 2.1b0__py3-none-any.whl → 2.1b2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. ragaai_catalyst/__init__.py +1 -0
  2. ragaai_catalyst/dataset.py +1 -4
  3. ragaai_catalyst/evaluation.py +4 -5
  4. ragaai_catalyst/guard_executor.py +97 -0
  5. ragaai_catalyst/guardrails_manager.py +41 -15
  6. ragaai_catalyst/internal_api_completion.py +1 -1
  7. ragaai_catalyst/prompt_manager.py +7 -2
  8. ragaai_catalyst/ragaai_catalyst.py +1 -1
  9. ragaai_catalyst/synthetic_data_generation.py +7 -0
  10. ragaai_catalyst/tracers/__init__.py +1 -1
  11. ragaai_catalyst/tracers/agentic_tracing/__init__.py +3 -0
  12. ragaai_catalyst/tracers/agentic_tracing/agent_tracer.py +422 -0
  13. ragaai_catalyst/tracers/agentic_tracing/agentic_tracing.py +198 -0
  14. ragaai_catalyst/tracers/agentic_tracing/base.py +376 -0
  15. ragaai_catalyst/tracers/agentic_tracing/data_structure.py +248 -0
  16. ragaai_catalyst/tracers/agentic_tracing/examples/FinancialAnalysisSystem.ipynb +536 -0
  17. ragaai_catalyst/tracers/agentic_tracing/examples/GameActivityEventPlanner.ipynb +134 -0
  18. ragaai_catalyst/tracers/agentic_tracing/examples/TravelPlanner.ipynb +563 -0
  19. ragaai_catalyst/tracers/agentic_tracing/file_name_tracker.py +46 -0
  20. ragaai_catalyst/tracers/agentic_tracing/llm_tracer.py +808 -0
  21. ragaai_catalyst/tracers/agentic_tracing/network_tracer.py +286 -0
  22. ragaai_catalyst/tracers/agentic_tracing/sample.py +197 -0
  23. ragaai_catalyst/tracers/agentic_tracing/tool_tracer.py +247 -0
  24. ragaai_catalyst/tracers/agentic_tracing/unique_decorator.py +165 -0
  25. ragaai_catalyst/tracers/agentic_tracing/unique_decorator_test.py +172 -0
  26. ragaai_catalyst/tracers/agentic_tracing/upload_agentic_traces.py +187 -0
  27. ragaai_catalyst/tracers/agentic_tracing/upload_code.py +115 -0
  28. ragaai_catalyst/tracers/agentic_tracing/user_interaction_tracer.py +43 -0
  29. ragaai_catalyst/tracers/agentic_tracing/utils/__init__.py +3 -0
  30. ragaai_catalyst/tracers/agentic_tracing/utils/api_utils.py +18 -0
  31. ragaai_catalyst/tracers/agentic_tracing/utils/data_classes.py +61 -0
  32. ragaai_catalyst/tracers/agentic_tracing/utils/generic.py +32 -0
  33. ragaai_catalyst/tracers/agentic_tracing/utils/llm_utils.py +177 -0
  34. ragaai_catalyst/tracers/agentic_tracing/utils/model_costs.json +7823 -0
  35. ragaai_catalyst/tracers/agentic_tracing/utils/trace_utils.py +74 -0
  36. ragaai_catalyst/tracers/agentic_tracing/zip_list_of_unique_files.py +184 -0
  37. ragaai_catalyst/tracers/exporters/raga_exporter.py +1 -7
  38. ragaai_catalyst/tracers/tracer.py +30 -4
  39. ragaai_catalyst/tracers/upload_traces.py +127 -0
  40. ragaai_catalyst-2.1b2.dist-info/METADATA +43 -0
  41. ragaai_catalyst-2.1b2.dist-info/RECORD +56 -0
  42. {ragaai_catalyst-2.1b0.dist-info → ragaai_catalyst-2.1b2.dist-info}/WHEEL +1 -1
  43. ragaai_catalyst-2.1b0.dist-info/METADATA +0 -295
  44. ragaai_catalyst-2.1b0.dist-info/RECORD +0 -28
  45. {ragaai_catalyst-2.1b0.dist-info → ragaai_catalyst-2.1b2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,247 @@
1
+ import functools
2
+ import uuid
3
+ from datetime import datetime
4
+ import psutil
5
+ from typing import Optional, Any, Dict, List
6
+ from .unique_decorator import generate_unique_hash_simple
7
+ import contextvars
8
+ import asyncio
9
+ from .file_name_tracker import TrackName
10
+
11
+
12
+ class ToolTracerMixin:
13
+ def __init__(self, *args, **kwargs):
14
+ super().__init__(*args, **kwargs)
15
+ self.file_tracker = TrackName()
16
+ self.current_tool_name = contextvars.ContextVar("tool_name", default=None)
17
+ self.current_tool_id = contextvars.ContextVar("tool_id", default=None)
18
+ self.component_network_calls = {}
19
+ self.component_user_interaction = {}
20
+ self.gt = None
21
+
22
+
23
+ def trace_tool(self, name: str, tool_type: str = "generic", version: str = "1.0.0"):
24
+ def decorator(func):
25
+ # Add metadata attribute to the function
26
+ metadata = {
27
+ "name": name,
28
+ "tool_type": tool_type,
29
+ "version": version,
30
+ "is_active": True
31
+ }
32
+
33
+ # Check if the function is async
34
+ is_async = asyncio.iscoroutinefunction(func)
35
+
36
+ @self.file_tracker.trace_decorator
37
+ @functools.wraps(func)
38
+ async def async_wrapper(*args, **kwargs):
39
+ async_wrapper.metadata = metadata
40
+ self.gt = kwargs.get('gt', None) if kwargs else None
41
+ return await self._trace_tool_execution(
42
+ func, name, tool_type, version, *args, **kwargs
43
+ )
44
+
45
+ @self.file_tracker.trace_decorator
46
+ @functools.wraps(func)
47
+ def sync_wrapper(*args, **kwargs):
48
+ sync_wrapper.metadata = metadata
49
+ self.gt = kwargs.get('gt', None) if kwargs else None
50
+ return self._trace_sync_tool_execution(
51
+ func, name, tool_type, version, *args, **kwargs
52
+ )
53
+
54
+ wrapper = async_wrapper if is_async else sync_wrapper
55
+ wrapper.metadata = metadata
56
+ return wrapper
57
+
58
+ return decorator
59
+
60
+ def _trace_sync_tool_execution(self, func, name, tool_type, version, *args, **kwargs):
61
+ """Synchronous version of tool tracing"""
62
+ if not self.is_active:
63
+ return func(*args, **kwargs)
64
+
65
+ start_time = datetime.now().astimezone()
66
+ start_memory = psutil.Process().memory_info().rss
67
+ component_id = str(uuid.uuid4())
68
+ hash_id = generate_unique_hash_simple(func)
69
+
70
+ # Start tracking network calls for this component
71
+ self.start_component(component_id)
72
+
73
+ try:
74
+ # Execute the tool
75
+ result = func(*args, **kwargs)
76
+
77
+ # Calculate resource usage
78
+ end_time = datetime.now().astimezone()
79
+ end_memory = psutil.Process().memory_info().rss
80
+ memory_used = max(0, end_memory - start_memory)
81
+
82
+ # End tracking network calls for this component
83
+ self.end_component(component_id)
84
+
85
+ # Create tool component
86
+ tool_component = self.create_tool_component(
87
+ component_id=component_id,
88
+ hash_id=hash_id,
89
+ name=name,
90
+ tool_type=tool_type,
91
+ version=version,
92
+ memory_used=memory_used,
93
+ start_time=start_time,
94
+ end_time=end_time,
95
+ input_data=self._sanitize_input(args, kwargs),
96
+ output_data=self._sanitize_output(result)
97
+ )
98
+
99
+ self.add_component(tool_component)
100
+ return result
101
+
102
+ except Exception as e:
103
+ error_component = {
104
+ "code": 500,
105
+ "type": type(e).__name__,
106
+ "message": str(e),
107
+ "details": {}
108
+ }
109
+
110
+ # End tracking network calls for this component
111
+ self.end_component(component_id)
112
+
113
+ end_time = datetime.now().astimezone()
114
+
115
+ tool_component = self.create_tool_component(
116
+ component_id=component_id,
117
+ hash_id=hash_id,
118
+ name=name,
119
+ tool_type=tool_type,
120
+ version=version,
121
+ memory_used=0,
122
+ start_time=start_time,
123
+ end_time=end_time,
124
+ input_data=self._sanitize_input(args, kwargs),
125
+ output_data=None,
126
+ error=error_component
127
+ )
128
+
129
+ self.add_component(tool_component)
130
+ raise
131
+
132
+ async def _trace_tool_execution(self, func, name, tool_type, version, *args, **kwargs):
133
+ """Asynchronous version of tool tracing"""
134
+ if not self.is_active:
135
+ return await func(*args, **kwargs)
136
+
137
+ start_time = datetime.now().astimezone()
138
+ start_memory = psutil.Process().memory_info().rss
139
+ component_id = str(uuid.uuid4())
140
+ hash_id = generate_unique_hash_simple(func)
141
+
142
+ try:
143
+ # Execute the tool
144
+ result = await func(*args, **kwargs)
145
+
146
+ # Calculate resource usage
147
+ end_time = datetime.now().astimezone()
148
+ end_memory = psutil.Process().memory_info().rss
149
+ memory_used = max(0, end_memory - start_memory)
150
+
151
+ # Create tool component
152
+ tool_component = self.create_tool_component(
153
+ component_id=component_id,
154
+ hash_id=hash_id,
155
+ name=name,
156
+ tool_type=tool_type,
157
+ version=version,
158
+ start_time=start_time,
159
+ end_time=end_time,
160
+ memory_used=memory_used,
161
+ input_data=self._sanitize_input(args, kwargs),
162
+ output_data=self._sanitize_output(result)
163
+ )
164
+ self.add_component(tool_component)
165
+ return result
166
+
167
+ except Exception as e:
168
+ error_component = {
169
+ "code": 500,
170
+ "type": type(e).__name__,
171
+ "message": str(e),
172
+ "details": {}
173
+ }
174
+
175
+ end_time = datetime.now().astimezone()
176
+
177
+ tool_component = self.create_tool_component(
178
+ component_id=component_id,
179
+ hash_id=hash_id,
180
+ name=name,
181
+ tool_type=tool_type,
182
+ version=version,
183
+ start_time=start_time,
184
+ end_time=end_time,
185
+ memory_used=0,
186
+ input_data=self._sanitize_input(args, kwargs),
187
+ output_data=None,
188
+ error=error_component
189
+ )
190
+ self.add_component(tool_component)
191
+ raise
192
+
193
+ def create_tool_component(self, **kwargs):
194
+
195
+
196
+ """Create a tool component according to the data structure"""
197
+ start_time = kwargs["start_time"]
198
+ component = {
199
+ "id": kwargs["component_id"],
200
+ "hash_id": kwargs["hash_id"],
201
+ "source_hash_id": None,
202
+ "type": "tool",
203
+ "name": kwargs["name"],
204
+ "start_time": start_time.isoformat(),
205
+ "end_time": kwargs["end_time"].isoformat(),
206
+ "error": kwargs.get("error"),
207
+ "parent_id": self.current_agent_id.get(),
208
+ "info": {
209
+ "tool_type": kwargs["tool_type"],
210
+ "version": kwargs["version"],
211
+ "memory_used": kwargs["memory_used"]
212
+ },
213
+ "data": {
214
+ "input": kwargs["input_data"],
215
+ "output": kwargs["output_data"],
216
+ "memory_used": kwargs["memory_used"]
217
+ },
218
+ "network_calls": self.component_network_calls.get(kwargs["component_id"], []),
219
+ "interactions": self.component_user_interaction.get(kwargs["component_id"], [])
220
+ }
221
+
222
+ if self.gt:
223
+ component["data"]["gt"] = self.gt
224
+
225
+ return component
226
+
227
+ def start_component(self, component_id):
228
+ self.component_network_calls[component_id] = []
229
+
230
+ def end_component(self, component_id):
231
+ pass
232
+
233
+ def _sanitize_input(self, args: tuple, kwargs: dict) -> Dict:
234
+ """Sanitize and format input data"""
235
+ return {
236
+ "args": [str(arg) if not isinstance(arg, (int, float, bool, str, list, dict)) else arg for arg in args],
237
+ "kwargs": {
238
+ k: str(v) if not isinstance(v, (int, float, bool, str, list, dict)) else v
239
+ for k, v in kwargs.items()
240
+ }
241
+ }
242
+
243
+ def _sanitize_output(self, output: Any) -> Any:
244
+ """Sanitize and format output data"""
245
+ if isinstance(output, (int, float, bool, str, list, dict)):
246
+ return output
247
+ return str(output)
@@ -0,0 +1,165 @@
1
+ import hashlib
2
+ import inspect
3
+ import functools
4
+ import re
5
+ import tokenize
6
+ import io
7
+ import types
8
+
9
+ def normalize_source_code(source):
10
+ """
11
+ Advanced normalization of source code that:
12
+ 1. Preserves docstrings
13
+ 2. Removes comments
14
+ 3. Removes extra whitespace
15
+
16
+ Args:
17
+ source (str): Original source code
18
+
19
+ Returns:
20
+ str: Normalized source code
21
+ """
22
+ normalized_tokens = []
23
+
24
+ try:
25
+ token_source = io.StringIO(source).readline
26
+
27
+ for token_type, token_string, _, _, _ in tokenize.generate_tokens(token_source):
28
+ if token_type == tokenize.STRING:
29
+ normalized_tokens.append(token_string.strip())
30
+ elif token_type in [tokenize.NAME, tokenize.NUMBER, tokenize.OP]:
31
+ normalized_tokens.append(token_string.strip())
32
+
33
+ except tokenize.TokenError:
34
+ normalized_tokens = re.findall(r'\w+|[^\w\s]', source)
35
+
36
+ return ''.join(normalized_tokens)
37
+
38
+ def generate_unique_hash(func, *args, **kwargs):
39
+ """Generate a unique hash based on the original function and its arguments"""
40
+ if inspect.ismethod(func) or inspect.isfunction(func):
41
+ # Get function name and source code
42
+ func_name = func.__name__
43
+ try:
44
+ func_source = inspect.getsource(func)
45
+ normalized_source = normalize_source_code(func_source)
46
+ except (IOError, TypeError):
47
+ normalized_source = ""
48
+
49
+ # Normalize argument values
50
+ def normalize_arg(arg):
51
+ if isinstance(arg, (str, int, float, bool)):
52
+ return str(arg)
53
+ elif isinstance(arg, (list, tuple, set)):
54
+ return '_'.join(normalize_arg(x) for x in arg)
55
+ elif isinstance(arg, dict):
56
+ return '_'.join(f"{normalize_arg(k)}:{normalize_arg(v)}"
57
+ for k, v in sorted(arg.items()))
58
+ elif callable(arg):
59
+ return arg.__name__
60
+ else:
61
+ return str(type(arg).__name__)
62
+
63
+ # Create normalized strings of arguments
64
+ args_str = '_'.join(normalize_arg(arg) for arg in args)
65
+ kwargs_str = '_'.join(f"{k}:{normalize_arg(v)}"
66
+ for k, v in sorted(kwargs.items()))
67
+
68
+ # Combine all components
69
+ hash_input = f"{func_name}_{normalized_source}_{args_str}_{kwargs_str}"
70
+
71
+ elif inspect.isclass(func):
72
+ try:
73
+ class_source = inspect.getsource(func)
74
+ normalized_source = normalize_source_code(class_source)
75
+ hash_input = f"{func.__name__}_{normalized_source}"
76
+ except (IOError, TypeError):
77
+ hash_input = f"{func.__name__}_{str(func)}"
78
+
79
+ else:
80
+ hash_input = str(func)
81
+
82
+ hash_obj = hashlib.md5(hash_input.encode('utf-8'))
83
+ return hash_obj.hexdigest()
84
+
85
+ def generate_unique_hash_simple(func):
86
+ """Generate a unique hash based on the function name and normalized source code.
87
+ Works for both standalone functions and class methods (where self would be passed)."""
88
+ import hashlib
89
+ import inspect
90
+
91
+ # Handle bound methods (instance methods of classes)
92
+ if hasattr(func, '__self__'):
93
+ # Get the underlying function from the bound method
94
+ func = func.__func__
95
+
96
+
97
+ # Get function name
98
+ func_name = func.__name__
99
+
100
+ # Get and normalize source code based on type
101
+ try:
102
+ if isinstance(func, (types.FunctionType, types.MethodType)):
103
+ source = inspect.getsource(func)
104
+ # Remove whitespace and normalize line endings
105
+ normalized_source = "\n".join(line.strip() for line in source.splitlines())
106
+ elif inspect.isclass(func):
107
+ source = inspect.getsource(func)
108
+ normalized_source = "\n".join(line.strip() for line in source.splitlines())
109
+ else:
110
+ normalized_source = str(func)
111
+ except (IOError, TypeError):
112
+ normalized_source = str(func)
113
+
114
+ # Use fixed timestamp for reproducibility
115
+ timestamp = "2025-01-03T18:15:16+05:30"
116
+
117
+ # Combine components
118
+ hash_input = f"{func_name}_{normalized_source}_{timestamp}"
119
+
120
+ # Generate MD5 hash
121
+ hash_obj = hashlib.md5(hash_input.encode('utf-8'))
122
+ return hash_obj.hexdigest()
123
+
124
+ class UniqueIdentifier:
125
+ _instance = None
126
+ _hash_cache = {}
127
+
128
+ def __new__(cls, *args, **kwargs):
129
+ if cls._instance is None:
130
+ cls._instance = super().__new__(cls)
131
+ return cls._instance
132
+
133
+ def __init__(self, salt=None):
134
+ if not hasattr(self, 'salt'):
135
+ self.salt = salt
136
+
137
+ def __call__(self, obj):
138
+ if inspect.isclass(obj):
139
+ hash_id = generate_unique_hash(obj)
140
+ setattr(obj, 'hash_id', hash_id)
141
+ return obj
142
+
143
+ @functools.wraps(obj)
144
+ def wrapper(*args, **kwargs):
145
+ # Generate hash based on the original function and its arguments
146
+ if hasattr(args[0], 'original_func'): # Check if it's a wrapped LLM call
147
+ original_func = args[0].original_func
148
+ func_args = args[1:] # Skip the original_func argument
149
+ hash_id = generate_unique_hash(original_func, *func_args, **kwargs)
150
+ else:
151
+ hash_id = generate_unique_hash(obj, *args, **kwargs)
152
+
153
+ # Store hash_id on the wrapper function
154
+ wrapper.hash_id = hash_id
155
+
156
+ return obj(*args, **kwargs)
157
+
158
+ # Initialize hash_id
159
+ initial_hash = generate_unique_hash(obj)
160
+ wrapper.hash_id = initial_hash
161
+
162
+ return wrapper
163
+
164
+ # Create a single instance to be used across all mixins
165
+ mydecorator = UniqueIdentifier()
@@ -0,0 +1,172 @@
1
+ from unique_decorator import mydecorator
2
+ from unique_decorator import generate_unique_hash
3
+ import inspect
4
+
5
+ def print_test_case(case_num, description, expected_behavior, hash1, hash2=None):
6
+ print(f"\n{'='*100}")
7
+ print(f"Test Case #{case_num}: {description}")
8
+ print(f"Expected Behavior: {expected_behavior}")
9
+ print(f"{'='*100}")
10
+ if hash2 is not None:
11
+ print(f"Hash ID 1: {hash1}")
12
+ print(f"Hash ID 2: {hash2}")
13
+ print(f"Hash IDs are {'EQUAL' if hash1 == hash2 else 'DIFFERENT'} (Expected: {expected_behavior})")
14
+ else:
15
+ print(f"Hash ID: {hash1}")
16
+ print(f"{'='*100}\n")
17
+
18
+ # Test Case 1: Same function with different formatting
19
+ # Expected: Same hash_id
20
+ @mydecorator
21
+ def example_function():
22
+ x = 1
23
+ return x
24
+
25
+ hash1 = example_function.hash_id
26
+
27
+ @mydecorator
28
+ def example_function():
29
+ # This is a comment
30
+ x = 1 # Another comment
31
+ return x # More spacing
32
+
33
+ hash2 = example_function.hash_id
34
+
35
+ print_test_case(1,
36
+ "Same function with different formatting and comments",
37
+ "Hash IDs should be EQUAL",
38
+ hash1, hash2)
39
+
40
+ # Test Case 2: Function with parameters - different argument orders
41
+ # Expected: Same hash_id for same arguments in different order
42
+ @mydecorator
43
+ def function_with_params(a: int, b: int = 10):
44
+ return a + b
45
+
46
+ result1 = function_with_params(a=2, b=3)
47
+ hash1 = function_with_params.hash_id
48
+
49
+ result2 = function_with_params(b=3, a=2)
50
+ hash2 = function_with_params.hash_id
51
+
52
+ print_test_case(2,
53
+ "Same function call with different argument order (a=2, b=3 vs b=3, a=2)",
54
+ "Hash IDs should be EQUAL",
55
+ hash1, hash2)
56
+
57
+ # Test Case 3: Function with different default value
58
+ # Expected: Different hash_id
59
+ @mydecorator
60
+ def function_with_params(a: int, b: int = 5): # Different default value
61
+ return a + b
62
+
63
+ hash3 = function_with_params.hash_id
64
+
65
+ print_test_case(3,
66
+ "Same function name but different default parameter value",
67
+ "Hash IDs should be DIFFERENT",
68
+ hash2, hash3)
69
+
70
+ # Test Case 4: Class methods with different formatting
71
+ # Expected: Same hash_id
72
+ @mydecorator
73
+ class ExampleClass:
74
+ @mydecorator
75
+ def method1(self):
76
+ x = 1
77
+ return x
78
+
79
+ hash1 = ExampleClass().method1.hash_id
80
+
81
+ @mydecorator
82
+ class ExampleClass:
83
+ @mydecorator
84
+ def method1(self):
85
+ # Comment here
86
+ x = 1
87
+ return x
88
+
89
+ hash2 = ExampleClass().method1.hash_id
90
+
91
+ print_test_case(4,
92
+ "Class method with different formatting",
93
+ "Hash IDs should be EQUAL",
94
+ hash1, hash2)
95
+
96
+ # Test Case 5: Functions with different argument types but same content
97
+ # Expected: Same hash_id
98
+ @mydecorator
99
+ def complex_function(a: dict, b: list = [1, 2]):
100
+ return a, b
101
+
102
+ test_dict1 = {"a": 1, "b": 2}
103
+ test_dict2 = {"b": 2, "a": 1} # Same content, different order
104
+ test_list1 = [1, 2, 3]
105
+ test_list2 = [1, 2, 3] # Identical list
106
+
107
+ result1 = complex_function(test_dict1, test_list1)
108
+ hash1 = complex_function.hash_id
109
+
110
+ result2 = complex_function(test_dict2, test_list2)
111
+ hash2 = complex_function.hash_id
112
+
113
+ print_test_case(5,
114
+ "Complex function with same content in different order",
115
+ "Hash IDs should be EQUAL",
116
+ hash1, hash2)
117
+
118
+ # Test Case 6: Function with docstring - different formatting
119
+ # Expected: Same hash_id
120
+ @mydecorator
121
+ def documented_function(x: int):
122
+ """
123
+ This is a docstring.
124
+ It should be preserved in the hash.
125
+ """
126
+ # This is a comment that should be ignored
127
+ return x * 2 # This comment should also be ignored
128
+
129
+ hash1 = documented_function.hash_id
130
+
131
+ @mydecorator
132
+ def documented_function(x:int):
133
+ """
134
+ This is a docstring.
135
+ It should be preserved in the hash.
136
+ """
137
+ return x*2
138
+
139
+ hash2 = documented_function.hash_id
140
+
141
+ print_test_case(6,
142
+ "Function with docstring - different formatting",
143
+ "Hash IDs should be EQUAL",
144
+ hash1, hash2)
145
+
146
+ # Test Case 7: Different functions with same structure
147
+ # Expected: Different hash_id
148
+ @mydecorator
149
+ def function_a(x):
150
+ return x + 1
151
+
152
+ @mydecorator
153
+ def function_b(x):
154
+ return x + 1
155
+
156
+ print_test_case(7,
157
+ "Different function names with same implementation",
158
+ "Hash IDs should be DIFFERENT",
159
+ function_a.hash_id, function_b.hash_id)
160
+
161
+ # Test Case 8: Same function with different argument values
162
+ # Expected: Different hash_id
163
+ result1 = function_with_params(a=1, b=2)
164
+ hash1 = function_with_params.hash_id
165
+
166
+ result2 = function_with_params(a=3, b=4)
167
+ hash2 = function_with_params.hash_id
168
+
169
+ print_test_case(8,
170
+ "Same function with different argument values",
171
+ "Hash IDs should be DIFFERENT",
172
+ hash1, hash2)