contextforge-eval 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. context_forge/__init__.py +95 -0
  2. context_forge/core/__init__.py +55 -0
  3. context_forge/core/trace.py +369 -0
  4. context_forge/core/types.py +121 -0
  5. context_forge/evaluation.py +267 -0
  6. context_forge/exceptions.py +56 -0
  7. context_forge/graders/__init__.py +44 -0
  8. context_forge/graders/base.py +264 -0
  9. context_forge/graders/deterministic/__init__.py +11 -0
  10. context_forge/graders/deterministic/memory_corruption.py +130 -0
  11. context_forge/graders/hybrid.py +190 -0
  12. context_forge/graders/judges/__init__.py +11 -0
  13. context_forge/graders/judges/backends/__init__.py +9 -0
  14. context_forge/graders/judges/backends/ollama.py +173 -0
  15. context_forge/graders/judges/base.py +158 -0
  16. context_forge/graders/judges/memory_hygiene_judge.py +332 -0
  17. context_forge/graders/judges/models.py +113 -0
  18. context_forge/harness/__init__.py +43 -0
  19. context_forge/harness/user_simulator/__init__.py +70 -0
  20. context_forge/harness/user_simulator/adapters/__init__.py +13 -0
  21. context_forge/harness/user_simulator/adapters/base.py +67 -0
  22. context_forge/harness/user_simulator/adapters/crewai.py +100 -0
  23. context_forge/harness/user_simulator/adapters/langgraph.py +157 -0
  24. context_forge/harness/user_simulator/adapters/pydanticai.py +105 -0
  25. context_forge/harness/user_simulator/llm/__init__.py +5 -0
  26. context_forge/harness/user_simulator/llm/ollama.py +119 -0
  27. context_forge/harness/user_simulator/models.py +103 -0
  28. context_forge/harness/user_simulator/persona.py +154 -0
  29. context_forge/harness/user_simulator/runner.py +342 -0
  30. context_forge/harness/user_simulator/scenario.py +95 -0
  31. context_forge/harness/user_simulator/simulator.py +307 -0
  32. context_forge/instrumentation/__init__.py +23 -0
  33. context_forge/instrumentation/base.py +307 -0
  34. context_forge/instrumentation/instrumentors/__init__.py +17 -0
  35. context_forge/instrumentation/instrumentors/langchain.py +671 -0
  36. context_forge/instrumentation/instrumentors/langgraph.py +534 -0
  37. context_forge/instrumentation/tracer.py +588 -0
  38. context_forge/py.typed +0 -0
  39. contextforge_eval-0.1.0.dist-info/METADATA +420 -0
  40. contextforge_eval-0.1.0.dist-info/RECORD +43 -0
  41. contextforge_eval-0.1.0.dist-info/WHEEL +5 -0
  42. contextforge_eval-0.1.0.dist-info/licenses/LICENSE +201 -0
  43. contextforge_eval-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,95 @@
