tinybird 0.0.1.dev267__py3-none-any.whl → 0.0.1.dev268__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/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.dev267'
8
- __revision__ = '8c3d4f6'
7
+ __version__ = '0.0.1.dev268'
8
+ __revision__ = '22adccb'
@@ -18,7 +18,14 @@ from tinybird.tb.client import TinyB
18
18
  from tinybird.tb.modules.agent.animations import ThinkingAnimation
19
19
  from tinybird.tb.modules.agent.banner import display_banner
20
20
  from tinybird.tb.modules.agent.command_agent import CommandAgent
21
- from tinybird.tb.modules.agent.memory import clear_history, clear_messages, load_messages, save_messages
21
+ from tinybird.tb.modules.agent.compactor import compact_messages
22
+ from tinybird.tb.modules.agent.explore_agent import ExploreAgent
23
+ from tinybird.tb.modules.agent.memory import (
24
+ clear_history,
25
+ clear_messages,
26
+ get_last_messages_from_last_user_prompt,
27
+ save_messages,
28
+ )
22
29
  from tinybird.tb.modules.agent.models import create_model
23
30
  from tinybird.tb.modules.agent.prompts import agent_system_prompt, load_custom_project_rules, resources_prompt
24
31
  from tinybird.tb.modules.agent.testing_agent import TestingAgent
@@ -29,13 +36,10 @@ from tinybird.tb.modules.agent.tools.create_datafile import create_datafile, ren
29
36
  from tinybird.tb.modules.agent.tools.deploy import deploy
30
37
  from tinybird.tb.modules.agent.tools.deploy_check import deploy_check
31
38
  from tinybird.tb.modules.agent.tools.diff_resource import diff_resource
32
- from tinybird.tb.modules.agent.tools.execute_query import execute_query
33
39
  from tinybird.tb.modules.agent.tools.get_endpoint_stats import get_endpoint_stats
34
40
  from tinybird.tb.modules.agent.tools.get_openapi_definition import get_openapi_definition
35
41
  from tinybird.tb.modules.agent.tools.mock import mock
36
42
  from tinybird.tb.modules.agent.tools.plan import plan
37
- from tinybird.tb.modules.agent.tools.preview_datafile import preview_datafile
38
- from tinybird.tb.modules.agent.tools.request_endpoint import request_endpoint
39
43
  from tinybird.tb.modules.agent.utils import AgentRunCancelled, TinybirdAgentContext, show_confirmation, show_input
40
44
  from tinybird.tb.modules.build_common import process as build_process
41
45
  from tinybird.tb.modules.common import _analyze, _get_tb_client, echo_safe_humanfriendly_tables_format_pretty_table
@@ -63,20 +67,21 @@ class TinybirdAgent:
63
67
  ):
64
68
  self.token = token
65
69
  self.user_token = user_token
70
+ self.workspace_id = workspace_id
66
71
  self.host = host
67
72
  self.dangerously_skip_permissions = dangerously_skip_permissions or prompt_mode
68
73
  self.project = project
69
74
  self.thinking_animation = ThinkingAnimation()
70
75
  if prompt_mode:
71
- self.messages: list[ModelMessage] = load_messages()[-5:]
76
+ self.messages: list[ModelMessage] = get_last_messages_from_last_user_prompt()
72
77
  else:
73
78
  self.messages = []
79
+
74
80
  self.agent = Agent(
75
81
  model=create_model(user_token, host, workspace_id),
76
82
  deps_type=TinybirdAgentContext,
77
83
  system_prompt=agent_system_prompt,
78
84
  tools=[
79
- Tool(preview_datafile, docstring_format="google", require_parameter_descriptions=True, takes_ctx=False),
80
85
  Tool(create_datafile, docstring_format="google", require_parameter_descriptions=True, takes_ctx=True),
81
86
  Tool(
82
87
  rename_datafile_or_fixture,
@@ -102,11 +107,9 @@ class TinybirdAgent:
102
107
  require_parameter_descriptions=True,
103
108
  takes_ctx=True,
104
109
  ),
105
- Tool(execute_query, docstring_format="google", require_parameter_descriptions=True, takes_ctx=True),
106
- Tool(request_endpoint, docstring_format="google", require_parameter_descriptions=True, takes_ctx=True),
107
110
  Tool(diff_resource, docstring_format="google", require_parameter_descriptions=True, takes_ctx=True),
108
111
  ],
109
- history_processors=[self._context_aware_processor],
112
+ history_processors=[compact_messages],
110
113
  )
