tinybird 0.0.1.dev262__tar.gz → 0.0.1.dev263__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 tinybird might be problematic. Click here for more details.

Files changed (146) hide show
  1. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/PKG-INFO +1 -1
  2. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/datafile/common.py +151 -0
  3. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/tb/__cli__.py +2 -2
  4. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/tb/modules/agent/agent.py +130 -17
  5. tinybird-0.0.1.dev263/tinybird/tb/modules/agent/banner.py +131 -0
  6. tinybird-0.0.1.dev263/tinybird/tb/modules/agent/command_agent.py +59 -0
  7. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/tb/modules/agent/prompts.py +91 -20
  8. tinybird-0.0.1.dev263/tinybird/tb/modules/agent/testing_agent.py +62 -0
  9. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/tb/modules/agent/tools/create_datafile.py +1 -1
  10. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/tb/modules/agent/tools/execute_query.py +18 -0
  11. tinybird-0.0.1.dev263/tinybird/tb/modules/agent/tools/run_command.py +38 -0
  12. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/tb/modules/agent/tools/test.py +28 -0
  13. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/tb/modules/agent/utils.py +7 -2
  14. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/tb/modules/cli.py +8 -6
  15. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/tb/modules/datasource.py +3 -1
  16. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/tb/modules/test_common.py +11 -2
  17. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird.egg-info/PKG-INFO +1 -1
  18. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird.egg-info/SOURCES.txt +3 -0
  19. tinybird-0.0.1.dev262/tinybird/tb/modules/agent/banner.py +0 -56
  20. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/setup.cfg +0 -0
  21. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/__cli__.py +0 -0
  22. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/ch_utils/constants.py +0 -0
  23. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/ch_utils/engine.py +0 -0
  24. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/check_pypi.py +0 -0
  25. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/client.py +0 -0
  26. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/config.py +0 -0
  27. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/connectors.py +0 -0
  28. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/context.py +0 -0
  29. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/datafile/exceptions.py +0 -0
  30. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/datafile/parse_connection.py +0 -0
  31. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/datafile/parse_datasource.py +0 -0
  32. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/datafile/parse_pipe.py +0 -0
  33. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/datatypes.py +0 -0
  34. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/feedback_manager.py +0 -0
  35. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/git_settings.py +0 -0
  36. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/prompts.py +0 -0
  37. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/sql.py +0 -0
  38. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/sql_template.py +0 -0
  39. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/sql_template_fmt.py +0 -0
  40. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/sql_toolset.py +0 -0
  41. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/syncasync.py +0 -0
  42. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/tb/check_pypi.py +0 -0
  43. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/tb/cli.py +0 -0
  44. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/tb/client.py +0 -0
  45. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/tb/config.py +0 -0
  46. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/tb/modules/agent/__init__.py +0 -0
  47. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/tb/modules/agent/animations.py +0 -0
  48. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/tb/modules/agent/memory.py +0 -0
  49. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/tb/modules/agent/models.py +0 -0
  50. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/tb/modules/agent/tools/__init__.py +0 -0
  51. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/tb/modules/agent/tools/analyze.py +0 -0
  52. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/tb/modules/agent/tools/append.py +0 -0
  53. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/tb/modules/agent/tools/build.py +0 -0
  54. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/tb/modules/agent/tools/deploy.py +0 -0
  55. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/tb/modules/agent/tools/deploy_check.py +0 -0
  56. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/tb/modules/agent/tools/diff_resource.py +0 -0
  57. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/tb/modules/agent/tools/explore.py +0 -0
  58. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/tb/modules/agent/tools/get_endpoint_stats.py +0 -0
  59. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/tb/modules/agent/tools/get_openapi_definition.py +0 -0
  60. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/tb/modules/agent/tools/mock.py +0 -0
  61. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/tb/modules/agent/tools/plan.py +0 -0
  62. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/tb/modules/agent/tools/preview_datafile.py +0 -0
  63. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/tb/modules/agent/tools/request_endpoint.py +0 -0
  64. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/tb/modules/build.py +0 -0
  65. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/tb/modules/build_common.py +0 -0
  66. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/tb/modules/cicd.py +0 -0
  67. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/tb/modules/common.py +0 -0
  68. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/tb/modules/config.py +0 -0
  69. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/tb/modules/connection.py +0 -0
  70. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/tb/modules/copy.py +0 -0
  71. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/tb/modules/create.py +0 -0
  72. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/tb/modules/datafile/build.py +0 -0
  73. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/tb/modules/datafile/build_common.py +0 -0
  74. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/tb/modules/datafile/build_datasource.py +0 -0
  75. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/tb/modules/datafile/build_pipe.py +0 -0
  76. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/tb/modules/datafile/diff.py +0 -0
  77. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/tb/modules/datafile/fixture.py +0 -0
  78. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/tb/modules/datafile/format_common.py +0 -0
  79. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/tb/modules/datafile/format_datasource.py +0 -0
  80. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/tb/modules/datafile/format_pipe.py +0 -0
  81. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/tb/modules/datafile/pipe_checker.py +0 -0
  82. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/tb/modules/datafile/playground.py +0 -0
  83. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/tb/modules/datafile/pull.py +0 -0
  84. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/tb/modules/deployment.py +0 -0
  85. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/tb/modules/deployment_common.py +0 -0
  86. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/tb/modules/deprecations.py +0 -0
  87. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/tb/modules/dev_server.py +0 -0
  88. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/tb/modules/endpoint.py +0 -0
  89. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/tb/modules/exceptions.py +0 -0
  90. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/tb/modules/feedback_manager.py +0 -0
  91. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/tb/modules/info.py +0 -0
  92. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/tb/modules/infra.py +0 -0
  93. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/tb/modules/job.py +0 -0
  94. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/tb/modules/llm.py +0 -0
  95. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/tb/modules/llm_utils.py +0 -0
  96. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/tb/modules/local.py +0 -0
  97. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/tb/modules/local_common.py +0 -0
  98. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/tb/modules/login.py +0 -0
  99. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/tb/modules/login_common.py +0 -0
  100. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/tb/modules/logout.py +0 -0
  101. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/tb/modules/materialization.py +0 -0
  102. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/tb/modules/mock.py +0 -0
  103. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/tb/modules/mock_common.py +0 -0
  104. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/tb/modules/open.py +0 -0
  105. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/tb/modules/pipe.py +0 -0
  106. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/tb/modules/project.py +0 -0
  107. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/tb/modules/regions.py +0 -0
  108. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/tb/modules/secret.py +0 -0
  109. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/tb/modules/secret_common.py +0 -0
  110. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/tb/modules/shell.py +0 -0
  111. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/tb/modules/sink.py +0 -0
  112. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/tb/modules/table.py +0 -0
  113. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/tb/modules/telemetry.py +0 -0
  114. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/tb/modules/test.py +0 -0
  115. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/tb/modules/tinyunit/tinyunit.py +0 -0
  116. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/tb/modules/tinyunit/tinyunit_lib.py +0 -0
  117. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/tb/modules/token.py +0 -0
  118. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/tb/modules/watch.py +0 -0
  119. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/tb/modules/workspace.py +0 -0
  120. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/tb/modules/workspace_members.py +0 -0
  121. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/tb_cli.py +0 -0
  122. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/tb_cli_modules/auth.py +0 -0
  123. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/tb_cli_modules/branch.py +0 -0
  124. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/tb_cli_modules/cicd.py +0 -0
  125. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/tb_cli_modules/cli.py +0 -0
  126. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/tb_cli_modules/common.py +0 -0
  127. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/tb_cli_modules/config.py +0 -0
  128. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/tb_cli_modules/connection.py +0 -0
  129. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/tb_cli_modules/datasource.py +0 -0
  130. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/tb_cli_modules/exceptions.py +0 -0
  131. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/tb_cli_modules/fmt.py +0 -0
  132. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/tb_cli_modules/job.py +0 -0
  133. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/tb_cli_modules/pipe.py +0 -0
  134. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/tb_cli_modules/regions.py +0 -0
  135. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/tb_cli_modules/tag.py +0 -0
  136. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/tb_cli_modules/telemetry.py +0 -0
  137. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/tb_cli_modules/test.py +0 -0
  138. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/tb_cli_modules/tinyunit/tinyunit.py +0 -0
  139. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/tb_cli_modules/tinyunit/tinyunit_lib.py +0 -0
  140. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/tb_cli_modules/workspace.py +0 -0
  141. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/tb_cli_modules/workspace_members.py +0 -0
  142. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird/tornado_template.py +0 -0
  143. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird.egg-info/dependency_links.txt +0 -0
  144. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird.egg-info/entry_points.txt +0 -0
  145. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird.egg-info/requires.txt +0 -0
  146. {tinybird-0.0.1.dev262 → tinybird-0.0.1.dev263}/tinybird.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: tinybird
