tinybird 0.0.1.dev255__py3-none-any.whl → 0.0.1.dev257__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.

@@ -1744,6 +1744,8 @@ def parse(
1744
1744
  def _f(*args: str, **kwargs: Any):
1745
1745
  __init_engine(f"ENGINE_{v}".upper())
1746
1746
  engine_arg = eval_var(_unquote((" ".join(args)).strip()), skip=skip_eval)
1747
+ if v.lower() == "ttl" and not engine_arg:
1748
+ return
1747
1749
  parser_state.current_node["engine"]["args"].append((v, engine_arg))
1748
1750
 
1749
1751
  return _f
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.dev255'
8
- __revision__ = '208306e'
7
+ __version__ = '0.0.1.dev257'
8
+ __revision__ = '249ae8d'
@@ -1,41 +1,23 @@
1
+ import asyncio
1
2
  import shlex
2
3
  import subprocess
3
4
  import sys
4
- from datetime import datetime
5
5
  from functools import partial
6
6
  from pathlib import Path
7
7
  from typing import Any, Optional
8
8
 
9
9
  import click
10
- from pydantic_ai import Agent, Tool
10
+ import humanfriendly
11
+ from pydantic_ai import Agent, RunContext, Tool
12
+ from pydantic_ai.agent import AgentRunResult
11
13
  from pydantic_ai.messages import ModelMessage
12
14
 
13
- from tinybird.prompts import (
14
- connection_instructions,
15
- copy_pipe_instructions,
16
- datasource_example,
17
- datasource_instructions,
18
- gcs_connection_example,
19
- kafka_connection_example,
20
- materialized_pipe_instructions,
21
- pipe_example,
22
- pipe_instructions,
23
- s3_connection_example,
24
- sink_pipe_instructions,
25
- )
26
15
  from tinybird.tb.client import TinyB
27
16
  from tinybird.tb.modules.agent.animations import ThinkingAnimation
28
17
  from tinybird.tb.modules.agent.banner import display_banner
29
- from tinybird.tb.modules.agent.memory import clear_history
18
+ from tinybird.tb.modules.agent.memory import clear_history, clear_messages, load_messages, save_messages
30
19
  from tinybird.tb.modules.agent.models import create_model, model_costs
31
- from tinybird.tb.modules.agent.prompts import (
32
- datafile_instructions,
33
- endpoint_optimization_instructions,
34
- plan_instructions,
35
- resources_prompt,
36
- sql_agent_instructions,
37
- sql_instructions,
38
- )
20
+ from tinybird.tb.modules.agent.prompts import agent_system_prompt, resources_prompt
39
21
  from tinybird.tb.modules.agent.tools.analyze import analyze_file, analyze_url
40
22
  from tinybird.tb.modules.agent.tools.append import append_file, append_url
41
23
  from tinybird.tb.modules.agent.tools.build import build
@@ -52,10 +34,10 @@ from tinybird.tb.modules.agent.tools.preview_datafile import preview_datafile
52
34
  from tinybird.tb.modules.agent.tools.request_endpoint import request_endpoint
53
35
  from tinybird.tb.modules.agent.utils import TinybirdAgentContext, show_input
54
36
  from tinybird.tb.modules.build_common import process as build_process
55
- from tinybird.tb.modules.common import _analyze, _get_tb_client
37
+ from tinybird.tb.modules.common import _analyze, _get_tb_client, echo_safe_humanfriendly_tables_format_pretty_table
56
38
  from tinybird.tb.modules.config import CLIConfig
57
39
  from tinybird.tb.modules.deployment_common import create_deployment
58
- from tinybird.tb.modules.exceptions import CLIBuildException, CLIMockException
40
+ from tinybird.tb.modules.exceptions import CLIBuildException, CLIDeploymentException, CLIMockException
59
41
  from tinybird.tb.modules.feedback_manager import FeedbackManager
60
42
  from tinybird.tb.modules.local_common import get_tinybird_local_client
61
43
  from tinybird.tb.modules.login_common import login
@@ -72,130 +54,21 @@ class TinybirdAgent:
72
54
  workspace_id: str,
73
55
  project: Project,
74
56
  dangerously_skip_permissions: bool,
57
+ prompt_mode: bool,
75
58
  ):
