fairo 25.9.1__tar.gz → 25.9.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of fairo might be problematic. Click here for more details.

Files changed (52) hide show
  1. {fairo-25.9.1 → fairo-25.9.3}/PKG-INFO +1 -1
  2. fairo-25.9.3/fairo/__init__.py +1 -0
  3. {fairo-25.9.1 → fairo-25.9.3}/fairo/core/chat/chat.py +2 -2
  4. {fairo-25.9.1 → fairo-25.9.3}/fairo/core/execution/executor.py +4 -1
  5. fairo-25.9.3/fairo/core/tools/__init__.py +2 -0
  6. fairo-25.9.3/fairo/core/tools/plot.py +229 -0
  7. fairo-25.9.3/fairo/core/tools/suggestion.py +43 -0
  8. fairo-25.9.3/fairo/core/utils.py +54 -0
  9. {fairo-25.9.1 → fairo-25.9.3}/fairo/core/workflow/dependency.py +0 -3
  10. {fairo-25.9.1 → fairo-25.9.3}/fairo.egg-info/PKG-INFO +1 -1
  11. {fairo-25.9.1 → fairo-25.9.3}/fairo.egg-info/SOURCES.txt +4 -0
  12. fairo-25.9.1/fairo/__init__.py +0 -1
  13. {fairo-25.9.1 → fairo-25.9.3}/README.md +0 -0
  14. {fairo-25.9.1 → fairo-25.9.3}/fairo/core/__init__.py +0 -0
  15. {fairo-25.9.1 → fairo-25.9.3}/fairo/core/agent/__init__.py +0 -0
  16. {fairo-25.9.1 → fairo-25.9.3}/fairo/core/agent/base_agent.py +0 -0
  17. {fairo-25.9.1 → fairo-25.9.3}/fairo/core/agent/code_analysis_agent.py +0 -0
  18. {fairo-25.9.1 → fairo-25.9.3}/fairo/core/agent/output/__init__.py +0 -0
  19. {fairo-25.9.1 → fairo-25.9.3}/fairo/core/agent/output/base_output.py +0 -0
  20. {fairo-25.9.1 → fairo-25.9.3}/fairo/core/agent/output/google_drive.py +0 -0
  21. {fairo-25.9.1 → fairo-25.9.3}/fairo/core/agent/tools/__init__.py +0 -0
  22. {fairo-25.9.1 → fairo-25.9.3}/fairo/core/agent/tools/base_tools.py +0 -0
  23. {fairo-25.9.1 → fairo-25.9.3}/fairo/core/agent/tools/code_analysis.py +0 -0
  24. {fairo-25.9.1 → fairo-25.9.3}/fairo/core/agent/tools/utils.py +0 -0
  25. {fairo-25.9.1 → fairo-25.9.3}/fairo/core/agent/utils.py +0 -0
  26. {fairo-25.9.1 → fairo-25.9.3}/fairo/core/chat/__init__.py +0 -0
  27. {fairo-25.9.1 → fairo-25.9.3}/fairo/core/client/__init__.py +0 -0
  28. {fairo-25.9.1 → fairo-25.9.3}/fairo/core/client/client.py +0 -0
  29. {fairo-25.9.1 → fairo-25.9.3}/fairo/core/exceptions.py +0 -0
  30. {fairo-25.9.1 → fairo-25.9.3}/fairo/core/execution/__init__.py +0 -0
  31. {fairo-25.9.1 → fairo-25.9.3}/fairo/core/execution/agent_serializer.py +0 -0
  32. {fairo-25.9.1 → fairo-25.9.3}/fairo/core/execution/env_finder.py +0 -0
  33. {fairo-25.9.1 → fairo-25.9.3}/fairo/core/execution/model_log_helper.py +0 -0
  34. {fairo-25.9.1 → fairo-25.9.3}/fairo/core/models/__init__.py +0 -0
  35. {fairo-25.9.1 → fairo-25.9.3}/fairo/core/models/custom_field_value.py +0 -0
  36. {fairo-25.9.1 → fairo-25.9.3}/fairo/core/models/resources.py +0 -0
  37. {fairo-25.9.1 → fairo-25.9.3}/fairo/core/runnable/__init__.py +0 -0
  38. {fairo-25.9.1 → fairo-25.9.3}/fairo/core/runnable/runnable.py +0 -0
  39. {fairo-25.9.1 → fairo-25.9.3}/fairo/core/workflow/__init__.py +0 -0
  40. {fairo-25.9.1 → fairo-25.9.3}/fairo/core/workflow/base_workflow.py +0 -0
  41. {fairo-25.9.1 → fairo-25.9.3}/fairo/core/workflow/utils.py +0 -0
  42. {fairo-25.9.1 → fairo-25.9.3}/fairo/metrics/__init__.py +0 -0
  43. {fairo-25.9.1 → fairo-25.9.3}/fairo/metrics/fairness_object.py +0 -0
  44. {fairo-25.9.1 → fairo-25.9.3}/fairo/metrics/metrics.py +0 -0
  45. {fairo-25.9.1 → fairo-25.9.3}/fairo/settings.py +0 -0
  46. {fairo-25.9.1 → fairo-25.9.3}/fairo/tests/__init__.py +0 -0
  47. {fairo-25.9.1 → fairo-25.9.3}/fairo/tests/test_metrics.py +0 -0
  48. {fairo-25.9.1 → fairo-25.9.3}/fairo.egg-info/dependency_links.txt +0 -0
  49. {fairo-25.9.1 → fairo-25.9.3}/fairo.egg-info/requires.txt +0 -0
  50. {fairo-25.9.1 → fairo-25.9.3}/fairo.egg-info/top_level.txt +0 -0
  51. {fairo-25.9.1 → fairo-25.9.3}/pyproject.toml +0 -0
  52. {fairo-25.9.1 → fairo-25.9.3}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fairo