3
- Version: 0.0.1.dev262
3
+ Version: 0.0.1.dev263
4
4
  Summary: Tinybird Command Line Tool
5
5
  Home-page: https://www.tinybird.co/docs/forward/commands
6
6
  Author: Tinybird
@@ -461,6 +461,15 @@ class Datafile:
461
461
  raise DatafileValidationError(
462
462
  f"Invalid permission {token['permission']} for token {token['token_name']}. Only READ and APPEND are allowed for datasources"
463
463
  )
464
+
465
+ # Validate sorting key if present
466
+ if "engine" in node and isinstance(node["engine"], dict) and "args" in node["engine"]:
467
+ for arg_name, arg_value in node["engine"]["args"]:
468
+ if arg_name.lower() == "sorting_key":
469
+ # Check for sorting key constraints
470
+ self._validate_sorting_key(arg_value, node)
471
+ break
472
+
464
473
  # Validate Kafka params
465
474
  if any(param in node for param in KAFKA_PARAMS) and (
466
475
  missing := [param for param in REQUIRED_KAFKA_PARAMS if param not in node]
@@ -483,6 +492,148 @@ class Datafile:
483
492
  # We cannot validate a datafile whose kind is unknown
484
493
  pass
485
494
 
495
+ def _validate_sorting_key(self, sorting_key: str, node: Dict[str, Any]) -> None:
496
+ """
497
+ Validates that a sorting key doesn't reference:
498
+ - Nullable columns
499
+ - AggregateFunction types
500
+ - Engine version columns for ReplacingMergeTree
501
+ """
502
+ if sorting_key == "tuple()" or not sorting_key:
503
+ return # Empty sorting key is valid
504
+
505
+ engine_ver_column = self._extract_engine_ver_column(node)
506
+ schema_columns = {col["name"]: col for col in node["columns"]}
507
+ sorting_key_columns = self._parse_sorting_key_columns(sorting_key, engine_ver_column)
508
+
509
+ self._validate_columns_against_schema(sorting_key_columns, schema_columns)
510
+
511
+ def _extract_engine_ver_column(self, node: Dict[str, Any]) -> Optional[str]:
512
+ engine_info = node.get("engine", {})
513
+
514
+ if not isinstance(engine_info, dict):
515
+ return None
516
+
517
+ engine_type = engine_info.get("type", "")
518
+ if engine_type != "ReplacingMergeTree":
519
+ return None
520
+
521
+ engine_args = engine_info.get("args", [])
522
+ for arg_name, arg_value in engine_args:
523
+ if arg_name == "ver":
524
+ return arg_value
525
+
526
+ return None
527
+
528
+ def _parse_sorting_key_columns(self, sorting_key: str, engine_ver_column: Optional[str]) -> List[str]:
529
+ """Parse sorting key to extract column names and validate constraints."""
530
+ # Validate ENGINE_VER column constraint early
531
+ if engine_ver_column and engine_ver_column in sorting_key:
532
+ raise DatafileValidationError(
533
+ f"ENGINE_VER column '{engine_ver_column}' cannot be included in the sorting key for ReplacingMergeTree. "
534
+ f"Including the version column in the sorting key prevents deduplication because rows with different "
535
+ f"versions will have different sorting keys and won't be considered duplicates. The sorting key should "
536
+ f"define the record identity (what makes it unique), while ENGINE_VER tracks which version to keep."
537
+ )
538
+
539
+ # Remove tuple() wrapper if present
540
+ column_str = sorting_key
541
+ if column_str.startswith("tuple(") and column_str.endswith(")"):
542
+ column_str = column_str[6:-1]
543
+
544
+ sorting_key_columns = []
545
+
546
+ for part in column_str.split(","):
547
+ part = part.strip()
548
+
549
+ if self._is_aggregate_function_expression(part):
550
+ raise DatafileValidationError(
551
+ f"Sorting key contains aggregate function expression '{part}'. Aggregate function expressions cannot be used in sorting keys."
552
+ )
553
+
554
+ # Extract column names from the part
555
+ extracted_columns = self._extract_column_names_from_part(part)
556
+ sorting_key_columns.extend(extracted_columns)
557
+
558
+ return sorting_key_columns
559
+
560
+ def _is_aggregate_function_expression(self, part: str) -> bool:
561
+ """Check if a sorting key part is an aggregate function expression."""
562
+ if not ("(" in part and part.endswith(")")):
563
+ return False
564
+
565
+ func_start = part.find("(")
566
+ func_name = part[:func_start].strip().lower()
567
+
568
+ aggregate_function_names = {
569
+ "sum",
570
+ "count",
571
+ "avg",
572
+ "min",
573
+ "max",
574
+ "any",
575
+ "grouparray",
576
+ "groupuniqarray",
577
+ "uniq",
578
+ "summerge",
579
+ "countmerge",
580
+ "avgmerge",
581
+ "minmerge",
582
+ "maxmerge",
583
+ "anymerge",
584
+ "grouparraymerge",
585
+ "groupuniqarraymerge",
586
+ "uniqmerge",
587
+ }
588
+
589
+ return func_name in aggregate_function_names
590
+
591
+ def _extract_column_names_from_part(self, part: str) -> List[str]:
592
+ """Extract column names from a sorting key part."""
593
+ columns = []
594
+
595
+ if "(" in part and part.endswith(")"):
596
+ # Function expression - extract column names from inside parentheses
597
+ func_start = part.find("(")
598
+ inner_content = part[func_start + 1 : -1].strip()
599
+ for inner_part in inner_content.split(","):
600
+ inner_part = inner_part.strip().strip("`")
601
+ if inner_part and inner_part.isidentifier():
602
+ columns.append(inner_part)
603
+ elif part:
604
+ # Simple column name
605
+ column_name = part.strip("`")
606
+ if column_name:
607
+ columns.append(column_name)
608
+
609
+ return columns
610
+
611
+ def _validate_columns_against_schema(
612
+ self, sorting_key_columns: List[str], schema_columns: Dict[str, Dict[str, Any]]
613
+ ) -> None:
614
+ """Validate each column in the sorting key against the schema."""
615
+ if not schema_columns:
616
+ return # No schema information available, can't validate
617
+
618
+ for col_name in sorting_key_columns:
619
+ if col_name not in schema_columns:
620
+ continue
621
+
622
+ self._validate_single_column(col_name, schema_columns[col_name])
623
+
624
+ def _validate_single_column(self, col_name: str, column_info: Dict[str, Any]) -> None:
625
+ """Validate a single column for use in sorting keys."""
626
+ col_type = column_info.get("type", "").lower()
627
+ is_nullable = column_info.get("nullable", False)
628
+ if is_nullable:
629
+ raise DatafileValidationError(
630
+ f"Sorting key contains nullable column '{col_name}'. Nullable columns cannot be used in sorting keys."
631
+ )
632
+ if "aggregatefunction" in col_type:
633
+ raise DatafileValidationError(
634
+ f"Sorting key contains column '{col_name}' with AggregateFunction type. AggregateFunction columns cannot be used in sorting keys."
635
+ )
636
+
486
637
 
487
638
  def format_filename(filename: str, hide_folders: bool = False):
488
639
  return os.path.basename(filename) if hide_folders else filename
@@ -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.dev262'
8
- __revision__ = 'b28ec2c'
7
+ __version__ = '0.0.1.dev263'
8
+ __revision__ = '1d13423'
@@ -2,6 +2,7 @@ import asyncio
2
2
  import shlex
3
3
  import subprocess
4
4
  import sys
5
+ import urllib.parse
5
6
  from functools import partial
6
7
  from pathlib import Path
7
8
  from typing import Any, Optional
@@ -11,13 +12,16 @@ import humanfriendly
11
12
  from pydantic_ai import Agent, RunContext, Tool
12
13
  from pydantic_ai.agent import AgentRunResult
13
14
  from pydantic_ai.messages import ModelMessage, ModelRequest, UserPromptPart
15
+ from requests import Response
14
16
 
15
17
  from tinybird.tb.client import TinyB
16
18
  from tinybird.tb.modules.agent.animations import ThinkingAnimation
17
19
  from tinybird.tb.modules.agent.banner import display_banner
20
+ from tinybird.tb.modules.agent.command_agent import CommandAgent
18
21
  from tinybird.tb.modules.agent.memory import clear_history, clear_messages, load_messages, save_messages
19
22
  from tinybird.tb.modules.agent.models import create_model, model_costs
20
23
  from tinybird.tb.modules.agent.prompts import agent_system_prompt, load_custom_project_rules, resources_prompt
24
+ from tinybird.tb.modules.agent.testing_agent import TestingAgent
21
25
  from tinybird.tb.modules.agent.tools.analyze import analyze_file, analyze_url
22
26
  from tinybird.tb.modules.agent.tools.append import append_file, append_url
23
27
  from tinybird.tb.modules.agent.tools.build import build
@@ -32,9 +36,7 @@ from tinybird.tb.modules.agent.tools.mock import mock
32
36
  from tinybird.tb.modules.agent.tools.plan import plan
33
37
  from tinybird.tb.modules.agent.tools.preview_datafile import preview_datafile
34
38
  from tinybird.tb.modules.agent.tools.request_endpoint import request_endpoint
35
- from tinybird.tb.modules.agent.tools.test import create_tests as create_tests_tool
36
- from tinybird.tb.modules.agent.tools.test import run_tests as run_tests_tool
37
- from tinybird.tb.modules.agent.utils import AgentRunCancelled, TinybirdAgentContext, show_input
39
+ from tinybird.tb.modules.agent.utils import AgentRunCancelled, TinybirdAgentContext, show_confirmation, show_input
38
40
  from tinybird.tb.modules.build_common import process as build_process
39
41
  from tinybird.tb.modules.common import _analyze, _get_tb_client, echo_safe_humanfriendly_tables_format_pretty_table
40
42
  from tinybird.tb.modules.config import CLIConfig
@@ -64,6 +66,7 @@ class TinybirdAgent:
64
66
  self.host = host
65
67
  self.dangerously_skip_permissions = dangerously_skip_permissions or prompt_mode
66
68
  self.project = project
69
+ self.thinking_animation = ThinkingAnimation()
67
70
  if prompt_mode:
68
71
  self.messages: list[ModelMessage] = load_messages()[-5:]
69
72
  else:
@@ -102,12 +105,61 @@ class TinybirdAgent:
102
105
  Tool(execute_query, docstring_format="google", require_parameter_descriptions=True, takes_ctx=True),
103
106
  Tool(request_endpoint, docstring_format="google", require_parameter_descriptions=True, takes_ctx=True),
104
107
  Tool(diff_resource, docstring_format="google", require_parameter_descriptions=True, takes_ctx=True),
105
- Tool(create_tests_tool, docstring_format="google", require_parameter_descriptions=True, takes_ctx=True),
106
- Tool(run_tests_tool, docstring_format="google", require_parameter_descriptions=True, takes_ctx=True),
107
108
  ],