76
59
  self.token = token
77
60
  self.user_token = user_token
78
61
  self.host = host
79
- self.dangerously_skip_permissions = dangerously_skip_permissions
62
+ self.dangerously_skip_permissions = dangerously_skip_permissions or prompt_mode
80
63
  self.project = project
81
- self.messages: list[ModelMessage] = []
64
+ if prompt_mode:
65
+ self.messages: list[ModelMessage] = load_messages()[-5:]
66
+ else:
67
+ self.messages = []
82
68
  self.agent = Agent(
83
69
  model=create_model(user_token, host, workspace_id),
84
70
  deps_type=TinybirdAgentContext,
85
- system_prompt=f"""
86
- You are a Tinybird Code, an agentic CLI that can help users to work with Tinybird.
87
-
88
- You are an interactive CLI tool that helps users with data engineering tasks. Use the instructions below and the tools available to you to assist the user.
89
-
90
- # Tone and style
91
- You should be concise, direct, and to the point.
92
- Remember that your output will be displayed on a command line interface. Your responses can use Github-flavored markdown for formatting. Do not use emojis.
93
- Output text to communicate with the user; all text you output outside of tool use is displayed to the user. Only use tools to complete tasks. Never use tools like Bash or code comments as means to communicate with the user during the session.
94
- If you cannot or will not help the user with something, please do not say why or what it could lead to, since this comes across as preachy and annoying. Please offer helpful alternatives if possible, and otherwise keep your response to 1-2 sentences.
95
- IMPORTANT: You should minimize output tokens as much as possible while maintaining helpfulness, quality, and accuracy. Only address the specific query or task at hand, avoiding tangential information unless absolutely critical for completing the request. If you can answer in 1-3 sentences or a short paragraph, please do.
96
- IMPORTANT: You should NOT answer with unnecessary preamble or postamble (such as explaining your code or summarizing your action), unless the user asks you to.
97
- IMPORTANT: Keep your responses short, since they will be displayed on a command line interface. You MUST answer concisely with fewer than 4 lines (not including tool use or code generation), unless user asks for detail. Answer the user's question directly, without elaboration, explanation, or details. One word answers are best. Avoid introductions, conclusions, and explanations. You MUST avoid text before/after your response, such as "The answer is <answer>.", "Here is the content of the file..." or "Based on the information provided, the answer is..." or "Here is what I will do next...". Here are some examples to demonstrate appropriate verbosity:
98
-
99
- # Proactiveness
100
- You are allowed to be proactive, but only when the user asks you to do something. You should strive to strike a balance between:
101
- Doing the right thing when asked, including taking actions and follow-up actions
102
- Not surprising the user with actions you take without asking
103
- For example, if the user asks you how to approach something, you should do your best to answer their question first, and not immediately jump into taking actions.
104
- Do not add additional code explanation summary unless requested by the user. After working on a file, just stop, rather than providing an explanation of what you did.
105
-
106
- # Code style
107
- IMPORTANT: DO NOT ADD ANY COMMENTS unless asked by the user.
108
-
109
- # Tools
110
- You have access to the following tools:
111
- 1. `preview_datafile` - Preview the content of a datafile (datasource, endpoint, materialized, sink, copy, connection).
112
- 2. `create_datafile` - Create a file in the project folder. Confirmation will be asked by the tool before creating the file.
113
- 3. `plan` - Plan the creation or update of resources.
114
- 4. `build` - Build the project.
115
- 5. `deploy` - Deploy the project to Tinybird Cloud.
116
- 6. `deploy_check` - Check if the project can be deployed to Tinybird Cloud before deploying it.
117
- 7. `mock` - Create mock data for a landing datasource.
118
- 8. `analyze_file` - Analyze the content of a fixture file present in the project folder.
119
- 9. `analyze_url` - Analyze the content of an external url.
120
- 9. `append_file` - Append a file present in the project to a datasource.
121
- 10. `append_url` - Append an external url to a datasource.
122
- 11. `get_endpoint_stats` - Get metrics of the requests to an endpoint.
123
- 12. `get_openapi_definition` - Get the OpenAPI definition for all endpoints that are built/deployed to Tinybird Cloud or Local.
124
- 13. `execute_query` - Execute a query against Tinybird Cloud or Local.
125
- 13. `request_endpoint` - Request an endpoint against Tinybird Cloud or Local.
126
- 14. `diff_resource` - Diff the content of a resource in Tinybird Cloud vs Tinybird Local vs Project local file.
127
-
128
- # When creating or updating datafiles:
129
- 1. Use `plan` tool to plan the creation or update of resources.
130
- 2. If the user confirms the plan, go from 3 to 7 steps until all the resources are created, updated or skipped.
131
- 3. Use `preview_datafile` tool to preview the content of a datafile.
132
- 4. Without asking, use the `create_datafile` tool to create the datafile, because it will ask for confirmation before creating the file.
133
- 5. Check the result of the `create_datafile` tool to see if the datafile was created successfully.
134
- 6. If the datafile was created successfully, report the result to the user.
135
- 7. If the datafile was not created, finish the process and just wait for a new user prompt.
136
- 8. If the datafile was created successfully, but the built failed, try to fix the error and repeat the process.
137
-
138
- # When creating a landing datasource given a .ndjson file:
139
- - If the user does not specify anything about the desired schema, create a schema like this:
140
- SCHEMA >
141
- `data` String `json:$`
142
-
143
- - Use always json paths with .ndjson files.
144
-
145
- # When user wants to optimize an endpoint:
146
- {endpoint_optimization_instructions}
147
-
148
- IMPORTANT: If the user cancels some of the steps or there is an error in file creation, DO NOT continue with the plan. Stop the process and wait for the user before using any other tool.
149
- IMPORTANT: Every time you finish a plan and start a new resource creation or update process, create a new plan before starting with the changes.
150
-
151
- # Using deployment tools:
152
- - Use `deploy_check` tool to check if the project can be deployed to Tinybird Cloud before deploying it.
153
- - Use `deploy` tool to deploy the project to Tinybird Cloud.
154
- - Only use deployment tools if user explicitly asks for it.
155
-
156
- # When planning the creation or update of resources:
157
- {plan_instructions}
158
- {datafile_instructions}
159
-
160
- # Working with datasource files:
161
- {datasource_instructions}
162
- {datasource_example}
163
-
164
- # Working with any type of pipe file:
165
- {pipe_instructions}
166
- {pipe_example}
167
-
168
- # Working with materialized pipe files:
169
- {materialized_pipe_instructions}
170
-
171
- # Working with sink pipe files:
172
- {sink_pipe_instructions}
173
-
174
- # Working with copy pipe files:
175
- {copy_pipe_instructions}
176
-
177
- # Working with SQL queries:
178
- {sql_agent_instructions}
179
- {sql_instructions}
180
-
181
- # Working with connections files:
182
- {connection_instructions}
183
-
184
- # Connection examples:
185
- Kafka: {kafka_connection_example}
186
- S3: {s3_connection_example}
187
- GCS: {gcs_connection_example}
188
-
189
- # When executing a query or requesting an endpoint:
190
- - You need to be sure that the selected resource is updated to the last version in the environment you are working on.
191
- - Use `diff_resource` tool to compare the content of the resource to compare the differences between environments.
192
- - Project local file is the source of truth.
193
- - If the resource is not present or updated to the last version in Tinybird Local, it means you need to build the project.
194
- - If the resource is not present or updated to the last version in Tinybird Cloud, it means you need to deploy the project.
195
-
196
- # Info
197
- Today is {datetime.now().strftime("%Y-%m-%d")}
198
- """,
71
+ system_prompt=agent_system_prompt,
199
72
  tools=[
200
73
  Tool(preview_datafile, docstring_format="google", require_parameter_descriptions=True, takes_ctx=False),
201
74
  Tool(create_datafile, docstring_format="google", require_parameter_descriptions=True, takes_ctx=True),
@@ -221,76 +94,131 @@ Today is {datetime.now().strftime("%Y-%m-%d")}
221
94
  Tool(request_endpoint, docstring_format="google", require_parameter_descriptions=True, takes_ctx=True),
222
95
  Tool(diff_resource, docstring_format="google", require_parameter_descriptions=True, takes_ctx=True),
223
96
  ],
97
+ # history_processors=[self._keep_recent_messages],
224
98
  )
225
99
 
226
- def _keep_recent_messages(self) -> list[ModelMessage]:
100
+ @self.agent.instructions
101
+ def get_local_host(ctx: RunContext[TinybirdAgentContext]) -> str:
102
+ return f"Tinybird Local host: {ctx.deps.local_host}"
103
+
104
+ @self.agent.instructions
105
+ def get_cloud_host(ctx: RunContext[TinybirdAgentContext]) -> str:
106
+ return f"Tinybird Cloud host: {ctx.deps.host}"
107
+
108
+ @self.agent.instructions
109
+ def get_local_token(ctx: RunContext[TinybirdAgentContext]) -> str:
110
+ return f"Tinybird Local token: {ctx.deps.local_token}"
111
+
112
+ @self.agent.instructions
113
+ def get_cloud_token(ctx: RunContext[TinybirdAgentContext]) -> str:
114
+ return f"Tinybird Cloud token: {ctx.deps.token}"
115
+
116
+ @self.agent.instructions
117
+ def get_project_files(ctx: RunContext[TinybirdAgentContext]) -> str:
118
+ return resources_prompt(self.project)
119
+
120
+ self.thinking_animation = ThinkingAnimation()
121
+
122
+ def _keep_recent_messages(self, messages: list[ModelMessage]) -> list[ModelMessage]:
227
123
  """Keep only the last 5 messages to manage token usage."""
228
- return self.messages[-5:] if len(self.messages) > 5 else self.messages
124
+ return messages[-5:] if len(messages) > 5 else messages
229
125
 
230
- def run(self, user_prompt: str, config: dict[str, Any], project: Project) -> None:
231
- user_prompt = f"{user_prompt}\n\n{resources_prompt(project)}"
126
+ def _build_agent_deps(self, config: dict[str, Any]) -> TinybirdAgentContext:
232
127
  client = TinyB(token=self.token, host=self.host)
128
+ project = self.project
233
129
  folder = self.project.folder
234
- click.echo()
235
- thinking_animation = ThinkingAnimation(message="Chirping", delay=0.15)
236
- thinking_animation.start()
130
+ local_client = get_tinybird_local_client(config, test=False, silent=False)
131
+ return TinybirdAgentContext(
132
+ # context does not support the whole client, so we need to pass only the functions we need
133
+ explore_data=client.explore_data,
134
+ build_project=partial(build_project, project=project, config=config),
135
+ deploy_project=partial(deploy_project, project=project, config=config),
136
+ deploy_check_project=partial(deploy_check_project, project=project, config=config),
137
+ mock_data=partial(mock_data, project=project, config=config),
138
+ append_data_local=partial(append_data_local, config=config),
139
+ append_data_cloud=partial(append_data_cloud, config=config),
140
+ analyze_fixture=partial(analyze_fixture, config=config),
141
+ execute_query_cloud=partial(execute_query_cloud, config=config),
142
+ execute_query_local=partial(execute_query_local, config=config),
143
+ request_endpoint_cloud=partial(request_endpoint_cloud, config=config),
144
+ request_endpoint_local=partial(request_endpoint_local, config=config),
145
+ get_datasource_datafile_cloud=partial(get_datasource_datafile_cloud, config=config),
146
+ get_datasource_datafile_local=partial(get_datasource_datafile_local, config=config),
147
+ get_pipe_datafile_cloud=partial(get_pipe_datafile_cloud, config=config),
148
+ get_pipe_datafile_local=partial(get_pipe_datafile_local, config=config),
149
+ get_connection_datafile_cloud=partial(get_connection_datafile_cloud, config=config),
150
+ get_connection_datafile_local=partial(get_connection_datafile_local, config=config),
151
+ get_project_files=project.get_project_files,
152
+ folder=folder,
153
+ thinking_animation=self.thinking_animation,
154
+ workspace_name=self.project.workspace_name,
155
+ dangerously_skip_permissions=self.dangerously_skip_permissions,
156
+ token=self.token,
157
+ user_token=self.user_token,
158
+ host=self.host,
159
+ local_host=local_client.host,
160
+ local_token=local_client.token,
161
+ )
162
+
163
+ def run(self, user_prompt: str, config: dict[str, Any]) -> None:
164
+ user_prompt = f"{user_prompt}\n\n{resources_prompt(self.project)}"
165
+ self.thinking_animation.start()
237
166
  result = self.agent.run_sync(
238
167
  user_prompt,
239
- deps=TinybirdAgentContext(
240
- # context does not support the whole client, so we need to pass only the functions we need
241
- explore_data=client.explore_data,
242
- build_project=partial(build_project, project=project, config=config),
243
- deploy_project=partial(deploy_project, project=project, config=config),
244
- deploy_check_project=partial(deploy_check_project, project=project, config=config),
245
- mock_data=partial(mock_data, project=project, config=config),
246
- append_data=partial(append_data, config=config),
247
- analyze_fixture=partial(analyze_fixture, config=config),
248
- execute_query_cloud=partial(execute_query_cloud, config=config),
249
- execute_query_local=partial(execute_query_local, config=config),
250
- request_endpoint_cloud=partial(request_endpoint_cloud, config=config),
251
- request_endpoint_local=partial(request_endpoint_local, config=config),
252
- get_datasource_datafile_cloud=partial(get_datasource_datafile_cloud, config=config),
253
- get_datasource_datafile_local=partial(get_datasource_datafile_local, config=config),
254
- get_pipe_datafile_cloud=partial(get_pipe_datafile_cloud, config=config),
255
- get_pipe_datafile_local=partial(get_pipe_datafile_local, config=config),
256
- get_connection_datafile_cloud=partial(get_connection_datafile_cloud, config=config),
257
- get_connection_datafile_local=partial(get_connection_datafile_local, config=config),
258
- get_project_files=project.get_project_files,
259
- folder=folder,
260
- thinking_animation=thinking_animation,
261
- workspace_name=self.project.workspace_name,
262
- dangerously_skip_permissions=self.dangerously_skip_permissions,
263
- token=self.token,
264
- user_token=self.user_token,
265
- host=self.host,
266
- ),
168
+ deps=self._build_agent_deps(config),
267
169
  message_history=self.messages,
268
170
  )
269
171
  new_messages = result.new_messages()
270
172
  self.messages.extend(new_messages)
271
- thinking_animation.stop()
272
- usage = result.usage()
273
- request_tokens = usage.request_tokens or 0
274
- response_tokens = usage.response_tokens or 0
275
- total_tokens = usage.total_tokens or 0
276
- cost = (
277
- request_tokens * model_costs["input_cost_per_token"]
278
- + response_tokens * model_costs["output_cost_per_token"]
279
- )
173
+ save_messages(new_messages)
174
+ self.thinking_animation.stop()
280
175
  click.echo(result.output)
281
- click.echo("\n")
282
-
176
+ self._echo_usage(config, result)
177
+
178
+ async def run_iter(self, user_prompt: str, config: dict[str, Any]) -> None:
179
+ user_prompt = f"{user_prompt}\n\n"
180
+ self.thinking_animation.start()
181
+ deps = self._build_agent_deps(config)
182
+
183
+ async with self.agent.iter(user_prompt, deps=deps, message_history=self.messages) as agent_run:
184
+ async for node in agent_run:
185
+ if hasattr(node, "model_response"):
186
+ for _i, part in enumerate(node.model_response.parts):
187
+ if hasattr(part, "content") and not agent_run.result:
188
+ animation_running = self.thinking_animation.running
189
+ if animation_running:
190
+ self.thinking_animation.stop()
191
+ click.echo(FeedbackManager.info(message=part.content))
192
+ if animation_running:
193
+ self.thinking_animation.start()
194
+
195
+ if agent_run.result is not None:
196
+ new_messages = agent_run.result.new_messages()
197
+ self.messages.extend(new_messages)
198
+ save_messages(new_messages)
199
+ self.thinking_animation.stop()
200
+ self._echo_usage(config, agent_run.result)
201
+
202
+ def _echo_usage(self, config: dict[str, Any], result: AgentRunResult) -> None:
283
203
  if "@tinybird.co" in config.get("user_email", ""):
204
+ usage = result.usage()
205
+ request_tokens = usage.request_tokens or 0
206
+ response_tokens = usage.response_tokens or 0
207
+ total_tokens = usage.total_tokens or 0
208
+ cost = (
209
+ request_tokens * model_costs["input_cost_per_token"]
210
+ + response_tokens * model_costs["output_cost_per_token"]
211
+ )
284
212
  click.echo(f"Input tokens: {request_tokens}")
285
213
  click.echo(f"Output tokens: {response_tokens}")
286
214
  click.echo(f"Total tokens: {total_tokens}")
287
215
  click.echo(f"Cost: ${cost:.6f}")
288
- click.echo("\n")
289
216
 
290
217
 
291
218
  def run_agent(
292
219
  config: dict[str, Any], project: Project, dangerously_skip_permissions: bool, prompt: Optional[str] = None
293
220
  ):
221
+ click.echo(FeedbackManager.highlight(message="» Initializing Tinybird Code..."))
294
222
  token = config.get("token", None)
295
223
  host = config.get("host", None)
296
224
  user_token = config.get("user_token", None)
@@ -307,13 +235,14 @@ def run_agent(
307
235
  default=True,
308
236
  )
309
237
  if yes:
310
- click.echo()
311
238
  login(host, auth_host="https://cloud.tinybird.co", workspace=None, interactive=False, method="browser")
312
- click.echo()
313
239
  cli_config = CLIConfig.get_project_config()
240
+ config = {**config, **cli_config.to_dict()}
314
241
  token = cli_config.get_token()
315
242
  user_token = cli_config.get_user_token()
316
243
  host = cli_config.get_host()
244
+ workspace_id = cli_config.get("id", "")
245
+ workspace_name = cli_config.get("name", "")
317
246
 
318
247
  if not token or not host or not user_token:
319
248
  click.echo(
@@ -321,23 +250,22 @@ def run_agent(
321
250
  )
322
251
  return
323
252
 
324
- # In print mode, always skip permissions to avoid interactive prompts
325
- skip_permissions = dangerously_skip_permissions or (prompt is not None)
326
- agent = TinybirdAgent(token, user_token, host, workspace_id, project, skip_permissions)
253
+ build_project(config, project, test=False, silent=True)
254
+
255
+ # In prompt mode, always skip permissions to avoid interactive prompts
256
+ prompt_mode = prompt is not None
257
+ agent = TinybirdAgent(token, user_token, host, workspace_id, project, dangerously_skip_permissions, prompt_mode)
327
258
 
328
259
  # Print mode: run once with the provided prompt and exit
329
260
  if prompt:
330
- agent.run(prompt, config, project)
261
+ agent.run(prompt, config)
331
262
  return
332
263
 
333
264
  # Interactive mode: show banner and enter interactive loop
334
265
  display_banner()
335
- click.echo()
336
266
  click.echo(FeedbackManager.info(message="Describe what you want to create and I'll help you build it"))
337
267
  click.echo(FeedbackManager.info(message="Run /help for more commands"))
338
268
 
339
- click.echo()
340
-
341
269
  except Exception as e:
342
270
  click.echo(FeedbackManager.error(message=f"Failed to initialize agent: {e}"))
343
271
  return
@@ -350,31 +278,48 @@ def run_agent(
350
278
  if user_input.startswith("tb "):
351
279
  cmd_parts = shlex.split(user_input)
352
280
  subprocess.run(cmd_parts)
353
- click.echo()
354
281
  continue
355
- if user_input.lower() in ["/exit", "/quit"]:
282
+ if user_input.lower() in ["/exit", "/quit", "exit", "quit"]:
356
283
  click.echo(FeedbackManager.info(message="Goodbye!"))
357
284
  break
358
- elif user_input.lower() == "/clear":
285
+ elif user_input.lower() in ["/clear", "clear"]:
359
286
  clear_history()
287
+ click.echo(FeedbackManager.info(message="Message history cleared!"))
288
+ clear_messages()
289
+ continue
290
+ elif user_input.lower().startswith("select ") or user_input.lower().startswith("with "):
291
+ query = f"SELECT * FROM ({user_input.strip()}) FORMAT JSON"
292
+ result = execute_query_local(config, query=query)
293
+ stats = result["statistics"]
294
+ seconds = stats["elapsed"]
295
+ rows_read = humanfriendly.format_number(stats["rows_read"])
296
+ bytes_read = humanfriendly.format_size(stats["bytes_read"])
297
+
298
+ click.echo(FeedbackManager.info_query_stats(seconds=seconds, rows=rows_read, bytes=bytes_read))
299
+
300
+ if not result["data"]:
301
+ click.echo(FeedbackManager.info_no_rows())
302
+ else:
303
+ echo_safe_humanfriendly_tables_format_pretty_table(
304
+ data=[d.values() for d in result["data"][:10]], column_names=result["data"][0].keys()
305
+ )
306
+ click.echo("Showing first 10 results\n")
360
307
  continue
361
308
  elif user_input.lower() == "/login":
362
- click.echo()
363
309
  subprocess.run(["tb", "login"], check=True)
364
- click.echo()
310
+
365
311
  continue
366
312
  elif user_input.lower() == "/help":
367
- click.echo()
368
313
  click.echo("• Describe what you want to create: 'Create a user analytics system'")
369
314
  click.echo("• Ask for specific resources: 'Create a pipe to aggregate daily clicks'")
370
315
  click.echo("• Connect to external services: 'Set up a Kafka connection for events'")
371
316
  click.echo("• Type '/exit' or '/quit' to leave")
372
- click.echo()
317
+
373
318
  continue
374
319
  elif user_input.strip() == "":
375
320
  continue
376
321
  else:
377
- agent.run(user_input, config, project)
322
+ asyncio.run(agent.run_iter(user_input, config))
378
323
 
379
324
  except KeyboardInterrupt:
380
325
  click.echo(FeedbackManager.info(message="Goodbye!"))
@@ -399,33 +344,37 @@ def build_project(config: dict[str, Any], project: Project, silent: bool = True,
399
344
 
400
345
  def deploy_project(config: dict[str, Any], project: Project) -> None:
401
346
  client = _get_tb_client(config["token"], config["host"])
402
- create_deployment(
403
- project=project,
404
- client=client,
405
- config=config,
406
- wait=True,
407
- auto=True,
408
- allow_destructive_operations=False,
409
- )
347
+ try:
348
+ create_deployment(
349
+ project=project,
350
+ client=client,
351
+ config=config,
352
+ wait=True,
353
+ auto=True,
354
+ allow_destructive_operations=False,
355
+ )
356
+ except SystemExit as e:
357
+ raise CLIDeploymentException(e.args[0])
410
358
 
411
359
 
412
360
  def deploy_check_project(config: dict[str, Any], project: Project) -> None:
413
361
  client = _get_tb_client(config["token"], config["host"])
414
- create_deployment(
415
- project=project,
416
- client=client,
417
- config=config,
418
- check=True,
419
- wait=True,
420
- auto=True,
421
- )
362
+ try:
363
+ create_deployment(project=project, client=client, config=config, check=True, wait=True, auto=True)
364
+ except SystemExit as e:
365
+ raise CLIDeploymentException(e.args[0])
422
366
 
423
367
 
424
- def append_data(config: dict[str, Any], datasource_name: str, path: str) -> None:
368
+ def append_data_local(config: dict[str, Any], datasource_name: str, path: str) -> None:
425
369
  client = get_tinybird_local_client(config, test=False, silent=False)
426
370
  append_mock_data(client, datasource_name, path)
427
371
 
428
372
 
373
+ def append_data_cloud(config: dict[str, Any], datasource_name: str, path: str) -> None:
374
+ client = _get_tb_client(config["token"], config["host"])
375
+ append_mock_data(client, datasource_name, path)
376
+
377
+
429
378
  def mock_data(
430
379
  config: dict[str, Any],
431
380
  project: Project,
@@ -1,7 +1,13 @@
1
+ import json
1
2
  from pathlib import Path
2
3
  from typing import Optional
3
4
 
5
+ import click
4
6
  from prompt_toolkit.history import FileHistory
7
+ from pydantic_ai.messages import ModelMessage, ModelMessagesTypeAdapter
8
+ from pydantic_core import to_jsonable_python
9
+
10
+ from tinybird.tb.modules.feedback_manager import FeedbackManager
5
11
 
6
12
 
7
13
  def get_history_file_path():
@@ -39,3 +45,59 @@ def clear_history():
39
45
  """Clear the history file"""
40
46
  history_file = get_history_file_path()
41
47
  history_file.unlink(missing_ok=True)
48
+
49
+
50
+ def clear_messages():
51
+ """Clear the messages file"""
52
+ messages_file = get_messages_file_path()
53
+ messages_file.unlink(missing_ok=True)
54
+
55
+
56
+ def get_messages_file_path():
57
+ """Get the history file path based on current working directory"""
58
+ # Get current working directory
59
+ cwd = Path.cwd()
60
+
61
+ # Get user's home directory
62
+ home = Path.home()
63
+
64
+ # Calculate relative path from home to current directory
65
+ try:
66
+ relative_path = cwd.relative_to(home)
67
+ except ValueError:
68
+ # If current directory is not under home, use absolute path components
69
+ relative_path = Path(*cwd.parts[1:]) if cwd.is_absolute() else cwd
70
+
71
+ # Create history directory structure
72
+ history_dir = home / ".tinybird" / "projects" / relative_path
73
+ history_dir.mkdir(parents=True, exist_ok=True)
74
+
75
+ # Return history file path
76
+ return history_dir / "messages.json"
77
+
78
+
79
+ def load_messages() -> list[ModelMessage]:
80
+ try:
81
+ messages_file = get_messages_file_path()
82
+ messages_file.touch()
83
+ if not messages_file.exists():
84
+ messages_file.touch(exist_ok=True)
85
+ messages_file.write_text("[]")
86
+ return []
87
+ with open(messages_file, "r") as f:
88
+ messages_json = json.loads(f.read() or "[]")
89
+ return ModelMessagesTypeAdapter.validate_python(messages_json)
90
+ except Exception as e:
91
+ click.echo(FeedbackManager.error(message=f"Could not load previous messages: {e}"))
92
+ messages_file.unlink(missing_ok=True)
93
+ return []
94
+
95
+
96
+ def save_messages(new_messages: list[ModelMessage]):
97
+ messages_file = get_messages_file_path()
98
+ messages_file.touch(exist_ok=True)
99
+ messages = load_messages()
100
+ messages.extend(new_messages)
101
+ messages_json = to_jsonable_python(messages)
102
+ with open(messages_file, "w") as f:
103
+ f.write(json.dumps(messages_json))