111
114
 
112
115
  self.testing_agent = TestingAgent(
@@ -129,6 +132,16 @@ class TinybirdAgent:
129
132
  workspace_id=workspace_id,
130
133
  project=self.project,
131
134
  )
135
+ self.explore_agent = ExploreAgent(
136
+ dangerously_skip_permissions=self.dangerously_skip_permissions,
137
+ prompt_mode=prompt_mode,
138
+ thinking_animation=self.thinking_animation,
139
+ token=self.token,
140
+ user_token=self.user_token,
141
+ host=self.host,
142
+ workspace_id=workspace_id,
143
+ project=self.project,
144
+ )
132
145
 
133
146
  @self.agent.tool
134
147
  def manage_tests(ctx: RunContext[TinybirdAgentContext], task: str) -> str:
@@ -160,6 +173,20 @@ class TinybirdAgent:
160
173
  result = self.command_agent.run(task, deps=ctx.deps, usage=ctx.usage)
161
174
  return result.output
162
175
 
176
+ @self.agent.tool
177
+ def explore_data(ctx: RunContext[TinybirdAgentContext], task: str) -> str:
178
+ """Explore the data in the project by executing SQL queries or requesting endpoints or exporting data or visualizing data as a chart.
179
+
180
+ Args:
181
+ task (str): The task to solve. Required.
182
+
183
+ Returns:
184
+ str: The summary of the result.
185
+ """
186
+ result = self.explore_agent.run(task, deps=ctx.deps, usage=ctx.usage)
187
+ self.explore_agent.clear_messages()
188
+ return result.output or "No result returned"
189
+
163
190
  @self.agent.instructions
164
191
  def get_local_host(ctx: RunContext[TinybirdAgentContext]) -> str:
165
192
  return f"Tinybird Local host: {ctx.deps.local_host}"
@@ -174,7 +201,7 @@ class TinybirdAgent:
174
201
 
175
202
  @self.agent.instructions
176
203
  def get_cloud_token(ctx: RunContext[TinybirdAgentContext]) -> str:
177
- return f"Tinybird Cloud token: {ctx.deps.token}"
204
+ return "When using in the output the Tinybird Cloud token, use the placeholder __TB_CLOUD_TOKEN__. Do not mention that it is a placeholder, because it will be replaced by the actual token by code."
178
205
 
179
206
  @self.agent.instructions
180
207
  def get_project_files(ctx: RunContext[TinybirdAgentContext]) -> str:
@@ -183,24 +210,7 @@ class TinybirdAgent:
183
210
  def add_message(self, message: ModelMessage) -> None:
184
211
  self.messages.append(message)
185
212
 
186
- def _context_aware_processor(
187
- self,
188
- ctx: RunContext[TinybirdAgentContext],
189
- messages: list[ModelMessage],
190
- ) -> list[ModelMessage]:
191
- # Access current usage
192
- if not ctx.usage:
193
- return messages
194
-
195
- current_tokens = ctx.usage.total_tokens or 0
196
-
197
- # Filter messages based on context
198
- if current_tokens < 200_000:
199
- return messages
200
-
201
- return messages[-10:] # Keep only recent messages when token usage is high
202
-
203
- def _build_agent_deps(self, config: dict[str, Any]) -> TinybirdAgentContext:
213
+ def _build_agent_deps(self, config: dict[str, Any], run_id: Optional[str] = None) -> TinybirdAgentContext:
204
214
  client = TinyB(token=self.token, host=self.host)