3
- Version: 25.9.1
3
+ Version: 25.9.3
4
4
  Summary: SDK for interfacing with Fairo SaaS platform.
5
5
  Author-email: "Fairo Systems, Inc." <support@fairo.ai>
6
6
  License: Apache-2.0
@@ -0,0 +1 @@
1
+ __version__ = "25.9.3"
@@ -193,11 +193,11 @@ class ChatFairo(ChatMlflow):
193
193
 
194
194
  @property
195
195
  def _target_uri(self):
196
- return os.environ.get("MLFLOW_GATEWAY_URI", None)
196
+ return os.environ.get("MLFLOW_GATEWAY_URI", get_mlflow_gateway_uri())
197
197
 
198
198
  @property
199
199
  def _endpoint(self):
200
- return os.environ.get("MLFLOW_GATEWAY_ROUTE", None)
200
+ return os.environ.get("MLFLOW_GATEWAY_ROUTE", get_mlflow_gateway_chat_route())
201
201
 
202
202
  def invoke(self, *args, **kwargs):
203
203
  # Override invoke to use dynamic target_uri
@@ -18,7 +18,7 @@ from fairo.core.execution.model_log_helper import ModelLogHelper
18
18
  from fairo.core.runnable.runnable import Runnable
19
19
  from fairo.core.workflow.utils import output_langchain_process_graph
20
20
  from fairo.settings import get_fairo_api_key, get_fairo_api_secret, get_mlflow_experiment_name, get_mlflow_server, get_fairo_base_url
21
-
21
+ from fairo.core.tools import ChatSuggestions
22
22
 
23
23
  logger = logging.getLogger(__name__)
24
24
 
@@ -35,6 +35,7 @@ class FairoExecutor:
35
35
  chain_class = SimpleSequentialChain,
36
36
  input_fields: List[str] = [],
37
37
  input_schema: Optional[Type[BaseModel]] = None,
38
+ chat_suggestions: Optional[ChatSuggestions] = None
38
39
  ):
39
40
  if agents and runnable:
40
41
  raise ValueError("FairoExecutor cannot be initialized with both 'agents' and 'runnable'. Please provide only one.")
@@ -55,6 +56,7 @@ class FairoExecutor:
55
56
  password=get_fairo_api_secret(),
56
57
  username=get_fairo_api_key()
57
58
  )
59
+ self.chat_suggestions = chat_suggestions
58
60
  self.input_fields = input_fields
59
61
  # Inject shared attributes into agents
60
62
  for agent in self.agents:
@@ -121,6 +123,7 @@ class FairoExecutor:
121
123
  "process_graph": process_graph,
122
124
  "schema": self.input_schema.model_json_schema() if self.input_schema else None,
123
125
  "input_fields": list(self.input_schema.model_fields.keys()) if self.input_schema else self.input_fields,
126
+ "chat_suggestions": self.chat_suggestions.model_dump() if self.chat_suggestions else None,
124
127
  }
125
128
  if process_graph:
126
129
  mlflow.log_text(json.dumps(fairo_settings, ensure_ascii=False, indent=2), artifact_file="fairo_settings.txt")
@@ -0,0 +1,2 @@
1
+ from .plot import generate_plot
2
+ from .suggestion import send_chat_suggestions, ChatSuggestions, ChatSuggestion
@@ -0,0 +1,229 @@
1
+ from typing import Dict, List, Literal, Optional, Union
2
+ from pydantic import BaseModel, Field, ConfigDict
3
+ from enum import Enum
4
+ from langchain_core.tools import tool
5
+ import matplotlib
6
+ matplotlib.use("Agg") # headless
7
+ import matplotlib.pyplot as plt
8
+ import io
9
+ import base64
10
+
11
+ class ChartTypeEnum(str, Enum):
12
+ line = "line"
13
+ bar = "bar"
14
+ scatter = "scatter"
15
+ hist = "hist"
16
+ area = "area"
17
+
18
+ EXAMPLES = [
19
+ {
20
+ "chart_type": "line",
21
+ "data": {"month": ["Jan","Feb","Mar"], "revenue": [10, 12, 15]},
22
+ "x": "month",
23
+ "y": "revenue",
24
+ "title": "Revenue by Month"
25
+ },
26
+ {
27
+ "chart_type": "bar",
28
+ "data": {"cat": ["A","B"], "m1": [1,2], "m2":[2,1]},
29
+ "x": "cat",
30
+ "y": ["m1","m2"],
31
+ "title": "Grouped Bars"
32
+ }
33
+ ]
34
+
35
+ class PlotSpec(BaseModel):
36
+ """
37
+ Either provide a 'spec' for a chart OR provide 'python_code' to execute.
38
+
39
+ If you provide 'spec', set:
40
+ - chart_type: one of line|bar|scatter|hist|area
41
+ - data: list of dicts (table rows) OR {"x":[...], "y":[...]} arrays
42
+ - x: x key (for list-of-dicts) or leave None if arrays
43
+ - y: y key or list of keys for multi-series
44
+ - title, xlabel, ylabel: optional labels
45
+ - width, height in inches (optional)
46
+ If you provide 'python_code', it must create a 'fig' and 'ax' variable.
47
+ """
48
+ model_config = ConfigDict(extra="allow", json_schema_extra={"examples": EXAMPLES})
49
+
50
+ # Option A: high-level specification
51
+ chart_type: Optional[ChartTypeEnum] = Field(None, description="Chart type.")
52
+ data: Optional[Union[List[Dict], Dict[str, List]]] = Field(
53
+ None, description="Either list of dict rows OR dict of arrays."
54
+ )
55
+ x: Optional[Union[str, List[str]]] = Field(None, description="X column or key(s).")
56
+ y: Optional[Union[str, List[str]]] = Field(None, description="Y column or key(s).")
57
+ title: Optional[str] = None
58
+ xlabel: Optional[str] = None
59
+ ylabel: Optional[str] = None
60
+ width: Optional[float] = Field(6.0, description="Figure width in inches.")
61
+ height: Optional[float] = Field(4.0, description="Figure height in inches.")
62
+ legend: Optional[bool] = True
63
+ grid: Optional[bool] = True
64
+ alpha: Optional[float] = 1.0
65
+
66
+ # Option B: low-level custom code (SAFE-ish)
67
+ python_code: Optional[str] = Field(
68
+ None,
69
+ description=(
70
+ "Matplotlib code that defines 'fig, ax'. Avoid imports; "
71
+ "numpy is available as 'np' and matplotlib.pyplot as 'plt'."
72
+ ),
73
+ )
74
+
75
+
76
+ def _png_base64_from_fig(fig) -> str:
77
+ buf = io.BytesIO()
78
+ fig.savefig(buf, format="png", dpi=150, bbox_inches="tight")
79
+ buf.seek(0)
80
+ b64 = base64.b64encode(buf.read()).decode("utf-8")
81
+ buf.close()
82
+ return b64
83
+
84
+
85
+ def _plot_from_spec(spec: PlotSpec) -> str:
86
+ # Build figure
87
+ fig, ax = plt.subplots(figsize=(spec.width or 6.0, spec.height or 4.0))
88
+
89
+ # Normalize data
90
+ data = spec.data or {}
91
+ is_rows = isinstance(data, list)
92
+ is_arrays = isinstance(data, dict) and all(isinstance(v, list) for v in data.values())
93
+
94
+ def extract_series(xkey, ykey):
95
+ if is_rows:
96
+ xs = [row.get(xkey) for row in data]
97
+ ys = [row.get(ykey) for row in data]
98
+ elif is_arrays:
99
+ xs = data.get(xkey) if xkey else range(len(data.get(ykey, [])))
100
+ ys = data.get(ykey, [])
101
+ else:
102
+ raise ValueError("Unsupported data format. Provide list[dict] or dict of arrays.")
103
+ return xs, ys
104
+
105
+ # Handle y being single or multiple
106
+ y_keys = [spec.y] if isinstance(spec.y, (str, type(None))) else list(spec.y or [])
107
+ if not y_keys: # auto-choose 'y' if present
108
+ y_keys = ["y"] if (is_arrays and "y" in data) else []
109
+ if not y_keys:
110
+ raise ValueError("Could not infer 'y' series; please set y.")
111
+
112
+ # Plot by chart_type
113
+ if spec.chart_type == "line":
114
+ for ykey in y_keys:
115
+ xs, ys = extract_series(spec.x, ykey)
116
+ ax.plot(xs, ys, alpha=spec.alpha, label=ykey if len(y_keys) > 1 else None)
117
+ elif spec.chart_type == "bar":
118
+ # Simple grouped bar if multiple y
119
+ import numpy as np
120
+ xs, _ = extract_series(spec.x, y_keys[0])
121
+ x_index = np.arange(len(xs))
122
+ n = len(y_keys)
123
+ width = 0.8 / n
124
+ for i, ykey in enumerate(y_keys):
125
+ _, ys = extract_series(spec.x, ykey)
126
+ ax.bar(x_index + i * width, ys, width=width, alpha=spec.alpha, label=ykey)
127
+ ax.set_xticks(x_index + width * (n - 1) / 2)
128
+ ax.set_xticklabels(xs, rotation=0)
129
+ elif spec.chart_type == "scatter":
130
+ for ykey in y_keys:
131
+ xs, ys = extract_series(spec.x, ykey)
132
+ ax.scatter(xs, ys, alpha=spec.alpha, label=ykey if len(y_keys) > 1 else None)
133
+ elif spec.chart_type == "hist":
134
+ # Expect a single series in y
135
+ xs, ys = extract_series(spec.x, y_keys[0])
136
+ values = ys if ys else xs
137
+ ax.hist(values, alpha=spec.alpha)
138
+ elif spec.chart_type == "area":
139
+ for ykey in y_keys:
140
+ xs, ys = extract_series(spec.x, ykey)
141
+ ax.fill_between(xs, ys, alpha=spec.alpha, label=ykey if len(y_keys) > 1 else None)
142
+ else:
143
+ raise ValueError("Unsupported chart_type. Use line|bar|scatter|hist|area.")
144
+
145
+ # Labels & cosmetics
146
+ if spec.title:
147
+ ax.set_title(spec.title)
148
+ if spec.xlabel:
149
+ ax.set_xlabel(spec.xlabel)
150
+ if spec.ylabel:
151
+ ax.set_ylabel(spec.ylabel)
152
+ if spec.grid:
153
+ ax.grid(True, linestyle="--", linewidth=0.5, alpha=0.5)
154
+ if spec.legend and len(y_keys) > 1:
155
+ ax.legend()
156
+
157
+ # Return base64
158
+ b64 = _png_base64_from_fig(fig)
159
+ plt.close(fig)
160
+ return b64
161
+
162
+
163
+ def _plot_from_code(code: str) -> str:
164
+ """
165
+ Execute minimal matplotlib code that must define 'fig' and 'ax'.
166
+ VERY LIMITED namespace to reduce risk.
167
+ """
168
+ import numpy as np
169
+
170
+ allowed_globals = {
171
+ "__builtins__": {
172
+ "len": len, "range": range, "min": min, "max": max, "sum": sum, "abs": abs
173
+ },
174
+ "np": np,
175
+ "plt": plt,
176
+ }
177
+ local_vars = {}
178
+
179
+ # Simple guardrails
180
+ forbidden = ["import os", "import sys", "open(", "subprocess", "socket", "eval(", "exec("]
181
+ if any(tok in code for tok in forbidden):
182
+ raise ValueError("Disallowed token in python_code.")
183
+
184
+ exec(code, allowed_globals, local_vars) # noqa: S102 (intentional, guarded)
185
+ fig = local_vars.get("fig")
186
+ ax = local_vars.get("ax")
187
+ if fig is None or ax is None:
188
+ raise ValueError("Your python_code must create variables 'fig' and 'ax'.")
189
+ b64 = _png_base64_from_fig(fig)
190
+ plt.close(fig)
191
+ return b64
192
+
193
+
194
+ class PlotReturn(BaseModel):
195
+ mime_type: Literal["image/png"] = "image/png"
196
+ data_base64: str
197
+ alt_text: Optional[str] = None
198
+ debug: Optional[str] = None
199
+
200
+
201
+ @tool(args_schema=PlotSpec)
202
+ def generate_plot(**kwargs) -> str:
203
+ """
204
+ Generate a plot image (PNG base64). Returns a JSON string with keys:
205
+ - mime_type: 'image/png'
206
+ - data_base64: <base64 string>
207
+ - alt_text: optional
208
+ """
209
+ spec = PlotSpec(**kwargs)
210
+ try:
211
+ if spec.python_code:
212
+ b64 = _plot_from_code(spec.python_code)
213
+ alt = spec.title or "Custom matplotlib figure"
214
+ else:
215
+ b64 = _plot_from_spec(spec)
216
+ alt = spec.title or (
217
+ f"{spec.chart_type} plot" if spec.chart_type else "Plot"
218
+ )
219
+ result = PlotReturn(data_base64=b64, alt_text=alt)
220
+ # Return a stringified JSON so the LLM can pass it through easily
221
+ return f"""data:{result.mime_type};base64,{result.data_base64}"""
222
+ except Exception as e:
223
+ # Return an error payload the orchestrator can handle
224
+ err = PlotReturn(
225
+ data_base64="",
226
+ alt_text="Plot generation failed.",
227
+ debug=str(e),
228
+ )
229
+ return err.model_dump_json()
@@ -0,0 +1,43 @@
1
+ import json
2
+ from typing import List, Literal, Optional
3
+ from pydantic import BaseModel, Field
4
+ from langchain_core.tools import tool
5
+
6
+ class ChatSuggestion(BaseModel):
7
+ action: Optional[Literal["enable_chat"]] = Field(default=None, description="Custom actions available to be dispatched by the chat interface")
8
+ prompt: str = Field(..., description="Suggestion text to show in UI")
9
+ model_config = {
10
+ "extra": "ignore"
11
+ }
12
+
13
+ class ChatSuggestions(BaseModel):
14
+ chat_enabled: Optional[bool] = Field(default=True, description="This will let user only answer using the suggestions provided")
15
+ suggestions: List[ChatSuggestion] = Field(..., description="List of suggestions that will be available for the user")
16
+
17
+ @tool(args_schema=ChatSuggestions)
18
+ def send_chat_suggestions(chat_enabled: Optional[bool] = True,
19
+ suggestions: List[ChatSuggestion] = None):
20
+ """
21
+ This tool can be used to provide user predefined prompts and help during the user experience
22
+ Example input:
23
+ {
24
+ "suggestions": [
25
+ {
26
+ "action": null,
27
+ "prompt": "Suggestion 1 Prompt"
28
+ },
29
+ {
30
+ "action": null,
31
+ "prompt": "Suggestion 2 Prompt"
32
+ },
33
+ {
34
+ "action": "enable_chat",
35
+ "prompt": "Other"
36
+ }
37
+ ],
38
+ }
39
+ """
40
+ # No side-effects needed; return the same payload so it's accessible
41
+ if not suggestions:
42
+ return []
43
+ return {"chat_enabled": chat_enabled, "suggestions": [s.model_dump() for s in suggestions]}
@@ -0,0 +1,54 @@
1
+ import os
2
+ from langchain_core.messages import ToolMessage
3
+ from pathlib import Path
4
+
5
+ def parse_chat_interface_output(agent_executor_result):
6
+ """
7
+ Parses agent executor result into chat interface response
8
+ return_intermediate_steps must be set as true on the AgentExecutor in order to properly parse plot and suggestions
9
+ """
10
+ messages = [{"role": "assistant", "content": [
11
+ {
12
+ "type": "text",
13
+ "text": agent_executor_result["output"]
14
+ }
15
+ ]}]
16
+ suggestions = []
17
+ intermediate_steps = agent_executor_result.get('intermediate_steps', [])
18
+ for step, output in intermediate_steps:
19
+ if step.tool == "generate_plot":
20
+ messages.append({"role": "assistant", "content": [
21
+ {
22
+ "type": "image",
23
+ "image": output
24
+ }
25
+ ]})
26
+ if step.tool == "send_chat_suggestions":
27
+ suggestions = output
28
+
29
+ # Check if some tool message has artifact and raw_html attribute
30
+ artifact = None
31
+ is_tool_msg = isinstance(output, ToolMessage)
32
+ if is_tool_msg:
33
+ artifact = getattr(output, "artifact", None)
34
+ if artifact is None:
35
+ artifact = getattr(output, "additional_kwargs", {}).get("artifact")
36
+ if artifact:
37
+ artifact_id = artifact.get("artifact_id")
38
+ if artifact_id:
39
+ base_dir = Path("/tmp") if Path("/tmp").exists() else Path.cwd()
40
+ artifact_path = base_dir / f"{artifact_id}.html"
41
+ messages.append({
42
+ "role": "assistant",
43
+ "content": [{
44
+ "type": "file",
45
+ "mimeType": "text/html",
46
+ "data": artifact_path.read_text(encoding="utf-8")
47
+ }]
48
+ })
49
+ if os.path.exists(artifact_path):
50
+ os.remove(artifact_path)
51
+ return {
52
+ "messages": messages,
53
+ "suggestions": suggestions
54
+ }
@@ -122,9 +122,6 @@ class FairoVectorStore(BaseVectorStore):
122
122
  # Convert documents to Fairo format
