tinybird 0.0.1.dev267__py3-none-any.whl → 0.0.1.dev269__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 +2 -2
- tinybird/tb/modules/agent/agent.py +46 -32
- tinybird/tb/modules/agent/command_agent.py +8 -1
- tinybird/tb/modules/agent/compactor.py +311 -0
- tinybird/tb/modules/agent/explore_agent.py +86 -0
- tinybird/tb/modules/agent/memory.py +11 -1
- tinybird/tb/modules/agent/prompts.py +51 -36
- tinybird/tb/modules/agent/testing_agent.py +8 -1
- tinybird/tb/modules/agent/tools/append.py +16 -6
- tinybird/tb/modules/agent/tools/create_datafile.py +17 -4
- tinybird/tb/modules/agent/tools/execute_query.py +138 -11
- tinybird/tb/modules/agent/tools/mock.py +30 -22
- tinybird/tb/modules/agent/tools/request_endpoint.py +16 -3
- tinybird/tb/modules/agent/tools/run_command.py +3 -1
- tinybird/tb/modules/agent/utils.py +42 -0
- tinybird/tb/modules/cli.py +3 -5
- {tinybird-0.0.1.dev267.dist-info → tinybird-0.0.1.dev269.dist-info}/METADATA +2 -1
- {tinybird-0.0.1.dev267.dist-info → tinybird-0.0.1.dev269.dist-info}/RECORD +21 -21
- tinybird/tb/modules/agent/tools/explore.py +0 -15
- tinybird/tb/modules/agent/tools/preview_datafile.py +0 -24
- {tinybird-0.0.1.dev267.dist-info → tinybird-0.0.1.dev269.dist-info}/WHEEL +0 -0
- {tinybird-0.0.1.dev267.dist-info → tinybird-0.0.1.dev269.dist-info}/entry_points.txt +0 -0
- {tinybird-0.0.1.dev267.dist-info → tinybird-0.0.1.dev269.dist-info}/top_level.txt +0 -0
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.
|
|
8
|
-
__revision__ = '
|
|
7
|
+
__version__ = '0.0.1.dev269'
|
|
8
|
+
__revision__ = '24737f5'
|
|
@@ -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.
|
|
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] =
|
|
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=[
|
|
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
|
|
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
|
|
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],
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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)
|