tinybird 0.0.1.dev286__py3-none-any.whl → 0.0.1.dev288__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.
Potentially problematic release.
This version of tinybird might be problematic. Click here for more details.
- tinybird/datafile/common.py +1 -1
- tinybird/service_datasources.py +911 -0
- tinybird/tb/__cli__.py +2 -2
- tinybird/tb/modules/agent/agent.py +27 -5
- tinybird/tb/modules/agent/explore_agent.py +5 -0
- tinybird/tb/modules/agent/prompts.py +61 -11
- tinybird/tb/modules/agent/tools/append.py +4 -2
- tinybird/tb/modules/agent/tools/datafile.py +6 -4
- tinybird/tb/modules/agent/tools/mock.py +3 -2
- tinybird/tb/modules/agent/tools/plan.py +62 -5
- tinybird/tb/modules/agent/tools/secret.py +2 -1
- tinybird/tb/modules/agent/utils.py +5 -3
- tinybird/tb/modules/deployment_common.py +5 -1
- {tinybird-0.0.1.dev286.dist-info → tinybird-0.0.1.dev288.dist-info}/METADATA +1 -1
- {tinybird-0.0.1.dev286.dist-info → tinybird-0.0.1.dev288.dist-info}/RECORD +18 -17
- {tinybird-0.0.1.dev286.dist-info → tinybird-0.0.1.dev288.dist-info}/WHEEL +0 -0
- {tinybird-0.0.1.dev286.dist-info → tinybird-0.0.1.dev288.dist-info}/entry_points.txt +0 -0
- {tinybird-0.0.1.dev286.dist-info → tinybird-0.0.1.dev288.dist-info}/top_level.txt +0 -0
tinybird/tb/__cli__.py
CHANGED
|
@@ -4,5 +4,5 @@ __description__ = 'Tinybird Command Line Tool'
|
|
|
4
4
|
__url__ = 'https://www.tinybird.co/docs/forward/commands'
|
|
5
5
|
__author__ = 'Tinybird'
|
|
6
6
|
__author_email__ = 'support@tinybird.co'
|
|
7
|
-
__version__ = '0.0.1.
|
|
8
|
-
__revision__ = '
|
|
7
|
+
__version__ = '0.0.1.dev288'
|
|
8
|
+
__revision__ = '6cd5b6c'
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
+
import hashlib
|
|
2
3
|
import shlex
|
|
3
4
|
import subprocess
|
|
4
5
|
import sys
|
|
@@ -35,6 +36,7 @@ from tinybird.tb.modules.agent.prompts import (
|
|
|
35
36
|
load_custom_project_rules,
|
|
36
37
|
resources_prompt,
|
|
37
38
|
secrets_prompt,
|
|
39
|
+
service_datasources_prompt,
|
|
38
40
|
)
|
|
39
41
|
from tinybird.tb.modules.agent.testing_agent import TestingAgent
|
|
40
42
|
from tinybird.tb.modules.agent.tools.analyze import analyze_file, analyze_url
|
|
@@ -46,7 +48,7 @@ from tinybird.tb.modules.agent.tools.deploy_check import deploy_check
|
|
|
46
48
|
from tinybird.tb.modules.agent.tools.diff_resource import diff_resource
|
|
47
49
|
from tinybird.tb.modules.agent.tools.get_endpoint_stats import get_endpoint_stats
|
|
48
50
|
from tinybird.tb.modules.agent.tools.get_openapi_definition import get_openapi_definition
|
|
49
|
-
from tinybird.tb.modules.agent.tools.plan import plan
|
|
51
|
+
from tinybird.tb.modules.agent.tools.plan import complete_plan, plan
|
|
50
52
|
from tinybird.tb.modules.agent.tools.secret import create_or_update_secrets
|
|
51
53
|
from tinybird.tb.modules.agent.utils import AgentRunCancelled, TinybirdAgentContext, show_confirmation, show_input
|
|
52
54
|
from tinybird.tb.modules.build_common import process as build_process
|
|
@@ -88,6 +90,7 @@ class TinybirdAgent:
|
|
|
88
90
|
self.dangerously_skip_permissions = dangerously_skip_permissions or prompt_mode
|
|
89
91
|
self.project = project
|
|
90
92
|
self.thinking_animation = ThinkingAnimation()
|
|
93
|
+
self.confirmed_plan_id: Optional[str] = None
|
|
91
94
|
if prompt_mode:
|
|
92
95
|
self.messages: list[ModelMessage] = get_last_messages_from_last_user_prompt()
|
|
93
96
|
else:
|
|
@@ -108,6 +111,7 @@ class TinybirdAgent:
|
|
|
108
111
|
takes_ctx=True,
|
|
109
112
|
),
|
|
110
113
|
Tool(plan, docstring_format="google", require_parameter_descriptions=True, takes_ctx=True),
|
|
114
|
+
Tool(complete_plan, docstring_format="google", require_parameter_descriptions=True, takes_ctx=True),
|
|
111
115
|
Tool(build, docstring_format="google", require_parameter_descriptions=True, takes_ctx=True),
|
|
112
116
|
Tool(deploy, docstring_format="google", require_parameter_descriptions=True, takes_ctx=True),
|
|
113
117
|
Tool(deploy_check, docstring_format="google", require_parameter_descriptions=True, takes_ctx=True),
|
|
@@ -285,6 +289,10 @@ class TinybirdAgent:
|
|
|
285
289
|
def get_project_files(ctx: RunContext[TinybirdAgentContext]) -> str:
|
|
286
290
|
return resources_prompt(self.project)
|
|
287
291
|
|
|
292
|
+
@self.agent.instructions
|
|
293
|
+
def get_service_datasources(ctx: RunContext[TinybirdAgentContext]) -> str:
|
|
294
|
+
return service_datasources_prompt()
|
|
295
|
+
|
|
288
296
|
@self.agent.instructions
|
|
289
297
|
def get_secrets(ctx: RunContext[TinybirdAgentContext]) -> str:
|
|
290
298
|
return secrets_prompt(self.project)
|
|
@@ -292,15 +300,25 @@ class TinybirdAgent:
|
|
|
292
300
|
def add_message(self, message: ModelMessage) -> None:
|
|
293
301
|
self.messages.append(message)
|
|
294
302
|
|
|
303
|
+
def start_plan(self, plan) -> str:
|
|
304
|
+
self.confirmed_plan_id = hashlib.sha256(plan.encode()).hexdigest()[:16]
|
|
305
|
+
return self.confirmed_plan_id
|
|
306
|
+
|
|
307
|
+
def cancel_plan(self) -> Optional[str]:
|
|
308
|
+
plan_id = self.confirmed_plan_id
|
|
309
|
+
self.confirmed_plan_id = None
|
|
310
|
+
return plan_id
|
|
311
|
+
|
|
312
|
+
def get_plan(self) -> Optional[str]:
|
|
313
|
+
return self.confirmed_plan_id
|
|
314
|
+
|
|
295
315
|
def _build_agent_deps(self, config: dict[str, Any], run_id: Optional[str] = None) -> TinybirdAgentContext:
|
|
296
|
-
client = TinyB(token=self.token, host=self.host)
|
|
297
316
|
project = self.project
|
|
298
317
|
folder = self.project.folder
|
|
299
318
|
local_client = get_tinybird_local_client(config, test=False, silent=False)
|
|
300
319
|
test_client = get_tinybird_local_client(config, test=True, silent=True)
|
|
301
320
|
return TinybirdAgentContext(
|
|
302
321
|
# context does not support the whole client, so we need to pass only the functions we need
|
|
303
|
-
explore_data=client.explore_data,
|
|
304
322
|
build_project=partial(build_project, project=project, config=config),
|
|
305
323
|
deploy_project=partial(deploy_project, project=project, config=config),
|
|
306
324
|
deploy_check_project=partial(deploy_check_project, project=project, config=config),
|
|
@@ -333,6 +351,9 @@ class TinybirdAgent:
|
|
|
333
351
|
local_host=local_client.host,
|
|
334
352
|
local_token=local_client.token,
|
|
335
353
|
run_id=run_id,
|
|
354
|
+
get_plan=self.get_plan,
|
|
355
|
+
start_plan=self.start_plan,
|
|
356
|
+
cancel_plan=self.cancel_plan,
|
|
336
357
|
)
|
|
337
358
|
|
|
338
359
|
def run(self, user_prompt: str, config: dict[str, Any]) -> None:
|
|
@@ -386,8 +407,8 @@ class TinybirdAgent:
|
|
|
386
407
|
ai_credits_limits = limits_data.get("limits", {}).get("ai_credits", {})
|
|
387
408
|
current_ai_credits = ai_credits_limits.get("quantity") or 0
|
|
388
409
|
ai_credits = ai_credits_limits.get("max") or 0
|
|
389
|
-
remaining_credits = max(ai_credits - current_ai_credits, 0)
|
|
390
|
-
current_ai_credits = min(ai_credits, current_ai_credits)
|
|
410
|
+
remaining_credits = round(max(ai_credits - current_ai_credits, 0), 2)
|
|
411
|
+
current_ai_credits = round(min(ai_credits, current_ai_credits), 2)
|
|
391
412
|
if not ai_credits:
|
|
392
413
|
return
|
|
393
414
|
warning_threshold = ai_credits * 0.8
|
|
@@ -566,6 +587,7 @@ def run_agent(
|
|
|
566
587
|
]
|
|
567
588
|
)
|
|
568
589
|
)
|
|
590
|
+
agent.cancel_plan()
|
|
569
591
|
continue
|
|
570
592
|
except KeyboardInterrupt:
|
|
571
593
|
click.echo(FeedbackManager.info(message="Goodbye!"))
|
|
@@ -10,6 +10,7 @@ from tinybird.tb.modules.agent.models import create_model
|
|
|
10
10
|
from tinybird.tb.modules.agent.prompts import (
|
|
11
11
|
explore_data_instructions,
|
|
12
12
|
resources_prompt,
|
|
13
|
+
service_datasources_prompt,
|
|
13
14
|
tone_and_style_instructions,
|
|
14
15
|
)
|
|
15
16
|
from tinybird.tb.modules.agent.tools.diff_resource import diff_resource
|
|
@@ -75,6 +76,10 @@ Once you finish the task, return a valid response for the task to complete.
|
|
|
75
76
|
def get_project_files(ctx: RunContext[TinybirdAgentContext]) -> str:
|
|
76
77
|
return resources_prompt(self.project)
|
|
77
78
|
|
|
79
|
+
@self.agent.instructions
|
|
80
|
+
def get_service_datasources(ctx: RunContext[TinybirdAgentContext]) -> str:
|
|
81
|
+
return service_datasources_prompt()
|
|
82
|
+
|
|
78
83
|
def run(self, task: str, deps: TinybirdAgentContext, usage: Usage):
|
|
79
84
|
result = self.agent.run_sync(
|
|
80
85
|
task,
|
|
@@ -12,6 +12,7 @@ from tinybird.prompts import (
|
|
|
12
12
|
pipe_instructions,
|
|
13
13
|
sink_pipe_instructions,
|
|
14
14
|
)
|
|
15
|
+
from tinybird.service_datasources import get_organization_service_datasources, get_tinybird_service_datasources
|
|
15
16
|
from tinybird.tb.modules.project import Project
|
|
16
17
|
|
|
17
18
|
available_commands = [
|
|
@@ -118,11 +119,6 @@ sql_instructions = """
|
|
|
118
119
|
- When you use defined function with a paremeter inside, do NOT add quotes around the parameter:
|
|
119
120
|
<invalid_defined_function_with_parameter>{% if defined('my_param') %}</invalid_defined_function_with_parameter>
|
|
120
121
|
<valid_defined_function_without_parameter>{% if defined(my_param) %}</valid_defined_function_without_parameter>
|
|
121
|
-
- Use datasource names as table names when doing SELECT statements.
|
|
122
|
-
- Do not use pipe names as table names.
|
|
123
|
-
- The available datasource names to use in the SQL are the ones present in the existing_resources section or the ones you will create.
|
|
124
|
-
- Use node names as table names only when nodes are present in the same file.
|
|
125
|
-
- Do not reference the current node name in the SQL.
|
|
126
122
|
- SQL queries only accept SELECT statements with conditions, aggregations, joins, etc.
|
|
127
123
|
- ONLY SELECT statements are allowed in any sql query.
|
|
128
124
|
- When using functions try always ClickHouse functions first, then SQL functions.
|
|
@@ -133,7 +129,7 @@ sql_instructions = """
|
|
|
133
129
|
datafile_instructions = """
|
|
134
130
|
<datafile_instructions>
|
|
135
131
|
- Endpoint files will be created under the `/endpoints` folder.
|
|
136
|
-
- Materialized pipe files will be created under the `/
|
|
132
|
+
- Materialized pipe files will be created under the `/materializations` folder.
|
|
137
133
|
- Sink pipe files will be created under the `/sinks` folder.
|
|
138
134
|
- Copy pipe files will be created under the `/copies` folder.
|
|
139
135
|
- Connection files will be created under the `/connections` folder.
|
|
@@ -184,7 +180,7 @@ def resources_prompt(project: Project) -> str:
|
|
|
184
180
|
"content": file_path.read_text(),
|
|
185
181
|
}
|
|
186
182
|
resources.append(resource)
|
|
187
|
-
resources_content
|
|
183
|
+
resources_content += format_as_xml(resources, root_tag="resources", item_tag="resource")
|
|
188
184
|
else:
|
|
189
185
|
resources_content += "No resources found"
|
|
190
186
|
|
|
@@ -198,7 +194,7 @@ def resources_prompt(project: Project) -> str:
|
|
|
198
194
|
"name": file_path.stem,
|
|
199
195
|
}
|
|
200
196
|
fixtures.append(fixture)
|
|
201
|
-
fixture_content
|
|
197
|
+
fixture_content += format_as_xml(fixtures, root_tag="fixtures", item_tag="fixture")
|
|
202
198
|
|
|
203
199
|
else:
|
|
204
200
|
fixture_content += "No fixture files found"
|
|
@@ -206,6 +202,50 @@ def resources_prompt(project: Project) -> str:
|
|
|
206
202
|
return resources_content + "\n" + fixture_content
|
|
207
203
|
|
|
208
204
|
|
|
205
|
+
def service_datasources_prompt() -> str:
|
|
206
|
+
def build_content(ds: dict[str, Any]) -> str:
|
|
207
|
+
content = "DESCRIPTION >\n"
|
|
208
|
+
content += f" {ds.get('description', 'No description')}\n"
|
|
209
|
+
|
|
210
|
+
content += "SCHEMA >\n"
|
|
211
|
+
for column in ds.get("columns", []):
|
|
212
|
+
content += f" `{column.get('name', '')}` {column.get('type', '')}\n"
|
|
213
|
+
|
|
214
|
+
if engine := ds.get("engine", {}).get("engine", ""):
|
|
215
|
+
content += f"ENGINE {engine}\n"
|
|
216
|
+
if sorting_key := ds.get("engine", {}).get("sorting_key", ""):
|
|
217
|
+
content += f"ENGINE_SORTING_KEY {sorting_key}\n"
|
|
218
|
+
if partition_key := ds.get("engine", {}).get("partition_key", ""):
|
|
219
|
+
content += f"ENGINE_PARTITION_KEY {partition_key}\n"
|
|
220
|
+
|
|
221
|
+
return content
|
|
222
|
+
|
|
223
|
+
skip_datasources = ["tinybird.bi_stats", "tinybird.bi_stats_rt", "tinybird.releases_log", "tinybird.hook_log"]
|
|
224
|
+
service_datasources = [
|
|
225
|
+
{"name": ds["name"], "content": build_content(ds)}
|
|
226
|
+
for ds in get_tinybird_service_datasources()
|
|
227
|
+
if ds["name"] not in skip_datasources
|
|
228
|
+
]
|
|
229
|
+
content = "# Service datasources:\n"
|
|
230
|
+
content += format_as_xml(
|
|
231
|
+
service_datasources, root_tag="workspace_service_datasources", item_tag="service_datasource"
|
|
232
|
+
)
|
|
233
|
+
content += "\n#Organization service datasources:\n"
|
|
234
|
+
skip_datasources = ["organization.bi_stats", "organization.bi_stats_rt"]
|
|
235
|
+
org_service_datasources = [
|
|
236
|
+
{"name": ds["name"], "content": build_content(ds)}
|
|
237
|
+
for ds in get_organization_service_datasources()
|
|
238
|
+
if ds["name"] not in skip_datasources
|
|
239
|
+
]
|
|
240
|
+
content += format_as_xml(
|
|
241
|
+
org_service_datasources,
|
|
242
|
+
root_tag="organization_service_datasources",
|
|
243
|
+
item_tag="service_datasource",
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
return content
|
|
247
|
+
|
|
248
|
+
|
|
209
249
|
def secrets_prompt(project: Project) -> str:
|
|
210
250
|
"""Generate a prompt showing available secrets from .env.local file."""
|
|
211
251
|
secrets = project.get_secrets()
|
|
@@ -224,7 +264,7 @@ def secrets_prompt(project: Project) -> str:
|
|
|
224
264
|
secrets_list.append(secret)
|
|
225
265
|
|
|
226
266
|
if secrets_list:
|
|
227
|
-
secrets_content
|
|
267
|
+
secrets_content += format_as_xml(secrets_list, root_tag="secrets", item_tag="secret")
|
|
228
268
|
|
|
229
269
|
return secrets_content
|
|
230
270
|
|
|
@@ -245,7 +285,7 @@ def tests_files_prompt(project: Project) -> str:
|
|
|
245
285
|
"content": file_path.read_text(),
|
|
246
286
|
}
|
|
247
287
|
resources.append(resource)
|
|
248
|
-
resources_content
|
|
288
|
+
resources_content += format_as_xml(resources, root_tag="resources", item_tag="resource")
|
|
249
289
|
else:
|
|
250
290
|
resources_content += "No resources found"
|
|
251
291
|
|
|
@@ -260,7 +300,7 @@ def tests_files_prompt(project: Project) -> str:
|
|
|
260
300
|
"content": file_path.read_text(),
|
|
261
301
|
}
|
|
262
302
|
tests.append(test)
|
|
263
|
-
test_content
|
|
303
|
+
test_content += format_as_xml(tests, root_tag="tests", item_tag="test")
|
|
264
304
|
else:
|
|
265
305
|
test_content += "No test files found"
|
|
266
306
|
|
|
@@ -836,6 +876,7 @@ You have access to the following tools:
|
|
|
836
876
|
15. `run_command` - Run a command using the Tinybird CLI.
|
|
837
877
|
16. `diff_resource` - Diff the content of a resource in Tinybird Cloud vs Tinybird Local vs Project local file.
|
|
838
878
|
17. `rename_datafile_or_fixture` - Rename a datafile or fixture.
|
|
879
|
+
18. `complete_plan` - Complete a plan.
|
|
839
880
|
|
|
840
881
|
# When creating, updating, or deleting files:
|
|
841
882
|
1. Use `plan` tool to plan the creation, update, rename, or deletion of resources.
|
|
@@ -845,6 +886,7 @@ You have access to the following tools:
|
|
|
845
886
|
5. If the file was created or removed successfully, report the result to the user.
|
|
846
887
|
6. If the file was not created or removed, finish the process and just wait for a new user prompt.
|
|
847
888
|
7. If the file was created or removed successfully, but the build failed, try to fix the error and repeat the process.
|
|
889
|
+
8. If the plan is completed or cancelled, use the `complete_plan` tool to complete the plan.
|
|
848
890
|
|
|
849
891
|
# When creating a landing datasource given a .ndjson file:
|
|
850
892
|
- If the user does not specify anything about the desired schema, create a schema like this (sorting key not needed in this case)
|
|
@@ -957,6 +999,14 @@ They can be run on a schedule, or executed on demand.
|
|
|
957
999
|
{sql_agent_instructions}
|
|
958
1000
|
{sql_instructions}
|
|
959
1001
|
|
|
1002
|
+
## Referencing tables in SQL queries:
|
|
1003
|
+
The following resources can be used as tables in SQL queries:
|
|
1004
|
+
- Datasources (.datasource files)
|
|
1005
|
+
- Materialized views (.datasource files target of .pipe files with `TYPE MATERIALIZED` defined)
|
|
1006
|
+
- Endpoints (.pipe files with `TYPE ENDPOINT` defined)
|
|
1007
|
+
- Default pipes (.pipe files with no `TYPE` defined)
|
|
1008
|
+
- Node names present in the same .pipe file
|
|
1009
|
+
|
|
960
1010
|
{secrets_instructions}
|
|
961
1011
|
|
|
962
1012
|
{external_tables_instructions}
|
|
@@ -53,9 +53,10 @@ def append_file(
|
|
|
53
53
|
return "Append operation cancelled by user."
|
|
54
54
|
|
|
55
55
|
cloud_or_local = "Cloud" if cloud else "Local"
|
|
56
|
+
active_plan = ctx.deps.get_plan() is not None and not cloud
|
|
56
57
|
confirmation = show_confirmation(
|
|
57
58
|
title=f"Append fixture {fixture_pathname} to datasource '{datasource_name}' in Tinybird {cloud_or_local}?",
|
|
58
|
-
skip_confirmation=ctx.deps.dangerously_skip_permissions,
|
|
59
|
+
skip_confirmation=ctx.deps.dangerously_skip_permissions or active_plan,
|
|
59
60
|
)
|
|
60
61
|
|
|
61
62
|
if confirmation == "review":
|
|
@@ -112,9 +113,10 @@ def append_url(
|
|
|
112
113
|
return "Append operation cancelled by user."
|
|
113
114
|
|
|
114
115
|
cloud_or_local = "Cloud" if cloud else "Local"
|
|
116
|
+
active_plan = ctx.deps.get_plan() is not None and not cloud
|
|
115
117
|
confirmation = show_confirmation(
|
|
116
118
|
title=f"Append URL {fixture_url} to datasource '{datasource_name}' in Tinybird {cloud_or_local}?",
|
|
117
|
-
skip_confirmation=ctx.deps.dangerously_skip_permissions,
|
|
119
|
+
skip_confirmation=ctx.deps.dangerously_skip_permissions or active_plan,
|
|
118
120
|
)
|
|
119
121
|
|
|
120
122
|
if confirmation == "review":
|
|
@@ -50,9 +50,10 @@ def create_datafile(
|
|
|
50
50
|
content = create_terminal_box(resource.content, title=resource.pathname)
|
|
51
51
|
click.echo(content)
|
|
52
52
|
action = "Create" if not exists else "Update"
|
|
53
|
+
active_plan = ctx.deps.get_plan() is not None
|
|
53
54
|
confirmation = show_confirmation(
|
|
54
55
|
title=f"{action} '{resource.pathname}'?",
|
|
55
|
-
skip_confirmation=ctx.deps.dangerously_skip_permissions,
|
|
56
|
+
skip_confirmation=ctx.deps.dangerously_skip_permissions or active_plan,
|
|
56
57
|
)
|
|
57
58
|
|
|
58
59
|
if confirmation == "review":
|
|
@@ -146,9 +147,10 @@ def rename_datafile_or_fixture(ctx: RunContext[TinybirdAgentContext], path: str,
|
|
|
146
147
|
"""
|
|
147
148
|
try:
|
|
148
149
|
ctx.deps.thinking_animation.stop()
|
|
150
|
+
active_plan = ctx.deps.get_plan() is not None
|
|
149
151
|
confirmation = show_confirmation(
|
|
150
152
|
title=f"Rename '{path}' to '{new_path}'?",
|
|
151
|
-
skip_confirmation=ctx.deps.dangerously_skip_permissions,
|
|
153
|
+
skip_confirmation=ctx.deps.dangerously_skip_permissions or active_plan,
|
|
152
154
|
)
|
|
153
155
|
|
|
154
156
|
if confirmation == "review":
|
|
@@ -211,10 +213,10 @@ def remove_file(ctx: RunContext[TinybirdAgentContext], path: str) -> str:
|
|
|
211
213
|
click.echo(FeedbackManager.error(message=f"Error: File {path} not found"))
|
|
212
214
|
ctx.deps.thinking_animation.start()
|
|
213
215
|
return f"Error: File {path} not found (double check the file path)"
|
|
214
|
-
|
|
216
|
+
active_plan = ctx.deps.get_plan() is not None
|
|
215
217
|
confirmation = show_confirmation(
|
|
216
218
|
title=f"Delete '{path}'?",
|
|
217
|
-
skip_confirmation=ctx.deps.dangerously_skip_permissions,
|
|
219
|
+
skip_confirmation=ctx.deps.dangerously_skip_permissions or active_plan,
|
|
218
220
|
)
|
|
219
221
|
|
|
220
222
|
if confirmation == "review":
|
|
@@ -70,9 +70,10 @@ def generate_mock_fixture(
|
|
|
70
70
|
content = create_terminal_box(preview_content, title=f"fixtures/{datasource_name}.{data_format}")
|
|
71
71
|
click.echo(content)
|
|
72
72
|
click.echo("Showing a preview of the file.\n")
|
|
73
|
+
active_plan = ctx.deps.get_plan() is not None
|
|
73
74
|
confirmation = show_confirmation(
|
|
74
75
|
title=f"Create fixture file for datasource '{datasource_name}'?",
|
|
75
|
-
skip_confirmation=ctx.deps.dangerously_skip_permissions,
|
|
76
|
+
skip_confirmation=ctx.deps.dangerously_skip_permissions or active_plan,
|
|
76
77
|
)
|
|
77
78
|
|
|
78
79
|
if confirmation == "review":
|
|
@@ -85,7 +86,7 @@ def generate_mock_fixture(
|
|
|
85
86
|
click.echo(FeedbackManager.success(message=f"✓ {fixture_path_name} created"))
|
|
86
87
|
confirmation = show_confirmation(
|
|
87
88
|
title=f"Append {fixture_path_name} to datasource '{datasource_name}'?",
|
|
88
|
-
skip_confirmation=ctx.deps.dangerously_skip_permissions,
|
|
89
|
+
skip_confirmation=ctx.deps.dangerously_skip_permissions or active_plan,
|
|
89
90
|
)
|
|
90
91
|
if confirmation == "review":
|
|
91
92
|
feedback = show_input(ctx.deps.workspace_name)
|
|
@@ -1,7 +1,45 @@
|
|
|
1
|
+
from typing import Literal
|
|
2
|
+
|
|
1
3
|
import click
|
|
2
4
|
from pydantic_ai import RunContext
|
|
3
5
|
|
|
4
|
-
from tinybird.tb.modules.agent.utils import
|
|
6
|
+
from tinybird.tb.modules.agent.utils import (
|
|
7
|
+
AgentRunCancelled,
|
|
8
|
+
TinybirdAgentContext,
|
|
9
|
+
show_input,
|
|
10
|
+
show_options,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
PlanConfirmationResult = Literal["yes", "review", "yes_and_auto_implement"]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def show_plan_confirmation(skip_confirmation: bool = False) -> PlanConfirmationResult:
|
|
17
|
+
if skip_confirmation:
|
|
18
|
+
return "yes"
|
|
19
|
+
|
|
20
|
+
title = "Do you want to continue with the plan?"
|
|
21
|
+
while True:
|
|
22
|
+
result = show_options(
|
|
23
|
+
options=[
|
|
24
|
+
"Yes, continue",
|
|
25
|
+
"Yes, continue and implement all",
|
|
26
|
+
"No, tell Tinybird Code what to do",
|
|
27
|
+
"Cancel",
|
|
28
|
+
],
|
|
29
|
+
title=title,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
if result is None: # Cancelled
|
|
33
|
+
raise AgentRunCancelled(f"User cancelled the operation: {title}")
|
|
34
|
+
|
|
35
|
+
if result.startswith("Yes, continue and implement all"):
|
|
36
|
+
return "yes_and_auto_implement"
|
|
37
|
+
if result.startswith("Yes"):
|
|
38
|
+
return "yes"
|
|
39
|
+
elif result.startswith("No"):
|
|
40
|
+
return "review"
|
|
41
|
+
|
|
42
|
+
raise AgentRunCancelled(f"User cancelled the operation: {title}")
|
|
5
43
|
|
|
6
44
|
|
|
7
45
|
def plan(ctx: RunContext[TinybirdAgentContext], plan: str) -> str:
|
|
@@ -15,15 +53,34 @@ def plan(ctx: RunContext[TinybirdAgentContext], plan: str) -> str:
|
|
|
15
53
|
"""
|
|
16
54
|
ctx.deps.thinking_animation.stop()
|
|
17
55
|
plan = plan.strip()
|
|
56
|
+
|
|
18
57
|
click.echo(plan)
|
|
19
|
-
confirmation =
|
|
20
|
-
title="Do you want to continue with the plan?", skip_confirmation=ctx.deps.dangerously_skip_permissions
|
|
21
|
-
)
|
|
58
|
+
confirmation = show_plan_confirmation(skip_confirmation=ctx.deps.dangerously_skip_permissions)
|
|
22
59
|
|
|
23
60
|
if confirmation == "review":
|
|
24
61
|
feedback = show_input(ctx.deps.workspace_name)
|
|
25
62
|
ctx.deps.thinking_animation.start()
|
|
63
|
+
ctx.deps.cancel_plan()
|
|
26
64
|
return f"User did not confirm the proposed plan and gave the following feedback: {feedback}"
|
|
27
65
|
|
|
28
66
|
ctx.deps.thinking_animation.start()
|
|
29
|
-
|
|
67
|
+
|
|
68
|
+
if confirmation == "yes_and_auto_implement":
|
|
69
|
+
plan_id = ctx.deps.start_plan(plan=plan)
|
|
70
|
+
return f"User confirmed the plan {plan_id}. Implementing..."
|
|
71
|
+
else:
|
|
72
|
+
return "User confirmed the plan. Implementing..."
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def complete_plan(ctx: RunContext[TinybirdAgentContext]) -> str:
|
|
76
|
+
"""Given an ongoing plan, complete it
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
ctx (RunContext[TinybirdAgentContext]): The context of the agent.
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
str: The result of the plan.
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
plan_id = ctx.deps.cancel_plan()
|
|
86
|
+
return f"Plan {plan_id} completed"
|
|
@@ -70,9 +70,10 @@ def create_or_update_secrets(ctx: RunContext[TinybirdAgentContext], secrets: dic
|
|
|
70
70
|
preview_content = create_terminal_box(new_content, title=".env.local")
|
|
71
71
|
click.echo(preview_content)
|
|
72
72
|
|
|
73
|
+
active_plan = ctx.deps.get_plan() is not None
|
|
73
74
|
confirmation = show_confirmation(
|
|
74
75
|
title=f"{action} {len(secrets)} secret(s) in .env.local?",
|
|
75
|
-
skip_confirmation=ctx.deps.dangerously_skip_permissions,
|
|
76
|
+
skip_confirmation=ctx.deps.dangerously_skip_permissions or active_plan,
|
|
76
77
|
)
|
|
77
78
|
|
|
78
79
|
if confirmation == "review":
|
|
@@ -40,7 +40,6 @@ class TinybirdAgentContext(BaseModel):
|
|
|
40
40
|
workspace_name: str
|
|
41
41
|
thinking_animation: Any
|
|
42
42
|
get_project_files: Callable[[], List[str]]
|
|
43
|
-
explore_data: Callable[[str], str]
|
|
44
43
|
build_project: Callable[..., None]
|
|
45
44
|
build_project_test: Callable[..., None]
|
|
46
45
|
deploy_project: Callable[..., None]
|
|
@@ -68,6 +67,9 @@ class TinybirdAgentContext(BaseModel):
|
|
|
68
67
|
local_host: str
|
|
69
68
|
local_token: str
|
|
70
69
|
run_id: Optional[str] = None
|
|
70
|
+
get_plan: Callable[..., Optional[str]]
|
|
71
|
+
start_plan: Callable[..., str]
|
|
72
|
+
cancel_plan: Callable[..., Optional[str]]
|
|
71
73
|
|
|
72
74
|
|
|
73
75
|
default_style = PromptStyle.from_dict(
|
|
@@ -779,10 +781,10 @@ def copy_fixture_to_project_folder_if_needed(
|
|
|
779
781
|
if input_path.exists() and not _is_path_inside_project(input_path, project_folder):
|
|
780
782
|
# Ask for confirmation to copy the file
|
|
781
783
|
click.echo(FeedbackManager.highlight(message=f"» File {fixture_pathname} is outside the project folder."))
|
|
782
|
-
|
|
784
|
+
active_plan = ctx.deps.get_plan() is not None
|
|
783
785
|
confirmation = show_confirmation(
|
|
784
786
|
title=f"Copy {input_path.name} to project folder for analysis?",
|
|
785
|
-
skip_confirmation=ctx.deps.dangerously_skip_permissions,
|
|
787
|
+
skip_confirmation=ctx.deps.dangerously_skip_permissions or active_plan,
|
|
786
788
|
)
|
|
787
789
|
|
|
788
790
|
if confirmation == "review":
|
|
@@ -134,7 +134,11 @@ def promote_deployment(host: Optional[str], headers: dict, wait: bool, ingest_hi
|
|
|
134
134
|
result = api_fetch(TINYBIRD_API_URL, headers=headers)
|
|
135
135
|
|
|
136
136
|
last_deployment = result.get("deployment")
|
|
137
|
-
if last_deployment
|
|
137
|
+
if not last_deployment:
|
|
138
|
+
click.echo(FeedbackManager.error(message="Error parsing deployment from response"))
|
|
139
|
+
sys_exit("deployment_error", "Error parsing deployment from response")
|
|
140
|
+
|
|
141
|
+
if last_deployment and last_deployment.get("status") == "deleted":
|
|
138
142
|
click.echo(FeedbackManager.success(message=f"✓ Deployment #{candidate_deployment.get('id')} is live!"))
|
|
139
143
|
break
|
|
140
144
|
|
|
@@ -4,6 +4,7 @@ tinybird/datatypes.py,sha256=r4WCvspmrXTJHiPjjyOTiZyZl31FO3Ynkwq4LQsYm6E,11059
|
|
|
4
4
|
tinybird/feedback_manager.py,sha256=XY8d83pRlq-LH7xHMApkaEebfXEWLjDzrGe1prpcTHE,69778
|
|
5
5
|
tinybird/git_settings.py,sha256=Sw_8rGmribEFJ4Z_6idrVytxpFYk7ez8ei0qHULzs3E,3934
|
|
6
6
|
tinybird/prompts.py,sha256=HoDv9TxPiP8v2XoGTWYxP133dK9CEbXVv4XE5IT339c,45483
|
|
7
|
+
tinybird/service_datasources.py,sha256=o6Az3T6OvcChR_7GXRu7sJH173KzGg1Gv-dNd0bI_vY,46085
|
|
7
8
|
tinybird/sql.py,sha256=UZJLop6zA9tTPEaS-Fq7M-QyzmC5uV_tIeXZzkjnhso,48299
|
|
8
9
|
tinybird/sql_template.py,sha256=kaF5pi-f2JiWSYXEF8JsU1OIxvdu2homHnw4MYjq0n8,101953
|
|
9
10
|
tinybird/sql_template_fmt.py,sha256=KUHdj5rYCYm_rKKdXYSJAE9vIyXUQLB0YSZnUXHeBlY,10196
|
|
@@ -12,12 +13,12 @@ tinybird/syncasync.py,sha256=IPnOx6lMbf9SNddN1eBtssg8vCLHMt76SuZ6YNYm-Yk,27761
|
|
|
12
13
|
tinybird/tornado_template.py,sha256=jjNVDMnkYFWXflmT8KU_Ssbo5vR8KQq3EJMk5vYgXRw,41959
|
|
13
14
|
tinybird/ch_utils/constants.py,sha256=v5-nkXHUhysu4i9Z4WVv0-sBbh6xSYUH5q5xHSY2xTI,4194
|
|
14
15
|
tinybird/ch_utils/engine.py,sha256=4X1B-iuhdW_mxKnX_m3iCsxgP9RPVgR75g7yH1vsJ6A,40851
|
|
15
|
-
tinybird/datafile/common.py,sha256=
|
|
16
|
+
tinybird/datafile/common.py,sha256=euJAKSu6GA67oIcRvrrBGsGI1nZtkSWM45ikjLUlJz0,106049
|
|
16
17
|
tinybird/datafile/exceptions.py,sha256=8rw2umdZjtby85QbuRKFO5ETz_eRHwUY5l7eHsy1wnI,556
|
|
17
18
|
tinybird/datafile/parse_connection.py,sha256=tRyn2Rpr1TeWet5BXmMoQgaotbGdYep1qiTak_OqC5E,1825
|
|
18
19
|
tinybird/datafile/parse_datasource.py,sha256=ssW8QeFSgglVFi3sDZj_HgkJiTJ2069v2JgqnH3CkDE,1825
|
|
19
20
|
tinybird/datafile/parse_pipe.py,sha256=xf4m0Tw44QWJzHzAm7Z7FwUoUUtr7noMYjU1NiWnX0k,3880
|
|
20
|
-
tinybird/tb/__cli__.py,sha256=
|
|
21
|
+
tinybird/tb/__cli__.py,sha256=i0Uj8XdvCB6yYSx3uE3zP-aqoSr6lSMqfDHHiLv2Euw,247
|
|
21
22
|
tinybird/tb/check_pypi.py,sha256=Gp0HkHHDFMSDL6nxKlOY51z7z1Uv-2LRexNTZSHHGmM,552
|
|
22
23
|
tinybird/tb/cli.py,sha256=FdDFEIayjmsZEVsVSSvRiVYn_FHOVg_zWQzchnzfWho,1008
|
|
23
24
|
tinybird/tb/client.py,sha256=IQRaInDjOwr9Fzaz3_xXc3aUGqh94tM2lew7IZbB9eM,53733
|
|
@@ -33,7 +34,7 @@ tinybird/tb/modules/copy.py,sha256=dPZkcIDvxjJrlQUIvToO0vsEEEs4EYumbNV77-BzNoU,4
|
|
|
33
34
|
tinybird/tb/modules/create.py,sha256=pJxHXG69c9Z_21s-7VuJ3RZOF_nJU51LEwiAkvI3dZY,23251
|
|
34
35
|
tinybird/tb/modules/datasource.py,sha256=kDFHdxckTnRosk2829icfltQvlJd8EY5c9oWB5eS5Xo,41797
|
|
35
36
|
tinybird/tb/modules/deployment.py,sha256=v0layOmG0IMnuXc3RT39mpGfa5M8yPlrL9F089fJFCo,15964
|
|
36
|
-
tinybird/tb/modules/deployment_common.py,sha256=
|
|
37
|
+
tinybird/tb/modules/deployment_common.py,sha256=8Cc0VyKthmTnULiTKgciPyOGtf1kaRghC3Q00bZJbD4,19897
|
|
37
38
|
tinybird/tb/modules/deprecations.py,sha256=rrszC1f_JJeJ8mUxGoCxckQTJFBCR8wREf4XXXN-PRc,4507
|
|
38
39
|
tinybird/tb/modules/dev_server.py,sha256=57FCKuWpErwYUYgHspYDkLWEm9F4pbvVOtMrFXX1fVU,10129
|
|
39
40
|
tinybird/tb/modules/endpoint.py,sha256=ksRj6mfDb9Xv63PhTkV_uKSosgysHElqagg3RTt21Do,11958
|
|
@@ -69,34 +70,34 @@ tinybird/tb/modules/watch.py,sha256=No0bK1M1_3CYuMaIgylxf7vYFJ72lTJe3brz6xQ-mJo,
|
|
|
69
70
|
tinybird/tb/modules/workspace.py,sha256=tCP1zZMwBhLRGm22TGfpSd4cHvQLAS1o_azIXv_r6uw,11172
|
|
70
71
|
tinybird/tb/modules/workspace_members.py,sha256=5JdkJgfuEwbq-t6vxkBhYwgsiTDxF790wsa6Xfif9nk,8608
|
|
71
72
|
tinybird/tb/modules/agent/__init__.py,sha256=i3oe3vDIWWPaicdCM0zs7D7BJ1W0k7th93ooskHAV00,54
|
|
72
|
-
tinybird/tb/modules/agent/agent.py,sha256=
|
|
73
|
+
tinybird/tb/modules/agent/agent.py,sha256=aCeZ1mhnd-WTEBHg_82lIpLPOVa6dXJ7uriEpU0N7yM,36100
|
|
73
74
|
tinybird/tb/modules/agent/animations.py,sha256=4WOC5_2BracttmMCrV0H91tXfWcUzQHBUaIJc5FA7tE,3490
|
|
74
75
|
tinybird/tb/modules/agent/banner.py,sha256=l6cO5Fi7lbVKp-GsBP8jf3IkjOWxg2jpAt9NBCy0WR8,4085
|
|
75
76
|
tinybird/tb/modules/agent/command_agent.py,sha256=0Z08rQsir59zQAr-kkOvsKIFpIBsBSTGJJ1VgqqF5WA,3654
|
|
76
77
|
tinybird/tb/modules/agent/compactor.py,sha256=BK5AxZFhrp3xWnsRnYaleiYoIWtVNc-_m650Hsopt8g,13841
|
|
77
|
-
tinybird/tb/modules/agent/explore_agent.py,sha256=
|
|
78
|
+
tinybird/tb/modules/agent/explore_agent.py,sha256=gyD5uV5TJwV24eeQiSwhkgfNPb4mtbeH7t2qSdoc18U,4100
|
|
78
79
|
tinybird/tb/modules/agent/memory.py,sha256=vBewB_64L_wHoT4tLT6UX2uxcHwSY880QZ26F9rPqXs,3793
|
|
79
80
|
tinybird/tb/modules/agent/mock_agent.py,sha256=zbAZfAqdSLUtMr2VqO0erWpzjT2F1tTcuYjvHb-gvbA,8023
|
|
80
81
|
tinybird/tb/modules/agent/models.py,sha256=eokO8XlY-kVJOsbqiVporGUAOCyKAXCO5xgTEK9SM6Y,2208
|
|
81
|
-
tinybird/tb/modules/agent/prompts.py,sha256=
|
|
82
|
+
tinybird/tb/modules/agent/prompts.py,sha256=a8ZnOZCTGRnxa_dlrTSmeaM9snOI3sMx0nwUdv7aVGI,44165
|
|
82
83
|
tinybird/tb/modules/agent/testing_agent.py,sha256=AtwtJViH7805i7djyBgDb7SSUtDyJnw0TWJu6lBFsrg,2953
|
|
83
|
-
tinybird/tb/modules/agent/utils.py,sha256=
|
|
84
|
+
tinybird/tb/modules/agent/utils.py,sha256=R1RcoPvjKh4OyuwLztWb74t2wBpV7JvNVLQ_JyClveM,31901
|
|
84
85
|
tinybird/tb/modules/agent/tools/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
85
86
|
tinybird/tb/modules/agent/tools/analyze.py,sha256=CR5LXg4fou-zYEksqnjpJ0icvxJVoKnTctoI1NRvqCM,3873
|
|
86
|
-
tinybird/tb/modules/agent/tools/append.py,sha256=
|
|
87
|
+
tinybird/tb/modules/agent/tools/append.py,sha256=8UsGMzmv7GYbzp0gerOBqpxxocDbous_haSuwS3zuGU,8222
|
|
87
88
|
tinybird/tb/modules/agent/tools/build.py,sha256=Hm-xDAP9ckMiKquT-DmDg5H0yxZefLOaWKANyoVSaEQ,846
|
|
88
|
-
tinybird/tb/modules/agent/tools/datafile.py,sha256=
|
|
89
|
+
tinybird/tb/modules/agent/tools/datafile.py,sha256=kTob7G2TwCwIgwom0rERgXQ13rgPtZv3_ByLnrvpIdU,10881
|
|
89
90
|
tinybird/tb/modules/agent/tools/deploy.py,sha256=6Vmm0lCG8XKE2iUF_ZJrOqXbTFhoe3anPzYCFehQ3_E,2027
|
|
90
91
|
tinybird/tb/modules/agent/tools/deploy_check.py,sha256=pE3d9TPtXVKZjYbU0G6ORAGI86lN5K_4JKUriClERbM,1229
|
|
91
92
|
tinybird/tb/modules/agent/tools/diff_resource.py,sha256=_9xHcDzCTKk_E1wKQbuktVqV6U9sA0kqYaBxWvtliX0,2613
|
|
92
93
|
tinybird/tb/modules/agent/tools/execute_query.py,sha256=DL2jsZ0jaEqFIkGoiWfR-IUAwsgoF0D-_JUhq7xe4gA,9145
|
|
93
94
|
tinybird/tb/modules/agent/tools/get_endpoint_stats.py,sha256=r2FrXg1L1s_Llr1tPdJ6k_gu6qw7qLsAXOkbz3eTk1g,2307
|
|
94
95
|
tinybird/tb/modules/agent/tools/get_openapi_definition.py,sha256=4TIMO2XzHBMhpt9zIWRfjjPZbThT8r_iPS4CVHcItE0,2904
|
|
95
|
-
tinybird/tb/modules/agent/tools/mock.py,sha256=
|
|
96
|
-
tinybird/tb/modules/agent/tools/plan.py,sha256=
|
|
96
|
+
tinybird/tb/modules/agent/tools/mock.py,sha256=Seo4WcYNLL1-SmPXutoaX94_pfOdIb47JXo8dHtUVhg,7106
|
|
97
|
+
tinybird/tb/modules/agent/tools/plan.py,sha256=uAJEHZ-xXIq-EpURJYV7GUyY7IbIgactw9NWeCsIT9Y,2516
|
|
97
98
|
tinybird/tb/modules/agent/tools/request_endpoint.py,sha256=bsLWrMn-ofJM3nn9vm8j_U8fdopVd3H5L0ii6ji-Kuw,4359
|
|
98
99
|
tinybird/tb/modules/agent/tools/run_command.py,sha256=ypvIU0j1XVUWghqt-dpWHm3GQIYsZwE7kRHC3Wau_H0,1708
|
|
99
|
-
tinybird/tb/modules/agent/tools/secret.py,sha256=
|
|
100
|
+
tinybird/tb/modules/agent/tools/secret.py,sha256=8AGTZgHLPg1bxCA2cPMnb-zNutWEwn4emHo7kLjJC5w,4323
|
|
100
101
|
tinybird/tb/modules/agent/tools/test.py,sha256=4XuEWVHLOTSO51Z9xJ08dTjk0j3IWY_JlPtSBO5aaUs,10373
|
|
101
102
|
tinybird/tb/modules/datafile/build.py,sha256=NFKBrusFLU0WJNCXePAFWiEDuTaXpwc0lHlOQWEJ43s,51117
|
|
102
103
|
tinybird/tb/modules/datafile/build_common.py,sha256=2yNdxe49IMA9wNvl25NemY2Iaz8L66snjOdT64dm1is,4511
|
|
@@ -118,8 +119,8 @@ tinybird/tb_cli_modules/config.py,sha256=IsgdtFRnUrkY8-Zo32lmk6O7u3bHie1QCxLwgp4
|
|
|
118
119
|
tinybird/tb_cli_modules/exceptions.py,sha256=pmucP4kTF4irIt7dXiG-FcnI-o3mvDusPmch1L8RCWk,3367
|
|
119
120
|
tinybird/tb_cli_modules/regions.py,sha256=QjsL5H6Kg-qr0aYVLrvb1STeJ5Sx_sjvbOYO0LrEGMk,166
|
|
120
121
|
tinybird/tb_cli_modules/telemetry.py,sha256=Hh2Io8ZPROSunbOLuMvuIFU4TqwWPmQTqal4WS09K1A,10449
|
|
121
|
-
tinybird-0.0.1.
|
|
122
|
-
tinybird-0.0.1.
|
|
123
|
-
tinybird-0.0.1.
|
|
124
|
-
tinybird-0.0.1.
|
|
125
|
-
tinybird-0.0.1.
|
|
122
|
+
tinybird-0.0.1.dev288.dist-info/METADATA,sha256=L0eOct16E7wiMuY7AZ7jIovDUt80Y1A3usyUbL_HN_0,1845
|
|
123
|
+
tinybird-0.0.1.dev288.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
|
124
|
+
tinybird-0.0.1.dev288.dist-info/entry_points.txt,sha256=LwdHU6TfKx4Qs7BqqtaczEZbImgU7Abe9Lp920zb_fo,43
|
|
125
|
+
tinybird-0.0.1.dev288.dist-info/top_level.txt,sha256=VqqqEmkAy7UNaD8-V51FCoMMWXjLUlR0IstvK7tJYVY,54
|
|
126
|
+
tinybird-0.0.1.dev288.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|