205
215
  project = self.project
206
216
  folder = self.project.folder
@@ -232,6 +242,7 @@ class TinybirdAgent:
232
242
  run_tests=partial(run_tests, project=project, client=test_client),
233
243
  folder=folder,
234
244
  thinking_animation=self.thinking_animation,
245
+ workspace_id=self.workspace_id,
235
246
  workspace_name=self.project.workspace_name,
236
247
  dangerously_skip_permissions=self.dangerously_skip_permissions,
237
248
  token=self.token,
@@ -239,6 +250,7 @@ class TinybirdAgent:
239
250
  host=self.host,
240
251
  local_host=local_client.host,
241
252
  local_token=local_client.token,
253
+ run_id=run_id,
242
254
  )
243
255
 
244
256
  def run(self, user_prompt: str, config: dict[str, Any]) -> None:
@@ -256,7 +268,8 @@ class TinybirdAgent:
256
268
  click.echo(result.output)
257
269
  self.echo_usage(config)
258
270
 
259
- async def run_iter(self, user_prompt: str, config: dict[str, Any], model: Any) -> None:
271
+ async def run_iter(self, user_prompt: str, config: dict[str, Any], run_id: Optional[str] = None) -> None:
272
+ model = create_model(self.user_token, self.host, self.workspace_id, run_id=run_id)
260
273
  user_prompt = f"{user_prompt}\n\n{load_custom_project_rules(self.project.folder)}"
261
274
  self.thinking_animation.start()
262
275
  deps = self._build_agent_deps(config)
@@ -269,7 +282,9 @@ class TinybirdAgent:
269
282
  animation_running = self.thinking_animation.running
270
283
  if animation_running:
271
284
  self.thinking_animation.stop()
272
- click.echo(FeedbackManager.info(message=part.content))
285
+ click.echo(
286
+ FeedbackManager.info(message=part.content.replace("__TB_CLOUD_TOKEN__", self.token))
287
+ )
273
288
  if animation_running:
274
289
  self.thinking_animation.start()
275
290
 