123
123
  docs_data = []
124
124
  for doc in documents:
125
- # Generate embeddings for the document content
126
- embedding = self.embeddings.embed_query(doc.page_content)
127
-
128
125
  # Create doc entry (let Fairo API generate document IDs)
129
126
  doc_entry = {
130
127
  "page_content": doc.page_content,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fairo
3
- Version: 25.9.1
3
+ Version: 25.9.3
4
4
  Summary: SDK for interfacing with Fairo SaaS platform.
5
5
  Author-email: "Fairo Systems, Inc." <support@fairo.ai>
6
6
  License: Apache-2.0
@@ -9,6 +9,7 @@ fairo.egg-info/requires.txt
9
9
  fairo.egg-info/top_level.txt
10
10
  fairo/core/__init__.py
11
11
  fairo/core/exceptions.py
12
+ fairo/core/utils.py
12
13
  fairo/core/agent/__init__.py
13
14
  fairo/core/agent/base_agent.py
14
15
  fairo/core/agent/code_analysis_agent.py
@@ -34,6 +35,9 @@ fairo/core/models/custom_field_value.py
34
35
  fairo/core/models/resources.py
35
36
  fairo/core/runnable/__init__.py
36
37
  fairo/core/runnable/runnable.py
38
+ fairo/core/tools/__init__.py
39
+ fairo/core/tools/plot.py
40
+ fairo/core/tools/suggestion.py
37
41
  fairo/core/workflow/__init__.py
38
42
  fairo/core/workflow/base_workflow.py
39
43
  fairo/core/workflow/dependency.py
@@ -1 +0,0 @@
1
- __version__ = "25.9.1"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes