tinybird 0.0.1.dev43__py3-none-any.whl → 0.0.1.dev44__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/prompts.py CHANGED
@@ -397,9 +397,19 @@ Follow the instructions and generate the following response with no additional t
397
397
  """
398
398
 
399
399
 
400
- def create_prompt(existing_resources: str) -> str:
400
+ def create_prompt(existing_resources: str, feedback: str = "", history: str = "") -> str:
401
+ feedback_history = ""
402
+ if feedback and history:
403
+ feedback_history = f"""In case the <feedback> and <history> tags are present and not empty,
404
+ it means there was a previous attempt to generate the resources and the user provided feedback and history about previous responses.
405
+ Use the following feedback and history to regenerate the response:
406
+ Feedback to improve the response:
407
+ {feedback}
408
+ History of previous results:
409
+ {history}"""
410
+
401
411
  return """
402
- You are a Tinybird expert. You will be given a prompt to generate Tinybird resources: datasources and/or pipes.
412
+ You are a Tinybird expert. You will be given a prompt to generate new or update existing Tinybird resources: datasources and/or pipes.
403
413
  <existing_resources>{existing_resources}</existing_resources>
404
414
  {datasource_instructions}
405
415
  {pipe_instructions}
@@ -408,6 +418,9 @@ You are a Tinybird expert. You will be given a prompt to generate Tinybird resou
408
418
  {pipe_example}
409
419
  {copy_pipe_instructions}
410
420
  {materialized_pipe_instructions}
421
+
422
+ {feedback_history}
423
+
411
424
  Use the following format to generate the response and do not wrap it in any other text, including the <response> tag.
412
425
  <response>
413
426
  <resource>
@@ -426,10 +439,21 @@ Use the following format to generate the response and do not wrap it in any othe
426
439
  pipe_example=pipe_example,
427
440
  copy_pipe_instructions=copy_pipe_instructions,
428
441
  materialized_pipe_instructions=materialized_pipe_instructions,
442
+ feedback_history=feedback_history,
429
443
  )
430
444
 
431
445
 
432
- def mock_prompt(rows: int) -> str:
446
+ def mock_prompt(rows: int, feedback: str = "", history: str = "") -> str:
447
+ feedback_history = ""
448
+ if feedback and history:
449
+ feedback_history = f"""In case the <feedback> and <history> tags are present and not empty,
450
+ it means there was a previous attempt to generate the resources and the user provided feedback and history about previous responses.
451
+ Use the following feedback and history to regenerate the response:
452
+ Feedback to improve the response:
453
+ {feedback}
454
+ History of previous results:
455
+ {history}"""
456
+
433
457
  return f"""
434
458
  Given the schema for a Tinybird datasource, return a can you create a clickhouse sql query to generate some random data that matches that schema.
435
459
 
@@ -570,6 +594,9 @@ Follow the instructions and generate the following response with no additional t
570
594
  <response>
571
595
  <sql>[raw sql query here]</sql>
572
596
  </response>
597
+
598
+ {feedback_history}
599
+
573
600
  """
574
601
 
575
602
 
tinybird/tb/__cli__.py CHANGED
@@ -4,5 +4,5 @@ __description__ = 'Tinybird Command Line Tool'
4
4
  __url__ = 'https://www.tinybird.co/docs/cli/introduction.html'
5
5
  __author__ = 'Tinybird'
6
6
  __author_email__ = 'support@tinybird.co'
7
- __version__ = '0.0.1.dev43'
8
- __revision__ = 'ecb1311'
7
+ __version__ = '0.0.1.dev44'
8
+ __revision__ = '637cb4c'
tinybird/tb/cli.py CHANGED
@@ -6,7 +6,6 @@ if sys.platform == "win32":
6
6
 
7
7
  import tinybird.tb.modules.auth
8
8
  import tinybird.tb.modules.build
9
- import tinybird.tb.modules.build_client
10
9
  import tinybird.tb.modules.cli
11
10
  import tinybird.tb.modules.common
12
11
  import tinybird.tb.modules.copy
@@ -18,6 +17,7 @@ import tinybird.tb.modules.fmt
18
17
  import tinybird.tb.modules.job
19
18
  import tinybird.tb.modules.local
20
19
  import tinybird.tb.modules.login
20
+ import tinybird.tb.modules.materialization
21
21
  import tinybird.tb.modules.mock
22
22
  import tinybird.tb.modules.pipe
23
23
  import tinybird.tb.modules.tag
@@ -12,6 +12,7 @@ import requests
12
12
  from tinybird.client import TinyB
13
13
  from tinybird.tb.modules.cli import cli
14
14
  from tinybird.tb.modules.common import push_data
15
+ from tinybird.tb.modules.datafile.build import folder_build
15
16
  from tinybird.tb.modules.datafile.fixture import build_fixture_name, get_fixture_dir
16
17
  from tinybird.tb.modules.feedback_manager import FeedbackManager
17
18
  from tinybird.tb.modules.local_common import get_tinybird_local_client
@@ -37,9 +38,10 @@ def build(ctx: click.Context, watch: bool) -> None:
37
38
  if file_changed and file_changed.endswith(".ndjson"):
38
39
  rebuild_fixture(project, tb_client, file_changed)
39
40
  else:
40
- build_project(project, tb_client)
41
+ build_project(project, tb_client, file_changed)
41
42
  try:
42
43
  if file_changed:
44
+ asyncio.run(folder_build(project, filenames=[file_changed]))
43
45
  build_and_print_resource(tb_client, file_changed, diff)
44
46
  except Exception:
45
47
  pass
@@ -61,7 +63,7 @@ def build(ctx: click.Context, watch: bool) -> None:
61
63
  shell.run()
62
64
 
63
65
 
64
- def build_project(project: Project, tb_client: TinyB) -> None:
66
+ def build_project(project: Project, tb_client: TinyB, file_changed: Optional[str] = None) -> None:
65
67
  MULTIPART_BOUNDARY_DATA_PROJECT = "data_project://"