@@ -442,8 +457,7 @@ def run_agent(
442
457
  continue
443
458
  else:
444
459
  run_id = str(uuid.uuid4())
445
- model = create_model(user_token, host, workspace_id, run_id=run_id)
446
- asyncio.run(agent.run_iter(user_input, config, model))
460
+ asyncio.run(agent.run_iter(user_input, config, run_id))
447
461
  except AgentRunCancelled:
448
462
  click.echo(FeedbackManager.info(message="User cancelled the operation"))
449
463
  agent.add_message(
@@ -25,6 +25,7 @@ class CommandAgent:
25
25
  self.token = token
26
26
  self.user_token = user_token
27
27
  self.host = host
28
+ self.workspace_id = workspace_id
28
29
  self.dangerously_skip_permissions = dangerously_skip_permissions or prompt_mode
29
30
  self.project = project
30
31
  self.thinking_animation = thinking_animation
@@ -54,6 +55,12 @@ Always run first help commands to be sure that the commands you are running is n
54
55
  return tests_files_prompt(self.project)
55
56
 
56
57
  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
+ result = self.agent.run_sync(
59
+ task,
60
+ deps=deps,
61
+ usage=usage,
62
+ message_history=self.messages,
63
+ model=create_model(self.user_token, self.host, self.workspace_id, run_id=deps.run_id),
64
+ )
58
65
  self.messages.extend(result.new_messages())
59
66
  return result
@@ -0,0 +1,311 @@
1
+ import enum
2
+
3
+ import click
4
+ from pydantic import BaseModel, Field
5
+ from pydantic_ai import Agent, RunContext, ToolOutput
6
+ from pydantic_ai.messages import (
7
+ ModelMessage,
8
+ ModelRequest,
9
+ ModelResponse,
10
+ SystemPromptPart,
11
+ TextPart,
12
+ ToolReturnPart,
13
+ UserPromptPart,
14
+ )
15
+
16
+ from tinybird.tb.modules.agent.utils import TinybirdAgentContext
17
+ from tinybird.tb.modules.feedback_manager import FeedbackManager
18
+
19
+ SYSTEM_PROMPT = """
20
+ Your task is to create a detailed summary of the conversation so far, paying close attention to the user's explicit requests and your previous actions.
21
+ This summary should be thorough in capturing technical details, code patterns, and architectural decisions that would be essential for continuing development work without losing context.
22
+
23
+ Before providing your final summary, wrap your analysis in <analysis> tags to organize your thoughts and ensure you've covered all necessary points. In your analysis process:
24
+
25
+ 1. Chronologically analyze each message and section of the conversation. For each section thoroughly identify:
26
+ - The user's explicit requests and intents
27
+ - Your approach to addressing the user's requests
28
+ - Key decisions, technical concepts and code patterns
29
+ - Specific details like file names, full code snippets, function signatures, file edits, etc
30
+ 2. Double-check for technical accuracy and completeness, addressing each required element thoroughly.
31
+
32
+ Your summary should include the following sections:
33
+
34
+ 1. Primary Request and Intent: Capture all of the user's explicit requests and intents in detail
35
+ 2. Key Technical Concepts: List all important technical concepts, technologies, and frameworks discussed.
36
+ 3. Files and Code Sections: Enumerate specific files and code sections examined, modified, or created. Pay special attention to the most recent messages and include full code snippets where applicable and include a summary of why this file read or edit is important.
37
+ 4. Problem Solving: Document problems solved and any ongoing troubleshooting efforts.
38
+ 5. Pending Tasks: Outline any pending tasks that you have explicitly been asked to work on.
39
+ 6. Current Work: Describe in detail precisely what was being worked on immediately before this summary request, paying special attention to the most recent messages from both user and assistant. Include file names and code snippets where applicable.
40
+ 7. Optional Next Step: List the next step that you will take that is related to the most recent work you were doing. IMPORTANT: ensure that this step is DIRECTLY in line with the user's explicit requests, and the task you were working on immediately before this summary request. If your last task was concluded, then only list next steps if they are explicitly in line with the users request. Do not start on tangential requests without confirming with the user first.
41
+ 8. If there is a next step, include direct quotes from the most recent conversation showing exactly what task you were working on and where you left off. This should be verbatim to ensure there's no drift in task interpretation.
42
+
43
+ Here's an example of how your output should be structured:
44
+
45
+ <example>
46
+ <condense>
47
+ <analysis>
48
+ [Your thought process, ensuring all points are covered thoroughly and accurately]
49
+ </analysis>
50
+
51
+ <context>
52
+ 1. Primary Request and Intent:
53
+ [Detailed description]
54
+
55
+ 2. Key Technical Concepts:
56
+ - [Concept 1]
57
+ - [Concept 2]
58
+ - [...]
59
+
60
+ 3. Files and Code Sections:
61
+ - [File Name 1]
62
+ - [Summary of why this file is important]
63
+ - [Summary of the changes made to this file, if any]
64
+ - [Important Code Snippet]
65
+ - [File Name 2]
66
+ - [Important Code Snippet]
67
+ - [...]
68
+
69
+ 4. Problem Solving:
70
+ [Description of solved problems and ongoing troubleshooting]
71
+
72
+ 5. Pending Tasks:
73
+ - [Task 1]
74
+ - [Task 2]
75
+ - [...]
76
+
77
+ 6. Current Work:
78
+ [Precise description of current work]
79
+
80
+ 7. Next Step:
81
+ [Next st
82
+
83
+ </context>
84
+ </condense>
85
+ </example>
86
+
87
+ Please provide your summary based on the conversation so far, following this structure and ensuring precision and thoroughness in your response.
88
+ """
89
+
90
+
91
+ class CondenseResult(BaseModel):
92
+ analysis: str = Field(
93
+ ...,
94
+ description="""A summary of the conversation so far, capturing technical details, code patterns, and architectural decisions.""",
95
+ )
96
+ context: str = Field(
97
+ ...,
98
+ description="""The context to continue the conversation with. If applicable based on the current task, this should include:
99
+
100
+ 1. Primary Request and Intent: Capture all of the user's explicit requests and intents in detail
101
+ 2. Key Technical Concepts: List all important technical concepts, technologies, and frameworks discussed.
102
+ 3. Files and Code Sections: Enumerate specific files and code sections examined, modified, or created. Pay special attention to the most recent messages and include full code snippets where applicable and include a summary of why this file read or edit is important.
103
+ 4. Problem Solving: Document problems solved and any ongoing troubleshooting efforts.
104
+ 5. Pending Tasks: Outline any pending tasks that you have explicitly been asked to work on.
105
+ 6. Current Work: Describe in detail precisely what was being worked on immediately before this summary request, paying special attention to the most recent messages from both user and assistant. Include file names and code snippets where applicable.
106
+ 7. Optional Next Step: List the next step that you will take that is related to the most recent work you were doing. IMPORTANT: ensure that this step is DIRECTLY in line with the user's explicit requests, and the task you were working on immediately before this summary request. If your last task was concluded, then only list next steps if they are explicitly in line with the users request. Do not start on tangential requests without confirming with the user first.
107
+ 8. If there is a next step, include direct quotes from the most recent conversation showing exactly what task you were working on and where you left off. This should be verbatim to ensure there's no drift in task interpretation.
108
+ """,
109
+ )
110
+
111
+
112
+ summarize_agent = Agent(
113
+ instructions=SYSTEM_PROMPT,
114
+ output_type=ToolOutput(
115
+ type_=CondenseResult,
116
+ name="condense",
117
+ description="""
118
+ Your task is to create a detailed summary of the conversation so far, paying close attention to the user's explicit requests and your previous actions. This summary should be thorough in capturing technical details, code patterns, and architectural decisions that would be essential for continuing with the conversation and supporting any continuing tasks.
119
+ The user will be presented with a preview of your generated summary and can choose to use it to compact their context window or keep chatting in the current conversation.
120
+ Users may refer to this tool as 'smol' or 'compact' as well. You should consider these to be equivalent to 'condense' when used in a similar context.
121
+ """,
122
+ max_retries=5,
123
+ ),
124
+ retries=3,
125
+ )
126
+
127
+
128
+ @summarize_agent.tool
129
+ def dummy_tool(ctx: RunContext[None]) -> str:
130
+ return "ok"
131
+
132
+
133
+ def get_current_token_consumption(message_history: list[ModelMessage]) -> int:
134
+ current_token_comsumption = 0
135
+ for msg in reversed(message_history):
136
+ if isinstance(msg, ModelResponse) and msg.usage.total_tokens:
137
+ current_token_comsumption = msg.usage.total_tokens
138
+ break
139
+ return current_token_comsumption
140
+
141
+
142
+ MODEL_CONTEXT_WINDOW = 200_000
143
+ COMPACT_THRESHOLD = 0.8
144
+ MODEL_MAX_TOKENS = 8_000
145
+
146
+
147
+ def need_compact(message_history: list[ModelMessage]) -> bool:
148
+ current_token_comsumption = get_current_token_consumption(message_history) or 0
149
+ token_threshold = COMPACT_THRESHOLD * MODEL_CONTEXT_WINDOW
150
+ will_overflow = current_token_comsumption + MODEL_MAX_TOKENS >= MODEL_CONTEXT_WINDOW
151
+ return (current_token_comsumption and current_token_comsumption >= token_threshold) or will_overflow
152
+
153
+
154
+ def compact_messages(
155
+ ctx: RunContext[TinybirdAgentContext],
156
+ messages: list[ModelMessage],
157
+ ) -> list[ModelMessage]:
158
+ if not ctx.usage:
159
+ return messages
160
+
161
+ if not need_compact(messages):
162
+ return messages
163
+
164
+ original_system_prompts = extract_system_prompts(messages)
165
+ history_messages, keep_messages = split_history(messages)
166
+
167
+ if len(history_messages) <= 2:
168
+ history_messages, keep_messages = split_history(messages, CompactStrategy.none)
169
+ if len(history_messages) <= 2:
170
+ history_messages, keep_messages = split_history(messages, CompactStrategy.in_conversation)
171
+
172
+ if not history_messages:
173
+ return messages
174
+
175
+ ctx.deps.thinking_animation.stop()
176
+ click.echo(FeedbackManager.highlight(message="» Compacting messages before continuing..."))
177
+ result = summarize_agent.run_sync(
178
+ "The user has accepted the condensed conversation summary you generated. Use `condense` to generate a summary and context of the conversation so far. "
179
+ "This summary covers important details of the historical conversation with the user which has been truncated. "
180
+ "It's crucial that you respond by ONLY asking the user what you should work on next. "
181
+ "You should NOT take any initiative or make any assumptions about continuing with work. "
182
+ "Keep this response CONCISE and wrap your analysis in <analysis> and <context> tags to organize your thoughts and ensure you've covered all necessary points. ",
183
+ message_history=fix_system_prompt(history_messages, SYSTEM_PROMPT),
184
+ model=ctx.model,
185
+ )
186
+ summary_prompt = f"""Condensed conversation summary(not in the history):
187
+ <condense>
188
+ <analysis>
189
+ {result.output.analysis}
190
+ </analysis>
191
+
192
+ <context>
193
+ {result.output.context}
194
+ </context>
195
+ </condense>
196
+ """
197
+ click.echo(FeedbackManager.info(message="✓ Compacted messages"))
198
+ ctx.deps.thinking_animation.start()
199
+ return [
200
+ ModelRequest(
201
+ parts=[
202
+ *[SystemPromptPart(content=p) for p in original_system_prompts],
203
+ UserPromptPart(content="Please summary the conversation"),
204
+ ]
205
+ ),
206
+ ModelResponse(
207
+ parts=[TextPart(content=summary_prompt)],
208
+ ),
209
+ *keep_messages,
210
+ ]
211
+
212
+
213
+ def fix_system_prompt(message_history: list[ModelMessage], system_prompt: str) -> list[ModelMessage]:
214
+ if not message_history:
215
+ return message_history
216
+
217
+ message_history_without_system: list[ModelMessage] = []
218
+ for msg in message_history:
219
+ # Filter out system prompts
220
+ if not isinstance(msg, ModelRequest):
221
+ message_history_without_system.append(msg)
222
+ continue
223
+ message_history_without_system.append(
224
+ ModelRequest(
225
+ parts=[part for part in msg.parts if not isinstance(part, SystemPromptPart)],
226
+ instructions=msg.instructions,
227
+ )
228
+ )
229
+ if message_history_without_system and isinstance(message_history_without_system[0], ModelRequest):
230
+ # inject system prompt
231
+ message_history_without_system[0].parts.insert(0, SystemPromptPart(content=system_prompt))
232
+
233
+ return message_history_without_system
234
+
235
+
236
+ def extract_system_prompts(message_history: list[ModelMessage]) -> list[str]:
237
+ system_prompts = []
238
+ for msg in message_history:
239
+ if isinstance(msg, ModelRequest) and isinstance(msg.parts[0], SystemPromptPart):
240
+ system_prompts.append(msg.parts[0].content)
241
+ return system_prompts
242
+
243
+
244
+ class CompactStrategy(str, enum.Enum):
245
+ in_conversation = "in_conversation"
246
+ """Compact all message, including this round conversation"""
247
+
248
+ none = "none"
249
+ """Compact all previous messages"""
250
+
251
+ last_two = "last_two"
252
+ """Keeping the last two previous messages"""
253
+
254
+
255
+ def _split_history(
256
+ message_history: list[ModelMessage],
257
+ n: int,
258
+ ) -> tuple[list[ModelMessage], list[ModelMessage]]:
259
+ """
260
+ Returns a tuple of (history, keep_messages)
261
+ """
262
+ if not message_history:
263
+ return [], []
264
+
265
+ user_prompt_indices: list[int] = []
266
+ for i, msg in enumerate(message_history):
267
+ if not isinstance(msg, ModelRequest):
268
+ continue
269
+ if any(isinstance(p, UserPromptPart) for p in msg.parts) and not any(
270
+ isinstance(p, ToolReturnPart) for p in msg.parts
271
+ ):
272
+ user_prompt_indices.append(i)
273
+ if not user_prompt_indices:
274
+ # No user prompt in history, keep all
275
+ return [], message_history
276
+
277
+ if not n:
278
+ # Keep current user prompt and compact all
279
+ keep_messages: list[ModelMessage] = []
280
+ last_model_request = message_history[user_prompt_indices[-1]]
281
+ keep_messages.append(last_model_request)
282
+ if any(isinstance(p, ToolReturnPart) for p in message_history[-1].parts):
283
+ # Include last tool-call and tool-return pair
284
+ keep_messages.extend(message_history[-2:])
285
+ return message_history, keep_messages
286
+
287
+ if len(user_prompt_indices) < n:
288
+ # No enough history to keep
289
+ return [], message_history
290
+ return (
291
+ message_history[: user_prompt_indices[-n]],
292
+ message_history[user_prompt_indices[-n] :],
293
+ )
294
+
295
+
296
+ def split_history(
297
+ message_history: list[ModelMessage],
298
+ compact_strategy: CompactStrategy = CompactStrategy.last_two,
299
+ ) -> tuple[list[ModelMessage], list[ModelMessage]]:
300
+ if compact_strategy == CompactStrategy.none:
301
+ # Only current 1
302
+ history_messages, keep_messages = _split_history(message_history, 1)
303
+ elif compact_strategy == CompactStrategy.last_two:
304
+ # Previous 2 + current 1
305
+ history_messages, keep_messages = _split_history(message_history, 3)
306
+ elif compact_strategy == CompactStrategy.in_conversation:
307
+ history_messages, keep_messages = _split_history(message_history, 0)
308
+ else:
309
+ raise NotImplementedError(f"Compact strategy {compact_strategy} not implemented")
310
+
311
+ return history_messages, keep_messages
@@ -0,0 +1,86 @@
1
+ from datetime import datetime
2
+
3
+ from pydantic_ai import Agent, RunContext, Tool
4
+ from pydantic_ai.messages import ModelMessage
5
+ from pydantic_ai.usage import Usage
6
+
7
+ from tinybird.tb.modules.agent.animations import ThinkingAnimation
8
+ from tinybird.tb.modules.agent.compactor import compact_messages
9
+ from tinybird.tb.modules.agent.models import create_model
10
+ from tinybird.tb.modules.agent.prompts import (
11
+ explore_data_instructions,
12
+ resources_prompt,
13
+ tone_and_style_instructions,
14
+ )
15
+ from tinybird.tb.modules.agent.tools.diff_resource import diff_resource
16
+ from tinybird.tb.modules.agent.tools.execute_query import execute_query
17
+ from tinybird.tb.modules.agent.tools.request_endpoint import request_endpoint
18
+ from tinybird.tb.modules.agent.utils import TinybirdAgentContext
19
+ from tinybird.tb.modules.project import Project
20
+
21
+
22
+ class ExploreAgent:
23
+ def __init__(
24
+ self,
25
+ token: str,
26
+ user_token: str,
27
+ host: str,
28
+ workspace_id: str,
29
+ project: Project,
30
+ dangerously_skip_permissions: bool,
31
+ prompt_mode: bool,
32
+ thinking_animation: ThinkingAnimation,
33
+ ):
34
+ self.token = token
35
+ self.user_token = user_token
36
+ self.host = host
37
+ self.workspace_id = workspace_id
38
+ self.dangerously_skip_permissions = dangerously_skip_permissions or prompt_mode
39
+ self.project = project
40
+ self.thinking_animation = thinking_animation
41
+ self.messages: list[ModelMessage] = []
42
+ self.agent = Agent(
43
+ model=create_model(user_token, host, workspace_id),
44
+ deps_type=TinybirdAgentContext,
45
+ instructions=[
46
+ """
47
+ You are part of Tinybird Code, an agentic CLI that can help users to work with Tinybird.
48
+ You are a sub-agent of the main Tinybird Code agent. You are responsible for querying the data in the project.
49
+ You can do the following:
50
+ - Executing SQL queries against Tinybird Cloud or Tinybird Local.
51
+ - Requesting endpoints in Tinybird Cloud or Tinybird Local.
52
+ - Visualizing data as a chart using execute_query tool with the `script` parameter.
53
+ """,
54
+ tone_and_style_instructions,
55
+ explore_data_instructions,
56
+ ],
57
+ tools=[
58
+ Tool(execute_query, docstring_format="google", require_parameter_descriptions=True, takes_ctx=True),
59
+ Tool(request_endpoint, docstring_format="google", require_parameter_descriptions=True, takes_ctx=True),
60
+ Tool(diff_resource, docstring_format="google", require_parameter_descriptions=True, takes_ctx=True),
61
+ ],
62
+ history_processors=[compact_messages],
63
+ )
64
+
65
+ @self.agent.instructions
66
+ def get_today_date(ctx: RunContext[TinybirdAgentContext]) -> str:
67
+ return f"Today's date is {datetime.now().strftime('%Y-%m-%d')}"
68
+
69
+ @self.agent.instructions
70
+ def get_project_files(ctx: RunContext[TinybirdAgentContext]) -> str:
71
+ return resources_prompt(self.project)
72
+
73
+ def run(self, task: str, deps: TinybirdAgentContext, usage: Usage):
74
+ result = self.agent.run_sync(
75
+ task,
76
+ deps=deps,
77
+ usage=usage,
78
+ message_history=self.messages,
79
+ model=create_model(self.user_token, self.host, self.workspace_id, run_id=deps.run_id),
80
+ )
81
+ new_messages = result.new_messages()
82
+ self.messages.extend(new_messages)
83
+ return result
84
+
85
+ def clear_messages(self):
86
+ self.messages = []
@@ -4,7 +4,7 @@ from typing import Optional
4
4
 
5
5
  import click
6
6
  from prompt_toolkit.history import FileHistory
7
- from pydantic_ai.messages import ModelMessage, ModelMessagesTypeAdapter
7
+ from pydantic_ai.messages import ModelMessage, ModelMessagesTypeAdapter, ModelRequest
8
8
  from pydantic_core import to_jsonable_python
9
9
 
10
10
  from tinybird.tb.modules.feedback_manager import FeedbackManager
@@ -93,6 +93,16 @@ def load_messages() -> list[ModelMessage]:
93
93
  return []
94
94
 
95
95
 
96
+ def get_last_messages_from_last_user_prompt() -> list[ModelMessage]:
97
+ all_messages = load_messages()
98
+ # look the last message message with a part_kind of type "user_prompt" inside "parts" field that is a list of objects.
99
+ # once you find it, return that message and all the messages after it
100
+ for msg in reversed(all_messages):
101
+ if isinstance(msg, ModelRequest) and msg.parts and any(part.part_kind == "user-prompt" for part in msg.parts):
102
+ return all_messages[all_messages.index(msg) :]
103
+ return []
104
+
105
+
96
106
  def save_messages(new_messages: list[ModelMessage]):
97
107
  messages_file = get_messages_file_path()
98
108
  messages_file.touch(exist_ok=True)