tinybird 0.0.1.dev266__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.dev266'
8
- __revision__ = 'b4193e9'
7
+ __version__ = '0.0.1.dev268'
8
+ __revision__ = '22adccb'
@@ -11,7 +11,6 @@ from typing import Any, Optional
11
11
  import click
12
12
  import humanfriendly
13
13
  from pydantic_ai import Agent, RunContext, Tool
14
- from pydantic_ai.agent import AgentRunResult
15
14
  from pydantic_ai.messages import ModelMessage, ModelRequest, UserPromptPart
16
15
  from requests import Response
17
16
 
@@ -19,7 +18,14 @@ from tinybird.tb.client import TinyB
19
18
  from tinybird.tb.modules.agent.animations import ThinkingAnimation
20
19
  from tinybird.tb.modules.agent.banner import display_banner
21
20
  from tinybird.tb.modules.agent.command_agent import CommandAgent
22
- 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
+ )
23
29
  from tinybird.tb.modules.agent.models import create_model
24
30
  from tinybird.tb.modules.agent.prompts import agent_system_prompt, load_custom_project_rules, resources_prompt
25
31
  from tinybird.tb.modules.agent.testing_agent import TestingAgent
@@ -30,13 +36,10 @@ from tinybird.tb.modules.agent.tools.create_datafile import create_datafile, ren
30
36
  from tinybird.tb.modules.agent.tools.deploy import deploy
31
37
  from tinybird.tb.modules.agent.tools.deploy_check import deploy_check
32
38
  from tinybird.tb.modules.agent.tools.diff_resource import diff_resource
33
- from tinybird.tb.modules.agent.tools.execute_query import execute_query
34
39
  from tinybird.tb.modules.agent.tools.get_endpoint_stats import get_endpoint_stats
35
40
  from tinybird.tb.modules.agent.tools.get_openapi_definition import get_openapi_definition
36
41
  from tinybird.tb.modules.agent.tools.mock import mock
37
42
  from tinybird.tb.modules.agent.tools.plan import plan
38
- from tinybird.tb.modules.agent.tools.preview_datafile import preview_datafile
39
- from tinybird.tb.modules.agent.tools.request_endpoint import request_endpoint
40
43
  from tinybird.tb.modules.agent.utils import AgentRunCancelled, TinybirdAgentContext, show_confirmation, show_input
41
44
  from tinybird.tb.modules.build_common import process as build_process
42
45
  from tinybird.tb.modules.common import _analyze, _get_tb_client, echo_safe_humanfriendly_tables_format_pretty_table
@@ -64,20 +67,21 @@ class TinybirdAgent:
64
67
  ):
65
68
  self.token = token
66
69
  self.user_token = user_token
70
+ self.workspace_id = workspace_id
67
71
  self.host = host
68
72
  self.dangerously_skip_permissions = dangerously_skip_permissions or prompt_mode
69
73
  self.project = project
70
74
  self.thinking_animation = ThinkingAnimation()
71
75
  if prompt_mode:
72
- self.messages: list[ModelMessage] = load_messages()[-5:]
76
+ self.messages: list[ModelMessage] = get_last_messages_from_last_user_prompt()
73
77
  else:
74
78
  self.messages = []
79
+
75
80
  self.agent = Agent(
76
81
  model=create_model(user_token, host, workspace_id),
77
82
  deps_type=TinybirdAgentContext,
78
83
  system_prompt=agent_system_prompt,
79
84
  tools=[
80
- Tool(preview_datafile, docstring_format="google", require_parameter_descriptions=True, takes_ctx=False),
81
85
  Tool(create_datafile, docstring_format="google", require_parameter_descriptions=True, takes_ctx=True),
82
86
  Tool(
83
87
  rename_datafile_or_fixture,
@@ -103,11 +107,9 @@ class TinybirdAgent:
103
107
  require_parameter_descriptions=True,
104
108
  takes_ctx=True,
105
109
  ),
106
- Tool(execute_query, docstring_format="google", require_parameter_descriptions=True, takes_ctx=True),
107
- Tool(request_endpoint, docstring_format="google", require_parameter_descriptions=True, takes_ctx=True),
108
110
  Tool(diff_resource, docstring_format="google", require_parameter_descriptions=True, takes_ctx=True),
109
111
  ],
110
- history_processors=[self._context_aware_processor],
112
+ history_processors=[compact_messages],
111
113
  )
112
114
 
113
115
  self.testing_agent = TestingAgent(
@@ -130,6 +132,16 @@ class TinybirdAgent:
130
132
  workspace_id=workspace_id,
131
133
  project=self.project,
132
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
+ )
133
145
 
134
146
  @self.agent.tool
135
147
  def manage_tests(ctx: RunContext[TinybirdAgentContext], task: str) -> str:
@@ -161,6 +173,20 @@ class TinybirdAgent:
161
173
  result = self.command_agent.run(task, deps=ctx.deps, usage=ctx.usage)
162
174
  return result.output
163
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
+
164
190
  @self.agent.instructions
165
191
  def get_local_host(ctx: RunContext[TinybirdAgentContext]) -> str:
166
192
  return f"Tinybird Local host: {ctx.deps.local_host}"
@@ -175,7 +201,7 @@ class TinybirdAgent:
175
201
 
176
202
  @self.agent.instructions
177
203
  def get_cloud_token(ctx: RunContext[TinybirdAgentContext]) -> str:
178
- 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."
179
205
 
180
206
  @self.agent.instructions
181
207
  def get_project_files(ctx: RunContext[TinybirdAgentContext]) -> str:
@@ -184,24 +210,7 @@ class TinybirdAgent:
184
210
  def add_message(self, message: ModelMessage) -> None:
185
211
  self.messages.append(message)
186
212
 
187
- def _context_aware_processor(
188
- self,
189
- ctx: RunContext[TinybirdAgentContext],
190
- messages: list[ModelMessage],
191
- ) -> list[ModelMessage]:
192
- # Access current usage
193
- if not ctx.usage:
194
- return messages
195
-
196
- current_tokens = ctx.usage.total_tokens or 0
197
-
198
- # Filter messages based on context
199
- if current_tokens < 200_000:
200
- return messages
201
-
202
- return messages[-10:] # Keep only recent messages when token usage is high
203
-
204
- 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:
205
214
  client = TinyB(token=self.token, host=self.host)
206
215
  project = self.project
207
216
  folder = self.project.folder
@@ -233,6 +242,7 @@ class TinybirdAgent:
233
242
  run_tests=partial(run_tests, project=project, client=test_client),
234
243
  folder=folder,
235
244
  thinking_animation=self.thinking_animation,
245
+ workspace_id=self.workspace_id,
236
246
  workspace_name=self.project.workspace_name,
237
247
  dangerously_skip_permissions=self.dangerously_skip_permissions,
238
248
  token=self.token,
@@ -240,6 +250,7 @@ class TinybirdAgent:
240
250
  host=self.host,
241
251
  local_host=local_client.host,
242
252
  local_token=local_client.token,
253
+ run_id=run_id,
243
254
  )
244
255
 
245
256
  def run(self, user_prompt: str, config: dict[str, Any]) -> None:
@@ -255,9 +266,10 @@ class TinybirdAgent:
255
266
  save_messages(new_messages)
256
267
  self.thinking_animation.stop()
257
268
  click.echo(result.output)
258
- self._echo_usage(config, result)
269
+ self.echo_usage(config)
259
270
 
260
- 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)
261
273
  user_prompt = f"{user_prompt}\n\n{load_custom_project_rules(self.project.folder)}"
262
274
  self.thinking_animation.start()
263
275
  deps = self._build_agent_deps(config)
@@ -270,7 +282,9 @@ class TinybirdAgent:
270
282
  animation_running = self.thinking_animation.running
271
283
  if animation_running:
272
284
  self.thinking_animation.stop()
273
- click.echo(FeedbackManager.info(message=part.content))
285
+ click.echo(
286
+ FeedbackManager.info(message=part.content.replace("__TB_CLOUD_TOKEN__", self.token))
287
+ )
274
288
  if animation_running:
275
289
  self.thinking_animation.start()
276
290
 
@@ -279,31 +293,28 @@ class TinybirdAgent:
279
293
  self.messages.extend(new_messages)
280
294
  save_messages(new_messages)
281
295
  self.thinking_animation.stop()
282
- self._echo_usage(config, agent_run.result)
296
+ self.echo_usage(config)
283
297
 
284
- def _echo_usage(self, config: dict[str, Any], result: AgentRunResult) -> None:
298
+ def echo_usage(self, config: dict[str, Any]) -> None:
285
299
  try:
286
300
  client = _get_tb_client(config["user_token"], config["host"])
287
301
  workspace_id = config.get("id", "")
288
302
  workspace = client.workspace(workspace_id, with_organization=True, version="v1")
289
303
  limits_data = client.organization_limits(workspace["organization"]["id"])
290
304
  ai_requests_limits = limits_data.get("limits", {}).get("ai_requests", {})
291
- current_ai_requests = ai_requests_limits.get("quantity", 0)
292
- max_ai_requests = ai_requests_limits.get("max", 0)
305
+ current_ai_requests = ai_requests_limits.get("quantity") or 0
306
+ max_ai_requests = ai_requests_limits.get("max") or 0
307
+ remaining_requests = max(max_ai_requests - current_ai_requests, 0)
308
+ current_ai_requests = min(max_ai_requests, current_ai_requests)
293
309
  if not max_ai_requests:
294
310
  return
295
- remaining_requests = max(max_ai_requests - current_ai_requests, 0)
296
311
  warning_threshold = max_ai_requests * 0.8