66
68
  DATAFILE_TYPE_TO_CONTENT_TYPE = {
67
69
  ".datasource": "text/plain",
@@ -103,20 +105,23 @@ def build_project(project: Project, tb_client: TinyB) -> None:
103
105
  if build_result == "success":
104
106
  datasources = result.get("datasources", [])
105
107
  pipes = result.get("pipes", [])
106
- for ds in datasources:
107
- ds_path_str: Optional[str] = next(
108
- (p for p in project_files if p.endswith(ds.get("name") + ".datasource")), None
109
- )
110
- if ds_path_str:
111
- ds_path = Path(ds_path_str)
112
- ds_path_str = ds_path_str.replace(f"{project.folder}/", "")
113
- click.echo(FeedbackManager.info(message=f"{ds_path_str} created"))
114
- for pipe in pipes:
115
- pipe_name = pipe.get("name")
116
- pipe_path_str: Optional[str] = next((p for p in project_files if p.endswith(pipe_name + ".pipe")), None)
117
- if pipe_path_str:
118
- pipe_path_str = pipe_path_str.replace(f"{project.folder}/", "")
119
- click.echo(FeedbackManager.info(message=f"✓ {pipe_path_str} created"))
108
+ if not file_changed:
109
+ for ds in datasources:
110
+ ds_path_str: Optional[str] = next(
111
+ (p for p in project_files if p.endswith(ds.get("name") + ".datasource")), None
112
+ )
113
+ if ds_path_str:
114
+ ds_path = Path(ds_path_str)
115
+ ds_path_str = ds_path_str.replace(f"{project.folder}/", "")
116
+ click.echo(FeedbackManager.info(message=f"✓ {ds_path_str} created"))
117
+ for pipe in pipes:
118
+ pipe_name = pipe.get("name")
119
+ pipe_path_str: Optional[str] = next(
120
+ (p for p in project_files if p.endswith(pipe_name + ".pipe")), None
121
+ )
122
+ if pipe_path_str:
123
+ pipe_path_str = pipe_path_str.replace(f"{project.folder}/", "")
124
+ click.echo(FeedbackManager.info(message=f"✓ {pipe_path_str} created"))
120
125
 
121
126
  try:
122
127
  for filename in project_files:
@@ -6,10 +6,9 @@
6
6
  import json
7
7
  import logging
8
8
  import os
9
- import pprint
10
9
  from os import getcwd
11
10
  from pathlib import Path
12
- from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple, Union
11
+ from typing import Any, Callable, Dict, List, Optional, Tuple, Union
13
12
 
14
13
  import click
15
14
  import humanfriendly
@@ -35,13 +34,7 @@ from tinybird.tb.modules.common import (
35
34
  )
36
35
  from tinybird.tb.modules.config import CLIConfig
37
36
  from tinybird.tb.modules.datafile.build import build_graph
38
- from tinybird.tb.modules.datafile.common import Datafile, DatafileSyntaxError
39
37
  from tinybird.tb.modules.datafile.diff import diff_command
40
- from tinybird.tb.modules.datafile.exceptions import (
41
- ParseException,
42
- )
43
- from tinybird.tb.modules.datafile.parse_datasource import parse_datasource
44
- from tinybird.tb.modules.datafile.parse_pipe import parse_pipe
45
38
  from tinybird.tb.modules.datafile.pull import folder_pull
46
39
  from tinybird.tb.modules.feedback_manager import FeedbackManager
47
40
  from tinybird.tb.modules.llm import LLM
@@ -132,55 +125,7 @@ async def cli(
132
125
  ctx.ensure_object(dict)["project"] = project
133
126
 
134
127
 
135
- @cli.command(hidden=True)
136
- @click.argument("filenames", type=click.Path(exists=True), nargs=-1, default=None)
137
- @click.option("--debug", is_flag=True, default=False, help="Print internal representation")
138
- @click.pass_context
139
- def check(ctx: Context, filenames: List[str], debug: bool) -> None:
140
- """Check file syntax."""
141
-
142
- if not filenames:
143
- project: Project = ctx.ensure_object(dict)["project"]
144
- filenames = project.get_project_files()
145
-
146
- def process(filenames: Iterable):
147
- parser_matrix = {".pipe": parse_pipe, ".datasource": parse_datasource}
148
- incl_suffix = ".incl"
149
- try:
150
- for filename in filenames:
151
- if os.path.isdir(filename):
152
- process(filenames=filename)
153
-
154
- click.echo(FeedbackManager.info_processing_file(filename=filename))
155
-
156
- file_suffix = Path(filename).suffix
157
- if file_suffix == incl_suffix:
158
- click.echo(FeedbackManager.info_ignoring_incl_file(filename=filename))
159
- continue
160
-
161
- doc: Datafile
162
- parser = parser_matrix.get(file_suffix)
163
- if not parser:
164
- raise ParseException(FeedbackManager.error_unsupported_datafile(extension=file_suffix))
165
-
166
- doc = parser(filename)
167
-
168
- click.echo(FeedbackManager.success_processing_file(filename=filename))
169
- if debug:
170
- pp = pprint.PrettyPrinter()
171
- for x in doc.nodes:
172
- pp.pprint(x)
173
-
174
- except DatafileSyntaxError as e:
175
- # TODO(eclbg): add the filename to the error message
176
- raise CLIException(str(e))
177
- except ParseException as e:
178
- raise CLIException(FeedbackManager.error_exception(error=e))
179
-
180
- process(filenames=filenames)
181
-
182
-
183
- @cli.command(hidden=True)
128
+ @cli.command()
184
129
  @click.option(
185
130
  "--folder", default=None, type=click.Path(exists=True, file_okay=False), help="Folder where files will be placed"
186
131
  )
@@ -163,7 +163,7 @@ def generate_datafile(
163
163
  force: Optional[bool] = False,
164
164
  _format: Optional[str] = "csv",
165
165
  folder: Optional[str] = None,
166
- ):
166
+ ) -> Path:
167
167
  p = Path(filename)
168
168
  base = Path("datasources")
169
169
  if folder:
@@ -190,6 +190,7 @@ def generate_datafile(
190
190
  fixture_file.write(data[: data.rfind(newline)])
191
191
  else:
192
192
  click.echo(FeedbackManager.error_file_already_exists(file=f))
193
+ return f
193
194
 
194
195
 
195
196
  async def get_current_workspace(config: CLIConfig) -> Optional[Dict[str, Any]]:
@@ -18,7 +18,7 @@ from tinybird.tb.modules.exceptions import CLIPipeException
18
18
  from tinybird.tb.modules.feedback_manager import FeedbackManager
19
19
 
20
20
 
21
- @cli.group(hidden=True)
21
+ @cli.group()
22
22
  @click.pass_context
23
23
  def copy(ctx):
24
24
  """Copy pipe commands"""
@@ -18,6 +18,7 @@ from tinybird.tb.modules.feedback_manager import FeedbackManager
18
18
  from tinybird.tb.modules.llm import LLM
19
19
  from tinybird.tb.modules.llm_utils import extract_xml, parse_xml
20
20
  from tinybird.tb.modules.local_common import get_tinybird_local_client
21
+ from tinybird.tb.modules.project import Project
21
22
 
22
23
 
23
24
  @cli.command()
@@ -41,8 +42,10 @@ from tinybird.tb.modules.local_common import get_tinybird_local_client
41
42
  )
42
43
  @click.option("--rows", type=int, default=10, help="Number of events to send")
43
44
  @click.option("--cursor", default=False, is_flag=True, help="Create .cursorrules file with Tinybird rules")
45
+ @click.pass_context
44
46
  @coro
45
47
  async def create(
48
+ ctx: click.Context,
46
49
  data: Optional[str],
47
50
  prompt: Optional[str],
48
51
  folder: Optional[str],
@@ -50,13 +53,14 @@ async def create(
50
53
  cursor: bool,
51
54
  ) -> None:
52
55
  """Initialize a new project."""
56
+ project: Project = ctx.ensure_object(dict)["project"]
53
57
  folder = folder or getcwd()
54
58
  folder_path = Path(folder)
55
59
  if not folder_path.exists():
56
60
  folder_path.mkdir()
57
61
 
58
62
  try:
59
- config = CLIConfig.get_project_config(folder)
63
+ config = CLIConfig.get_project_config(str(project.path))
60
64
  tb_client = config.get_client()
61
65
  user_token: Optional[str] = None
62
66
  if prompt:
@@ -80,7 +84,7 @@ async def create(
80
84
  click.echo(FeedbackManager.success(message="✓ Scaffolding completed!\n"))
81
85
 
82
86
  click.echo(FeedbackManager.highlight(message="\n» Creating resources..."))
83
- datasources_created = await create_resources(local_client, tb_client, user_token, data, prompt, folder)
87
+ result = await create_resources(local_client, tb_client, user_token, data, prompt, folder)
84
88
  click.echo(FeedbackManager.success(message="✓ Done!\n"))
85
89
 
86
90
  if not already_has_cicd(folder):
@@ -89,7 +93,7 @@ async def create(
89
93
  await init_cicd(data_project_dir=os.path.relpath(folder))
90
94
  click.echo(FeedbackManager.success(message="✓ Done!\n"))
91
95
 
92
- if validate_fixtures(folder) and datasources_created:
96
+ if should_generate_fixtures(result):
93
97
  click.echo(FeedbackManager.highlight(message="\n» Generating fixtures..."))
94
98
 
95
99
  if data:
@@ -110,7 +114,7 @@ async def create(
110
114
  datasource_content = datasource_path.read_text()
111
115
  has_json_path = "`json:" in datasource_content
112
116
  if has_json_path:
113
- prompt = f"<datasource_schema>{datasource_content}</datasource_schema>\n<user_input>{prompt}</user_input>"
117
+ prompt = f"<datasource_schema>{datasource_content}</datasource_schema>"
114
118
  response = llm.ask(system_prompt=mock_prompt(rows), prompt=prompt)
115
119
  sql = extract_xml(response, "sql")
116
120
  sql = sql.split("FORMAT")[0]
@@ -139,9 +143,10 @@ def validate_project_structure(folder: str) -> bool:
139
143
  return all((Path(folder) / path).exists() for path in PROJECT_PATHS)
140
144
 
141
145
 
142
- def validate_fixtures(folder: str) -> bool:
143
- datasource_files = [f for f in os.listdir(Path(folder) / "datasources") if f.endswith(".datasource")]
144
- return len(datasource_files) > 0
146
+ def should_generate_fixtures(result: str) -> bool:
147
+ if not result:
148
+ return False
149
+ return "<type>datasource</type>" in result
145
150
 
146
151
 
147
152
  def already_has_cicd(folder: str) -> bool:
@@ -168,12 +173,12 @@ async def create_resources(
168
173
  prompt: Optional[str],
169
174
  folder: str,
170
175
  ):
171
- force = True
176
+ result = ""
172
177
  folder_path = Path(folder)
173
178
  if data:
174
179
  path = folder_path / data
175
180
  format = path.suffix.lstrip(".")
176
- await _generate_datafile(str(path), local_client, format=format, force=force)
181
+ await _generate_datafile(str(path), local_client, format=format, force=True)
177
182
  name = data.split(".")[0]
178
183
  generate_pipe_file(
179
184
  f"{name}_endpoint",
@@ -185,7 +190,10 @@ TYPE ENDPOINT
185
190
  """,
186
191
  folder,
187
192
  )
188
- return True
193
+ result = (
194
+ f"<response><resource><type>datasource</type><name>{name}</name><content></content></resource></response>"
195
+ )
196
+
189
197
  elif prompt and user_token:
190
198
  datasource_paths = [
191
199
  Path(folder) / "datasources" / f
@@ -212,41 +220,74 @@ TYPE ENDPOINT
212
220
  ]
213
221
  )
214
222
  llm = LLM(user_token=user_token, host=tb_client.host)
215
- result = llm.ask(system_prompt=create_prompt(resources_xml), prompt=prompt)
216
- result = extract_xml(result, "response")
217
- resources = parse_xml(result, "resource")
218
- datasources = []
219
- pipes = []
220
- for resource_xml in resources:
221
- resource_type = extract_xml(resource_xml, "type")
222
- name = extract_xml(resource_xml, "name")
223
- content = extract_xml(resource_xml, "content")
224
- resource = {
225
- "name": name,
226
- "content": content,
227
- }
228
- if resource_type.lower() == "datasource":
229
- datasources.append(resource)
230
- elif resource_type.lower() == "pipe":
231
- pipes.append(resource)
232
-
233
- for ds in datasources:
234
- content = ds["content"].replace("```", "")
235
- filename = f"{ds['name']}.datasource"
236
- generate_datafile(
237
- content,
238
- filename=filename,
239
- data=None,
240
- _format="ndjson",
241
- force=force,
242
- folder=folder,
243
- )
244
-
245
- for pipe in pipes:
246
- content = pipe["content"].replace("```", "")
247
- generate_pipe_file(pipe["name"], content, folder)
248
-
249
- return len(datasources) > 0
223
+ result = ""
224
+ iterations = 0
225
+ history = ""
226
+ generated_paths: list[Path] = []
227
+
228
+ while iterations < 10:
229
+ feedback = ""
230
+ if iterations > 0:
231
+ feedback = click.prompt("\nFollow-up instructions or continue", default="continue")
232
+ if iterations > 0 and (not feedback or feedback in ("continue", "ok", "exit", "quit", "q")):
233
+ break
234
+ else:
235
+ if iterations > 0:
236
+ click.echo(FeedbackManager.highlight(message="\n» Creating resources..."))
237
+ for path in generated_paths:
238
+ path.unlink()
239
+ generated_paths = []
240
+
241
+ save_context(prompt, feedback)
242
+ result = llm.ask(system_prompt=create_prompt(resources_xml, feedback, history), prompt=prompt)
243
+ result = extract_xml(result, "response")
244
+ history = (
245
+ history
246
+ + f"""
247
+ <result_iteration_{iterations}>
248
+ {result}
249
+ </result_iteration_{iterations}>
250
+ """
251
+ )
252
+ resources = parse_xml(result, "resource")
253
+ datasources = []
254
+ pipes = []
255
+ for resource_xml in resources:
256
+ resource_type = extract_xml(resource_xml, "type")
257
+ name = extract_xml(resource_xml, "name")
258
+ content = extract_xml(resource_xml, "content")
259
+ resource = {
260
+ "name": name,
261
+ "content": content,
262
+ }
263
+ if resource_type.lower() == "datasource":
264
+ datasources.append(resource)
265
+ elif resource_type.lower() == "pipe":
266
+ pipes.append(resource)
267
+
268
+ for ds in datasources:
269
+ content = ds["content"].replace("```", "")
270
+ filename = f"{ds['name']}.datasource"
271
+ datasource_path = generate_datafile(
272
+ content,
273
+ filename=filename,
274
+ data=None,
275
+ _format="ndjson",
276
+ force=True,
277
+ folder=folder,
278
+ )
279
+ generated_paths.append(datasource_path)
280
+ for pipe in pipes:
281
+ content = pipe["content"].replace("```", "")
282
+ pipe_path = generate_pipe_file(pipe["name"], content, folder)
283
+ generated_paths.append(pipe_path)
284
+
285
+ iterations += 1
286
+
287
+ if iterations == 10:
288
+ click.echo(FeedbackManager.info(message="Too many iterations. Change the prompt and try again."))
289
+
290
+ return result
250
291
 
251
292
 
252
293
  def init_git(folder: str):
@@ -266,7 +307,7 @@ def init_git(folder: str):
266
307
  raise CLIException(f"Error initializing Git: {e}")
267
308
 
268
309
 
269
- def generate_pipe_file(name: str, content: str, folder: str):
310
+ def generate_pipe_file(name: str, content: str, folder: str) -> Path:
270
311
  def is_copy(content: str) -> bool:
271
312
  return re.search(r"TYPE copy", content, re.IGNORECASE) is not None
272
313
 
@@ -292,6 +333,7 @@ def generate_pipe_file(name: str, content: str, folder: str):
292
333
  with open(f"{f}", "w") as file:
293
334
  file.write(content)
294
335
  click.echo(FeedbackManager.info_file_created(file=f.relative_to(folder)))
336
+ return f.relative_to(folder)
295
337
 
296
338
 
297
339
  def already_has_cursorrules(folder: str) -> bool:
@@ -301,3 +343,20 @@ def already_has_cursorrules(folder: str) -> bool:
301
343
  def create_cursorrules(folder: str):
302
344
  cursorrules_file = Path(folder) / ".cursorrules"
303
345
  cursorrules_file.write_text(cursorrules_prompt)
346
+
347
+
348
+ def get_context_file() -> Path:
349
+ context_file = Path(os.path.expanduser("~/.tb_create_context"))
350
+ if not context_file.exists():
351
+ context_file.touch()
352
+ return context_file
353
+
354
+
355
+ def get_context() -> str:
356
+ context_file = get_context_file()
357
+ return context_file.read_text()
358
+
359
+
360
+ def save_context(prompt: str, feedback: str):
361
+ context_file = get_context_file()
362
+ context_file.write_text(f"- {prompt}\n{feedback}")