1
+ """ContextForge: Evaluation framework for context-aware, agentic AI systems."""
2
+
3
+ __version__ = "0.1.0"
4
+
5
+ # Simple evaluation API (Level 2)
6
+ from context_forge.evaluation import (
7
+ EvaluationResult,
8
+ evaluate_agent,
9
+ evaluate_trace,
10
+ )
11
+
12
+ # Core trace types
13
+ from context_forge.core import (
14
+ AgentInfo,
15
+ BaseStep,
16
+ FinalOutputStep,
17
+ InterruptStep,
18
+ LLMCallStep,
19
+ MemoryReadStep,
20
+ MemoryWriteStep,
21
+ ResourceImpact,
22
+ RetrievalResult,
23
+ RetrievalStep,
24
+ StateChangeStep,
25
+ StepType,
26
+ TaskInfo,
27
+ ToolCallStep,
28
+ TraceRun,
29
+ TraceStep,
30
+ UserInputStep,
31
+ )
32
+
33
+ # Instrumentation
34
+ from context_forge.instrumentation import (
35
+ BaseInstrumentor,
36
+ LangChainInstrumentor,
37
+ LangGraphInstrumentor,
38
+ RedactionConfig,
39
+ )
40
+ from context_forge.instrumentation.tracer import Tracer
41
+
42
+ # Simulation (user simulator)
43
+ from context_forge.harness.user_simulator import (
44
+ GenerativeScenario,
45
+ Goal,
46
+ LangGraphAdapter,
47
+ LLMUserSimulator,
48
+ Persona,
49
+ ScriptedScenario,
50
+ SimulationResult,
51
+ SimulationRunner,
52
+ SimulationState,
53
+ )
54
+
55
+ __all__ = [
56
+ "__version__",
57
+ # Simple evaluation API (Level 2)
58
+ "evaluate_agent",
59
+ "evaluate_trace",
60
+ "EvaluationResult",
61
+ # Core trace types
62
+ "StepType",
63
+ "AgentInfo",
64
+ "TaskInfo",
65
+ "ResourceImpact",
66
+ "RetrievalResult",
67
+ "BaseStep",
68
+ "LLMCallStep",
69
+ "ToolCallStep",
70
+ "RetrievalStep",
71
+ "MemoryReadStep",
72
+ "MemoryWriteStep",
73
+ "InterruptStep",
74
+ "StateChangeStep",
75
+ "UserInputStep",
76
+ "FinalOutputStep",
77
+ "TraceStep",
78
+ "TraceRun",
79
+ # Instrumentation
80
+ "Tracer",
81
+ "BaseInstrumentor",
82
+ "LangChainInstrumentor",
83
+ "LangGraphInstrumentor",
84
+ "RedactionConfig",
85
+ # Simulation
86
+ "SimulationRunner",
87
+ "SimulationState",
88
+ "SimulationResult",
89
+ "Persona",
90
+ "Goal",
91
+ "ScriptedScenario",
92
+ "GenerativeScenario",
93
+ "LLMUserSimulator",
94
+ "LangGraphAdapter",
95
+ ]
@@ -0,0 +1,55 @@
1
+ """Core types and trace models for ContextForge.
2
+
3
+ This module contains the canonical trace specification including:
4
+ - StepType enum for step discrimination
5
+ - AgentInfo, TaskInfo metadata models
6
+ - All step type models (LLMCallStep, ToolCallStep, etc.)
7
+ - TraceStep discriminated union
8
+ - TraceRun complete trace model
9
+ """
10
+
11
+ from context_forge.core.types import (
12
+ AgentInfo,
13
+ ResourceImpact,
14
+ RetrievalResult,
15
+ StepType,
16
+ TaskInfo,
17
+ )
18
+ from context_forge.core.trace import (
19
+ BaseStep,
20
+ FinalOutputStep,
21
+ InterruptStep,
22
+ LLMCallStep,
23
+ MemoryReadStep,
24
+ MemoryWriteStep,
25
+ RetrievalStep,
26
+ StateChangeStep,
27
+ ToolCallStep,
28
+ TraceRun,
29
+ TraceStep,
30
+ UserInputStep,
31
+ )
32
+
33
+ __all__ = [
34
+ # Enums
35
+ "StepType",
36
+ # Metadata models
37
+ "AgentInfo",
38
+ "TaskInfo",
39
+ "ResourceImpact",
40
+ "RetrievalResult",
41
+ # Step models
42
+ "BaseStep",
43
+ "LLMCallStep",
44
+ "ToolCallStep",
45
+ "RetrievalStep",
46
+ "MemoryReadStep",
47
+ "MemoryWriteStep",
48
+ "InterruptStep",
49
+ "StateChangeStep",
50
+ "UserInputStep",
51
+ "FinalOutputStep",
52
+ # Union and container
53
+ "TraceStep",
54
+ "TraceRun",
55
+ ]
@@ -0,0 +1,369 @@
1
+ """Trace models for ContextForge.
2
+
3
+ This module implements the canonical trace schema:
4
+ - T016: BaseStep model
5
+ - T017-T022: All step type models
6
+ - T023: TraceStep discriminated union
7
+ - T024: TraceRun model
8
+ """
9
+
10
+ from datetime import datetime
11
+ from typing import Annotated, Any, Literal, Optional, Union
12
+
13
+ from pydantic import BaseModel, ConfigDict, Field, model_validator
14
+
15
+ from context_forge.core.types import (
16
+ AgentInfo,
17
+ FieldChange,
18
+ ResourceImpact,
19
+ RetrievalResult,
20
+ StepType,
21
+ TaskInfo,
22
+ )
23
+
24
+
25
+ class BaseStep(BaseModel):
26
+ """Base fields shared by all step types.
27
+
28
+ All trace steps inherit these common fields for identification,
29
+ timing, and hierarchical organization.
30
+
31
+ Attributes:
32
+ step_id: Unique identifier for this step within the trace
33
+ timestamp: When this step occurred (ISO8601 with ms precision)
34
+ parent_step_id: Optional reference to parent step for nested calls
35
+ metadata: Optional additional metadata for this step
36
+ """
37
+
38
+ model_config = ConfigDict(
39
+ validate_by_alias=True,
40
+ extra="ignore",
41
+ )
42
+
43
+ step_id: str
44
+ timestamp: datetime
45
+ parent_step_id: Optional[str] = None
46
+ metadata: Optional[dict[str, Any]] = None
47
+
48
+
49
+ class LLMCallStep(BaseStep):
50
+ """An LLM invocation step.
51
+
52
+ Records a call to a language model including the prompt,
53
+ response, and resource usage.
54
+
55
+ Attributes:
56
+ step_type: Discriminator (always 'llm_call')
57
+ model: Model identifier (e.g., 'gpt-4', 'claude-3-opus')
58
+ input: Prompt text or list of messages
59
+ output: Model response text or structured output
60
+ tokens_in: Number of input/prompt tokens
61
+ tokens_out: Number of output/completion tokens
62
+ tokens_total: Total tokens (input + output)
63
+ latency_ms: Response latency in milliseconds
64
+ cost_estimate: Estimated cost in USD
65
+ provider: LLM provider name (openai, anthropic, etc.)
66
+ """
67
+
68
+ step_type: Literal[StepType.LLM_CALL] = StepType.LLM_CALL
69
+ model: str
70
+ input: str | list[dict[str, Any]]
71
+ output: str | dict[str, Any]
72
+ tokens_in: Optional[int] = None
73
+ tokens_out: Optional[int] = None
74
+ tokens_total: Optional[int] = None
75
+ latency_ms: Optional[int] = None
76
+ cost_estimate: Optional[float] = None
77
+ provider: Optional[str] = None
78
+
79
+
80
+ class ToolCallStep(BaseStep):
81
+ """A tool/function invocation step.
82
+
83
+ Records execution of a tool with its arguments and result.
84
+
85
+ Attributes:
86
+ step_type: Discriminator (always 'tool_call')
87
+ tool_name: Tool identifier
88
+ arguments: Arguments passed to the tool
89
+ result: Tool execution result
90
+ latency_ms: Execution time in milliseconds
91
+ success: Whether the tool call succeeded
92
+ error: Error message if the call failed
93
+ resource_impact: Optional cost/credit impact
94
+ """
95
+
96
+ step_type: Literal[StepType.TOOL_CALL] = StepType.TOOL_CALL
97
+ tool_name: str
98
+ arguments: dict[str, Any]
99
+ result: Optional[Any] = None
100
+ latency_ms: Optional[int] = None
101
+ success: Optional[bool] = None
102
+ error: Optional[str] = None
103
+ resource_impact: Optional[ResourceImpact] = None
104
+
105
+
106
+ class RetrievalStep(BaseStep):
107
+ """A retrieval/search step.
108
+
109
+ Records a query to a retrieval system (vector DB, search, etc.)
110
+ and its results.
111
+
112
+ Attributes:
113
+ step_type: Discriminator (always 'retrieval')
114
+ query: The search/retrieval query
115
+ results: List of retrieved documents/items
116
+ match_count: Number of matches returned
117
+ latency_ms: Query execution time in milliseconds
118
+ """
119
+
120
+ step_type: Literal[StepType.RETRIEVAL] = StepType.RETRIEVAL
121
+ query: str
122
+ results: list[RetrievalResult]
123
+ match_count: int
124
+ latency_ms: Optional[int] = None
125
+
126
+
127
+ class MemoryReadStep(BaseStep):
128
+ """A memory read operation step.
129
+
130
+ Records reading from agent memory (context, history, etc.).
131
+
132
+ Attributes:
133
+ step_type: Discriminator (always 'memory_read')
134
+ query: Memory query (string or structured)
135
+ results: Items retrieved from memory
136
+ match_count: Number of matches found
137
+ relevance_scores: Optional relevance scores for results
138
+ total_available: Total items available in memory
139
+ """
140
+
141
+ step_type: Literal[StepType.MEMORY_READ] = StepType.MEMORY_READ
142
+ query: str | dict[str, Any]
143
+ results: list[Any]
144
+ match_count: int
145
+ relevance_scores: Optional[list[float]] = None
146
+ total_available: Optional[int] = None
147
+
148
+
149
+ class MemoryWriteStep(BaseStep):
150
+ """A memory write operation step.
151
+
152
+ Records writing to agent memory with field-level change tracking
153
+ for model-agnostic hygiene grading.
154
+
155
+ Attributes:
156
+ step_type: Discriminator (always 'memory_write')
157
+ namespace: Storage namespace as list (e.g., ["profiles", "user_123"])
158
+ key: Storage key within the namespace
159
+ operation: Operation type (put, delete)
160
+ data: The complete data being written (for reference)
161
+ changes: Field-level changes with JSON paths (model-agnostic)
162
+ triggered_by_step_id: Step ID that triggered this write (for trace linking)
163
+ entity_type: Legacy field for backward compatibility
164
+ entity_id: Optional identifier for the entity
165
+ """
166
+
167
+ step_type: Literal[StepType.MEMORY_WRITE] = StepType.MEMORY_WRITE
168
+ namespace: Optional[list[str]] = None
169
+ key: Optional[str] = None
170
+ operation: Literal["add", "update", "delete", "put"]
171
+ data: dict[str, Any]
172
+ changes: Optional[list[FieldChange]] = None
173
+ triggered_by_step_id: Optional[str] = None
174
+ entity_type: Optional[str] = None # Legacy, derived from namespace if not set
175
+ entity_id: Optional[str] = None
176
+
177
+
178
+ class InterruptStep(BaseStep):
179
+ """A human-in-the-loop interrupt step.
180
+
181
+ Records when the agent pauses for human input.
182
+
183
+ Attributes:
184
+ step_type: Discriminator (always 'interrupt')
185
+ prompt: The prompt/question shown to the user
186
+ response: The user's response
187
+ wait_duration_ms: How long the agent waited for response
188
+ """
189
+
190
+ step_type: Literal[StepType.INTERRUPT] = StepType.INTERRUPT
191
+ prompt: str
192
+ response: str | dict[str, Any]
193
+ wait_duration_ms: int
194
+
195
+
196
+ class StateChangeStep(BaseStep):
197
+ """An agent state change step.
198
+
199
+ Records changes to internal agent state.
200
+
201
+ Attributes:
202
+ step_type: Discriminator (always 'state_change')
203
+ state_key: The state field that changed
204
+ old_value: Previous value (if available)
205
+ new_value: New value
206
+ reason: Optional reason for the change
207
+ """
208
+
209
+ step_type: Literal[StepType.STATE_CHANGE] = StepType.STATE_CHANGE
210
+ state_key: str
211
+ old_value: Optional[Any] = None
212
+ new_value: Any
213
+ reason: Optional[str] = None
214
+
215
+
216
+ class UserInputStep(BaseStep):
217
+ """A user input step.
218
+
219
+ Records input provided by the user to the agent.
220
+
221
+ Attributes:
222
+ step_type: Discriminator (always 'user_input')
223
+ content: The user's input content
224
+ input_type: Type of input (text, file, voice, etc.)
225
+ """
226
+
227
+ step_type: Literal[StepType.USER_INPUT] = StepType.USER_INPUT
228
+ content: str
229
+ input_type: Optional[str] = None
230
+
231
+
232
+ class FinalOutputStep(BaseStep):
233
+ """A final output step.
234
+
235
+ Records the agent's final response/output.
236
+
237
+ Attributes:
238
+ step_type: Discriminator (always 'final_output')
239
+ content: The final output content
240
+ format: Output format (text, json, markdown, etc.)
241
+ """
242
+
243
+ step_type: Literal[StepType.FINAL_OUTPUT] = StepType.FINAL_OUTPUT
244
+ content: Any
245
+ format: Optional[str] = None
246
+
247
+
248
+ # Discriminated union for all step types
249
+ TraceStep = Annotated[
250
+ Union[
251
+ LLMCallStep,
252
+ ToolCallStep,
253
+ RetrievalStep,
254
+ MemoryReadStep,
255
+ MemoryWriteStep,
256
+ InterruptStep,
257
+ StateChangeStep,
258
+ UserInputStep,
259
+ FinalOutputStep,
260
+ ],
261
+ Field(discriminator="step_type"),
262
+ ]
263
+
264
+
265
+ class TraceRun(BaseModel):
266
+ """Complete record of an agent execution run.
267
+
268
+ This is the top-level container for a trace, holding all steps
269
+ and metadata about the run.
270
+
271
+ Attributes:
272
+ run_id: Unique identifier for this run (UUID v4)
273
+ started_at: When the run started (ISO8601)
274
+ ended_at: When the run ended (None if still running)
275
+ agent_info: Metadata about the agent
276
+ task_info: Optional metadata about the task
277
+ steps: Ordered list of trace steps
278
+ metadata: Additional run-level metadata
279
+ """
280
+
281
+ model_config = ConfigDict(
282
+ validate_by_alias=True,
283
+ extra="ignore",
284
+ )
285
+
286
+ run_id: str
287
+ started_at: datetime
288
+ ended_at: Optional[datetime] = None
289
+ agent_info: AgentInfo
290
+ task_info: Optional[TaskInfo] = None
291
+ steps: list[TraceStep] = Field(default_factory=list)
292
+ metadata: Optional[dict[str, Any]] = None
293
+
294
+ @model_validator(mode="after")
295
+ def validate_steps(self) -> "TraceRun":
296
+ """Validate step constraints.
297
+
298
+ Checks:
299
+ - All step_ids are unique within the trace
300
+ - ended_at >= started_at if both are set
301
+ """
302
+ # Check unique step_ids
303
+ step_ids = [step.step_id for step in self.steps]
304
+ duplicates = [sid for sid in step_ids if step_ids.count(sid) > 1]
305
+ if duplicates:
306
+ raise ValueError(
307
+ f"Duplicate step_ids found: {list(set(duplicates))}. "
308
+ "Each step must have a unique step_id within a trace."
309
+ )
310
+
311
+ # Check ended_at >= started_at
312
+ if self.ended_at is not None and self.ended_at < self.started_at:
313
+ raise ValueError(
314
+ f"ended_at ({self.ended_at}) cannot be before started_at ({self.started_at})"
315
+ )
316
+
317
+ return self
318
+
319
+ def add_step(self, step: TraceStep) -> None:
320
+ """Add a step to the trace.
321
+
322
+ Args:
323
+ step: The step to add
324
+ """
325
+ self.steps.append(step)
326
+
327
+ def get_steps_by_type(self, step_type: StepType) -> list[TraceStep]:
328
+ """Get all steps of a specific type.
329
+
330
+ Args:
331
+ step_type: The type of steps to retrieve
332
+
333
+ Returns:
334
+ List of matching steps
335
+ """
336
+ return [s for s in self.steps if s.step_type == step_type]
337
+
338
+ def get_llm_calls(self) -> list[LLMCallStep]:
339
+ """Get all LLM call steps."""
340
+ return [s for s in self.steps if isinstance(s, LLMCallStep)]
341
+
342
+ def get_tool_calls(self) -> list[ToolCallStep]:
343
+ """Get all tool call steps."""
344
+ return [s for s in self.steps if isinstance(s, ToolCallStep)]
345
+
346
+ def total_tokens(self) -> int:
347
+ """Calculate total tokens used across all LLM calls."""
348
+ total = 0
349
+ for step in self.get_llm_calls():
350
+ if step.tokens_total is not None:
351
+ total += step.tokens_total
352
+ elif step.tokens_in is not None and step.tokens_out is not None:
353
+ total += step.tokens_in + step.tokens_out
354
+ return total
355
+
356
+ def total_tool_calls(self) -> int:
357
+ """Count total tool calls in the trace."""
358
+ return len(self.get_tool_calls())
359
+
360
+ def to_json(self, **kwargs) -> str:
361
+ """Serialize the trace to JSON.
362
+
363
+ Args:
364
+ **kwargs: Additional arguments passed to model_dump_json
365
+
366
+ Returns:
367
+ JSON string representation
368
+ """
369
+ return self.model_dump_json(exclude_none=True, **kwargs)
@@ -0,0 +1,121 @@
1
+ """Core types and metadata models for ContextForge traces.
2
+
3
+ This module implements:
4
+ - T013: StepType enum
5
+ - T014: AgentInfo and TaskInfo models
6
+ - T015: ResourceImpact and RetrievalResult models
7
+ """
8
+
9
+ from enum import Enum
10
+ from typing import Any, Optional
11
+
12
+ from pydantic import BaseModel, ConfigDict
13
+
14
+
15
+ class StepType(str, Enum):
16
+ """Discriminator for trace step types.
17
+
18
+ Each value represents a distinct operation that can occur during
19
+ an agent execution run.
20
+ """
21
+
22
+ USER_INPUT = "user_input"
23
+ LLM_CALL = "llm_call"
24
+ TOOL_CALL = "tool_call"
25
+ RETRIEVAL = "retrieval"
26
+ MEMORY_READ = "memory_read"
27
+ MEMORY_WRITE = "memory_write"
28
+ STATE_CHANGE = "state_change"
29
+ INTERRUPT = "interrupt"
30
+ FINAL_OUTPUT = "final_output"
31
+
32
+
33
+ class AgentInfo(BaseModel):
34
+ """Metadata about the agent being traced.
35
+
36
+ Attributes:
37
+ name: Agent name/identifier (required)
38
+ version: Agent version string
39
+ framework: Framework used (langchain, crewai, langgraph, custom)
40
+ framework_version: Version of the framework
41
+ """
42
+
43
+ model_config = ConfigDict(extra="ignore")
44
+
45
+ name: str
46
+ version: Optional[str] = None
47
+ framework: Optional[str] = None
48
+ framework_version: Optional[str] = None
49
+
50
+
51
+ class TaskInfo(BaseModel):
52
+ """Metadata about the task being executed.
53
+
54
+ Attributes:
55
+ description: Human-readable task description
56
+ goal: The objective or goal of the task
57
+ input: Task input data as a dictionary
58
+ """
59
+
60
+ model_config = ConfigDict(extra="ignore")
61
+
62
+ description: Optional[str] = None
63
+ goal: Optional[str] = None
64
+ input: Optional[dict[str, Any]] = None
65
+
66
+
67
+ class ResourceImpact(BaseModel):
68
+ """Cost or credit impact of a tool call.
69
+
70
+ Used to track resource consumption for tools that have
71
+ associated costs (API credits, compute units, etc.).
72
+
73
+ Attributes:
74
+ amount: Numeric amount of resource consumed
75
+ unit: Unit of measurement (credits, USD, compute_units, etc.)
76
+ breakdown: Optional detailed breakdown by category
77
+ """
78
+
79
+ model_config = ConfigDict(extra="ignore")
80
+
81
+ amount: float
82
+ unit: str
83
+ breakdown: Optional[dict[str, Any]] = None
84
+
85
+
86
+ class RetrievalResult(BaseModel):
87
+ """A single retrieved document or item.
88
+
89
+ Represents one result from a retrieval operation (vector search,
90
+ keyword search, etc.).
91
+
92
+ Attributes:
93
+ content: The retrieved content (text, document chunk, etc.)
94
+ score: Relevance score from 0.0 to 1.0 (higher is more relevant)
95
+ metadata: Additional metadata about the retrieved item
96
+ """
97
+
98
+ model_config = ConfigDict(extra="ignore")
99
+
100
+ content: str
101
+ score: Optional[float] = None
102
+ metadata: Optional[dict[str, Any]] = None
103
+
104
+
105
+ class FieldChange(BaseModel):
106
+ """A single field-level change in a memory write operation.
107
+
108
+ Model-agnostic representation of a field modification using JSON path
109
+ notation. Enables hygiene grading without domain knowledge.
110
+
111
+ Attributes:
112
+ path: JSON path to the changed field (e.g., "$.equipment.solar_capacity_kw")
113
+ old_value: Previous value (None if field didn't exist)
114
+ new_value: New value (None if field was deleted)
115
+ """
116
+
117
+ model_config = ConfigDict(extra="ignore")
118
+
119
+ path: str
120
+ old_value: Optional[Any] = None
121
+ new_value: Optional[Any] = None