297
- if current_ai_requests >= warning_threshold:
298
- message_color = FeedbackManager.warning
299
- else:
300
- message_color = FeedbackManager.gray
301
-
302
- current_ai_requests = min(max_ai_requests, current_ai_requests)
303
-
312
+ message_color = (
313
+ FeedbackManager.warning if current_ai_requests >= warning_threshold else FeedbackManager.gray
314
+ )
304
315
  click.echo(
305
316
  message_color(
306
- message=f"{remaining_requests} agent requests left ({current_ai_requests}/{max_ai_requests}). This message is informative. Limits will be enforced soon."
317
+ message=f"{remaining_requests} requests left ({current_ai_requests}/{max_ai_requests}). You can continue using Tinybird Code. Limits will be enforced soon."
307
318
  )
308
319
  )
309
320
  except Exception:
@@ -339,7 +350,7 @@ def run_agent(
339
350
  workspace_id = cli_config.get("id", "")
340
351
  workspace_name = cli_config.get("name", "")
341
352
 
342
- if not token or not host or not user_token:
353
+ if not token or not host or not user_token or not workspace_id:
343
354
  click.echo(
344
355
  FeedbackManager.error(message="Tinybird Code requires authentication. Run 'tb login' first.")
345
356
  )
@@ -383,8 +394,17 @@ def run_agent(
383
394
 
384
395
  # Interactive mode: show banner and enter interactive loop
385
396
  display_banner()
386
- click.echo(FeedbackManager.info(message="Describe what you want to create and I'll help you build it"))
387
- click.echo(FeedbackManager.info(message="Run /help for more commands"))
397
+ click.echo(
398
+ FeedbackManager.info(
399
+ message="""Tips for getting started:
400
+ - Describe what you want to build or ask for specific resources.
401
+ - Run tb commands directly without leaving interactive mode.
402
+ - Create a TINYBIRD.md file to customize your interactions.
403
+ """
404
+ )
405
+ )
406
+ agent.echo_usage(config)
407
+ click.echo()
388
408
 
389
409
  except Exception as e:
390
410
  click.echo(FeedbackManager.error(message=f"Failed to initialize agent: {e}"))
@@ -431,18 +451,13 @@ def run_agent(
431
451
 
432
452
  continue
433
453
  elif user_input.lower() == "/help":
434
- click.echo(" Describe what you want to create: 'Create a user analytics system'")
435
- click.echo("• Ask for specific resources: 'Create a pipe to aggregate daily clicks'")
436
- click.echo("• Connect to external services: 'Set up a Kafka connection for events'")
437
- click.echo("• Type '/exit' or '/quit' to leave")
438
-
454
+ subprocess.run(["tb", "--help"], check=True)
439
455
  continue
440
456
  elif user_input.strip() == "":
441
457
  continue
442
458
  else:
443
459
  run_id = str(uuid.uuid4())
444
- model = create_model(user_token, host, workspace_id, run_id=run_id)
445
- asyncio.run(agent.run_iter(user_input, config, model))
460
+ asyncio.run(agent.run_iter(user_input, config, run_id))
446
461
  except AgentRunCancelled:
447
462
  click.echo(FeedbackManager.info(message="User cancelled the operation"))
448
463
  agent.add_message(
@@ -41,31 +41,14 @@ def display_banner():
41
41
 
42
42
  click.echo("\n")
43
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)]
44
+ banner = [
45
+ " ████████╗██╗███╗ ██╗██╗ ██╗██████╗ ██╗██████╗ ██████╗ ██████╗ ██████╗ ██████╗ ███████╗",
46
+ " ╚══██╔══╝██║████╗ ██║╚██╗ ██╔╝██╔══██╗██║██╔══██╗██╔══██╗ ██╔════╝██╔═══██╗██╔══██╗██╔════╝",
47
+ " ██║ ██║██╔██╗ ██║ ╚████╔╝ ██████╔╝██║██████╔╝██║ ██║ ██║ ██║ ██║██║ ██║█████╗ ",
48
+ " ██║ ██║██║╚██╗██║ ╚██╔╝ ██╔══██╗██║██╔══██╗██║ ██║ ██║ ██║ ██║██║ ██║██╔══╝ ",
49
+ " ██║ ██║██║ ╚████║ ██║ ██████╔╝██║██║ ██║██████╔╝ ╚██████╗╚██████╔╝██████╔╝███████╗",
50
+ " ╚═╝ ╚═╝╚═╝ ╚═══╝ ╚═╝ ╚═════╝ ╚═╝╚═╝ ╚═╝╚═════╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝",
51
+ ]
69
52
 
70
53
  def rgb_to_ansi(r: int, g: int, b: int, use_truecolor: bool):
71
54
  """Convert RGB values to ANSI escape code"""
@@ -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