108
109
  history_processors=[self._context_aware_processor],
109
110
  )
110
111
 
112
+ self.testing_agent = TestingAgent(
113
+ dangerously_skip_permissions=self.dangerously_skip_permissions,
114
+ prompt_mode=prompt_mode,
115
+ thinking_animation=self.thinking_animation,
116
+ token=self.token,
117
+ user_token=self.user_token,
118
+ host=self.host,
119
+ workspace_id=workspace_id,
120
+ project=self.project,
121
+ )
122
+ self.command_agent = CommandAgent(
123
+ dangerously_skip_permissions=self.dangerously_skip_permissions,
124
+ prompt_mode=prompt_mode,
125
+ thinking_animation=self.thinking_animation,
126
+ token=self.token,
127
+ user_token=self.user_token,
128
+ host=self.host,
129
+ workspace_id=workspace_id,
130
+ project=self.project,
131
+ )
132
+
133
+ @self.agent.tool
134
+ def manage_tests(ctx: RunContext[TinybirdAgentContext], task: str) -> str:
135
+ """Delegate test management to the test agent:
136
+
137
+ Args:
138
+ task (str): The detailed task to perform. Required.
139
+
140
+ Returns:
141
+ str: The result of the query.
142
+ """
143
+ result = self.testing_agent.run(task, deps=ctx.deps, usage=ctx.usage)
144
+
145
+ if not result:
146
+ return "Could not solve the task using the test agent"
147
+
148
+ return result.output
149
+
150
+ @self.agent.tool
151
+ def run_command(ctx: RunContext[TinybirdAgentContext], task: str) -> str:
152
+ """Solve a task using directly Tinybird CLI commands.
153
+
154
+ Args:
155
+ task (str): The task to solve. Required.
156
+
157
+ Returns:
158
+ str: The result of the command.
159
+ """
160
+ result = self.command_agent.run(task, deps=ctx.deps, usage=ctx.usage)
161
+ return result.output
162
+
111
163
  @self.agent.instructions
112
164
  def get_local_host(ctx: RunContext[TinybirdAgentContext]) -> str:
113
165
  return f"Tinybird Local host: {ctx.deps.local_host}"
@@ -128,8 +180,6 @@ class TinybirdAgent:
128
180
  def get_project_files(ctx: RunContext[TinybirdAgentContext]) -> str:
129
181
  return resources_prompt(self.project)
130
182
 
131
- self.thinking_animation = ThinkingAnimation()
132
-
133
183
  def add_message(self, message: ModelMessage) -> None:
134
184
  self.messages.append(message)
135
185
 
@@ -155,6 +205,7 @@ class TinybirdAgent:
155
205
  project = self.project
156
206
  folder = self.project.folder
157
207
  local_client = get_tinybird_local_client(config, test=False, silent=False)
208
+ test_client = get_tinybird_local_client(config, test=True, silent=True)
158
209
  return TinybirdAgentContext(
159
210
  # context does not support the whole client, so we need to pass only the functions we need
160
211
  explore_data=client.explore_data,
@@ -169,6 +220,8 @@ class TinybirdAgent:
169
220
  execute_query_local=partial(execute_query_local, config=config),
170
221
  request_endpoint_cloud=partial(request_endpoint_cloud, config=config),
171
222
  request_endpoint_local=partial(request_endpoint_local, config=config),
223
+ build_project_test=partial(build_project_test, project=project, client=test_client),
224
+ get_pipe_data_test=partial(get_pipe_data_test, client=test_client),
172
225
  get_datasource_datafile_cloud=partial(get_datasource_datafile_cloud, config=config),
173
226
  get_datasource_datafile_local=partial(get_datasource_datafile_local, config=config),
174
227
  get_pipe_datafile_cloud=partial(get_pipe_datafile_cloud, config=config),
@@ -176,7 +229,7 @@ class TinybirdAgent:
176
229
  get_connection_datafile_cloud=partial(get_connection_datafile_cloud, config=config),
177
230
  get_connection_datafile_local=partial(get_connection_datafile_local, config=config),
178
231
  get_project_files=project.get_project_files,
179
- run_tests=partial(run_tests, project=project, config=config),
232
+ run_tests=partial(run_tests, project=project, client=test_client),
180
233
  folder=folder,
181
234
  thinking_animation=self.thinking_animation,
182
235
  workspace_name=self.project.workspace_name,
@@ -277,15 +330,40 @@ def run_agent(
277
330
  FeedbackManager.error(message="Tinybird Code requires authentication. Run 'tb login' first.")
278
331
  )
279
332
  return
333
+ build_user_input: Optional[str] = None
334
+ try:
335
+ build_project(config, project, test=False, silent=True)
336
+ except CLIBuildException as e:
337
+ if prompt:
338
+ raise e
339
+ click.echo(FeedbackManager.error(message=e))
340
+ try:
341
+ show_confirmation(
342
+ title="Fix project errors?", skip_confirmation=dangerously_skip_permissions, show_review=False
343
+ )
344
+ except AgentRunCancelled:
345
+ click.echo(FeedbackManager.info(message="User cancelled the operation"))
346
+ return
280
347
 
281
- build_project(config, project, test=False, silent=True)
348
+ build_user_input = f"Error building project. Fix the errors before continuing. {e}"
282
349
 
283
350
  # In prompt mode, always skip permissions to avoid interactive prompts
284
351
  prompt_mode = prompt is not None
285
- agent = TinybirdAgent(token, user_token, host, workspace_id, project, dangerously_skip_permissions, prompt_mode)
352
+
353
+ agent = TinybirdAgent(
354
+ token,
355
+ user_token,
356
+ host,
357
+ workspace_id,
358
+ project,
359
+ dangerously_skip_permissions,
360
+ prompt_mode,
361
+ )
286
362
 
287
363
  # Print mode: run once with the provided prompt and exit
288
364
  if prompt:
365
+ if build_user_input:
366
+ prompt = f"User input: {prompt}\n\n{build_user_input}"
289
367
  agent.run(prompt, config)
290
368
  return
291
369
 
@@ -302,7 +380,8 @@ def run_agent(
302
380
  try:
303
381
  while True:
304
382
  try:
305
- user_input = show_input(workspace_name)
383
+ user_input = build_user_input or show_input(workspace_name)
384
+ build_user_input = None
306
385
  if user_input.startswith("tb "):
307
386
  cmd_parts = shlex.split(user_input)
308
387
  subprocess.run(cmd_parts)
@@ -373,12 +452,12 @@ def run_agent(
373
452
 
374
453
 
375
454
  def build_project(
376
- config: dict[str, Any], project: Project, silent: bool = True, test: bool = True, load_fixtures: bool = False
455
+ config: dict[str, Any], project: Project, silent: bool = False, test: bool = True, load_fixtures: bool = False
377
456
  ) -> None:
378
- local_client = get_tinybird_local_client(config, test=test, silent=silent)
457
+ client = get_tinybird_local_client(config, test=test, silent=silent)
379
458
  build_error = build_process(
380
459
  project=project,
381
- tb_client=local_client,
460
+ tb_client=client,
382
461
  watch=False,
383
462
  silent=silent,
384
463
  exit_on_error=False,
@@ -388,6 +467,23 @@ def build_project(
388
467
  raise CLIBuildException(build_error)
389
468
 
390
469
 
470
+ def build_project_test(
471
+ client: TinyB,
472
+ project: Project,
473
+ silent: bool = False,
474
+ ) -> None:
475
+ build_error = build_process(
476
+ project=project,
477
+ tb_client=client,
478
+ watch=False,
479
+ silent=silent,
480
+ exit_on_error=False,
481
+ load_fixtures=True,
482
+ )
483
+ if build_error:
484
+ raise CLIBuildException(build_error)
485
+
486
+
391
487
  def deploy_project(config: dict[str, Any], project: Project) -> None:
392
488
  client = _get_tb_client(config["token"], config["host"])
393
489
  try:
@@ -408,6 +504,8 @@ def deploy_check_project(config: dict[str, Any], project: Project) -> None:
408
504
  try:
409
505
  create_deployment(project=project, client=client, config=config, check=True, wait=True, auto=True)
410
506
  except SystemExit as e:
507
+ if hasattr(e, "code") and e.code == 0:
508
+ return
411
509
  raise CLIDeploymentException(e.args[0])
412
510
 
413
511
 
@@ -529,9 +627,24 @@ def get_connection_datafile_local(config: dict[str, Any], connection_name: str)
529
627
  return "Connection not found"
530
628
 
531
629
 
532
- def run_tests(config: dict[str, Any], project: Project, pipe_name: Optional[str] = None) -> None:
533
- local_client = get_tinybird_local_client(config, test=True, silent=True)
630
+ def run_tests(client: TinyB, project: Project, pipe_name: Optional[str] = None) -> None:
534
631
  try:
535
- run_tests_common(name=(pipe_name,) if pipe_name else (), project=project, client=local_client)
632
+ run_tests_common(name=(pipe_name,) if pipe_name else (), project=project, client=client)
536
633
  except SystemExit as e:
537
634
  raise Exception(e.args[0])
635
+
636
+
637
+ def get_pipe_data_test(client: TinyB, pipe_name: str, test_params: Optional[dict[str, str]] = None) -> Response:
638
+ pipe = client._req(f"/v0/pipes/{pipe_name}")
639
+ output_node = next(
640
+ (node for node in pipe["nodes"] if node["node_type"] != "default" and node["node_type"] != "standard"),
641
+ {"name": "not_found"},
642
+ )
643
+ if output_node["node_type"] == "endpoint":
644
+ return client._req_raw(f"/v0/pipes/{pipe_name}.ndjson?{test_params}")
645
+
646
+ params = {
647
+ "q": output_node["sql"],
648
+ "pipeline": pipe_name,
649
+ }
650
+ return client._req_raw(f"""/v0/sql?{urllib.parse.urlencode(params)}&{test_params}""")
@@ -0,0 +1,131 @@
1
+ import os
2
+ import sys
3
+
4
+ import click
5
+
6
+
7
+ def detect_terminal_capabilities():
8
+ """Detect terminal color and Unicode capabilities"""
9
+ # Check for true color support
10
+ colorterm = os.environ.get("COLORTERM", "").lower()
11
+ term = os.environ.get("TERM", "").lower()
12
+ term_program = os.environ.get("TERM_PROGRAM", "").lower()
13
+
14
+ # Known terminals with good true color support
15
+ modern_terminals = ["warp", "ghostty", "iterm2", "alacritty", "kitty", "hyper"]
16
+
17
+ # Check for true color support
18
+ has_truecolor = (
19
+ colorterm in ["truecolor", "24bit"]
20
+ or term_program in modern_terminals
21
+ or "truecolor" in term
22
+ or "24bit" in term
23
+ )
24
+
25
+ # Check if it's standard macOS Terminal
26
+ is_macos_terminal = term_program == "apple_terminal"
27
+
28
+ # Check for Unicode support (most modern terminals support this)
29
+ has_unicode = sys.stdout.encoding and "utf" in sys.stdout.encoding.lower()
30
+
31
+ return {
32
+ "truecolor": has_truecolor and not is_macos_terminal,
33
+ "unicode": has_unicode,
34
+ "is_macos_terminal": is_macos_terminal,
35
+ }
36
+
37
+
38
+ def display_banner():
39
+ reset = "\033[0m"
40
+ capabilities = detect_terminal_capabilities()
41
+
42
+ click.echo("\n")
43
+
44
+ # Choose banner based on Unicode support
45
+ if capabilities["unicode"]:
46
+ # Unicode box-drawing characters banner
47
+ banner = [
48
+ " ████████╗██╗███╗ ██╗██╗ ██╗██████╗ ██╗██████╗ ██████╗ ██████╗ ██████╗ ██████╗ ███████╗",
49
+ " ╚══██╔══╝██║████╗ ██║╚██╗ ██╔╝██╔══██╗██║██╔══██╗██╔══██╗ ██╔════╝██╔═══██╗██╔══██╗██╔════╝",
50
+ " ██║ ██║██╔██╗ ██║ ╚████╔╝ ██████╔╝██║██████╔╝██║ ██║ ██║ ██║ ██║██║ ██║█████╗ ",
51
+ " ██║ ██║██║╚██╗██║ ╚██╔╝ ██╔══██╗██║██╔══██╗██║ ██║ ██║ ██║ ██║██║ ██║██╔══╝ ",
52
+ " ██║ ██║██║ ╚████║ ██║ ██████╔╝██║██║ ██║██████╔╝ ╚██████╗╚██████╔╝██████╔╝███████╗",
53
+ " ╚═╝ ╚═╝╚═╝ ╚═══╝ ╚═╝ ╚═════╝ ╚═╝╚═╝ ╚═╝╚═════╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝",
54
+ ]
55
+ else:
56
+ # ASCII fallback banner
57
+ banner = [
58
+ " ████████T██I███N ██N██ ██Y██████B ██I██████B ██████B ██████C ██████O ██████D ███████E",
59
+ " ╚══██╔══╝██║████╗ ██║╚██╗ ██╔╝██╔══██╗██║██╔══██╗██╔══██╗ ██╔════╝██╔═══██╗██╔══██╗██╔════╝",
60
+ " ██║ ██║██╔██╗ ██║ ╚████╔╝ ██████╔╝██║██████╔╝██║ ██║ ██║ ██║ ██║██║ ██║█████╗ ",
61
+ " ██║ ██║██║╚██╗██║ ╚██╔╝ ██╔══██╗██║██╔══██╗██║ ██║ ██║ ██║ ██║██║ ██║██╔══╝ ",
62
+ " ██║ ██║██║ ╚████║ ██║ ██████╔╝██║██║ ██║██████╔╝ ╚██████╗╚██████╔╝██████╔╝███████╗",
63
+ " ╚═╝ ╚═╝╚═╝ ╚═══╝ ╚═╝ ╚═════╝ ╚═╝╚═╝ ╚═╝╚═════╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝",
64
+ ]
65
+
66
+ def interpolate_color(start_rgb, end_rgb, factor):
67
+ """Interpolate between two RGB colors"""
68
+ return [int(start_rgb[i] + (end_rgb[i] - start_rgb[i]) * factor) for i in range(3)]
69
+
70
+ def rgb_to_ansi(r: int, g: int, b: int, use_truecolor: bool):
71
+ """Convert RGB values to ANSI escape code"""
72
+ if use_truecolor:
73
+ return f"\033[38;2;{r};{g};{b}m"
74
+ else:
75
+ # Convert to 8-bit color (256 color palette)
76
+ # Simple approximation: map RGB to 216-color cube + grayscale
77
+ if r == g == b:
78
+ # Grayscale
79
+ gray = int(r / 255 * 23) + 232
80
+ return f"\033[38;5;{gray}m"
81
+ else:
82
+ # Color cube (6x6x6)
83
+ r_idx = int(r / 255 * 5)
84
+ g_idx = int(g / 255 * 5)
85
+ b_idx = int(b / 255 * 5)
86
+ color_idx = 16 + (36 * r_idx) + (6 * g_idx) + b_idx
87
+ return f"\033[38;5;{color_idx}m"
88
+
89
+ # Define start and end colors for smooth gradient
90
+ start_color = [0, 128, 128] # Deep teal
91
+ end_color = [100, 190, 190] # Light turquoise (balanced green and blue)
92
+
93
+ # Print each line with gradient for modern terminals, solid color for limited terminals
94
+ for line in banner:
95
+ colored_line = ""
96
+
97
+ if capabilities["truecolor"]:
98
+ # Use gradient for modern terminals
99
+ non_space_chars = sum(1 for char in line if char != " ")
100
+ char_count = 0
101
+
102
+ for char in line:
103
+ if char == " ":
104
+ colored_line += char
105
+ continue
106
+
107
+ # Calculate smooth gradient position (0.0 to 1.0)
108
+ if non_space_chars > 1:
109
+ gradient_position = char_count / (non_space_chars - 1)
110
+ else:
111
+ gradient_position = 0
112
+
113
+ # Interpolate color
114
+ current_rgb = interpolate_color(start_color, end_color, gradient_position)
115
+ color_code = rgb_to_ansi(*current_rgb, use_truecolor=True) # type: ignore
116
+
117
+ colored_line += f"{color_code}{char}"
118
+ char_count += 1
119
+ else:
120
+ # Use solid color for limited terminals (like macOS Terminal)
121
+ solid_color = start_color # Use the deep teal consistently
122
+ color_code = rgb_to_ansi(*solid_color, use_truecolor=False) # type: ignore
123
+
124
+ for char in line:
125
+ if char == " ":
126
+ colored_line += char
127
+ else:
128
+ colored_line += f"{color_code}{char}"
129
+
130
+ click.echo(colored_line + reset)
131
+ click.echo()
@@ -0,0 +1,59 @@
1
+ from pydantic_ai import Agent, RunContext, Tool
2
+ from pydantic_ai.messages import ModelMessage
3
+ from pydantic_ai.usage import Usage
4
+
5
+ from tinybird.tb.modules.agent.animations import ThinkingAnimation
6
+ from tinybird.tb.modules.agent.models import create_model
7
+ from tinybird.tb.modules.agent.prompts import tests_files_prompt
8
+ from tinybird.tb.modules.agent.tools.run_command import run_command
9
+ from tinybird.tb.modules.agent.utils import TinybirdAgentContext
10
+ from tinybird.tb.modules.project import Project
11
+
12
+
13
+ class CommandAgent:
14
+ def __init__(
15
+ self,
16
+ token: str,
17
+ user_token: str,
18
+ host: str,
19
+ workspace_id: str,
20
+ project: Project,
21
+ dangerously_skip_permissions: bool,
22
+ prompt_mode: bool,
23
+ thinking_animation: ThinkingAnimation,
24
+ ):
25
+ self.token = token
26
+ self.user_token = user_token
27
+ self.host = host
28
+ self.dangerously_skip_permissions = dangerously_skip_permissions or prompt_mode
29
+ self.project = project
30
+ self.thinking_animation = thinking_animation
31
+ self.messages: list[ModelMessage] = []
32
+ self.agent = Agent(
33
+ model=create_model(user_token, host, workspace_id),
34
+ deps_type=TinybirdAgentContext,
35
+ instructions=[
36
+ """
37
+ You are part of Tinybird Code, an agentic CLI that can help users to work with Tinybird.
38
+ You are a sub-agent of the main Tinybird Code agent. You are responsible for running commands on the user's machine.
39
+ You will be given a task to perform and you will use `run_command` tool to complete it.
40
+ If you do not find a command that can solve the task, just say that there is no command that can solve the task.
41
+ You can run `-h` in every level of the command to get help. E.g. `tb -h`, `tb datasource -h`, `tb datasource ls -h`.
42
+ When you need to access Tinybird Cloud, add the `--cloud` flag. E.g. `tb --cloud datasource ls`.
43
+ Token and host are not required to add to the commands.
44
+ Always run first help commands to be sure that the commands you are running is not interactive.
45
+ """,
46
+ ],
47
+ tools=[
48
+ Tool(run_command, docstring_format="google", require_parameter_descriptions=True, takes_ctx=True),
49
+ ],
50
+ )
51
+
52
+ @self.agent.instructions
53
+ def get_tests_files(ctx: RunContext[TinybirdAgentContext]) -> str:
54
+ return tests_files_prompt(self.project)
55
+
56
+ def run(self, task: str, deps: TinybirdAgentContext, usage: Usage):
57
+ result = self.agent.run_sync(task, deps=deps, usage=usage, message_history=self.messages)
58
+ self.messages.extend(result.new_messages())
59
+ return result