tinybird 0.0.1.dev238__py3-none-any.whl → 0.0.1.dev240__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/client.py +1 -1
- tinybird/tb/modules/agent/__init__.py +3 -0
- tinybird/tb/modules/agent/agent.py +212 -0
- tinybird/tb/modules/agent/banner.py +55 -0
- tinybird/tb/modules/agent/client.py +42 -0
- tinybird/tb/modules/agent/memory.py +41 -0
- tinybird/tb/modules/agent/models.py +20 -0
- tinybird/tb/modules/agent/prompts.py +83 -0
- tinybird/tb/modules/agent/tools/__init__.py +0 -0
- tinybird/tb/modules/agent/tools/create_datafile.py +62 -0
- tinybird/tb/modules/agent/tools/explore.py +15 -0
- tinybird/tb/modules/agent/tools/plan.py +45 -0
- tinybird/tb/modules/agent/tools/preview_datafile.py +24 -0
- tinybird/tb/modules/agent/utils.py +382 -0
- tinybird/tb/modules/cli.py +1 -1
- tinybird/tb/modules/local.py +11 -0
- {tinybird-0.0.1.dev238.dist-info → tinybird-0.0.1.dev240.dist-info}/METADATA +4 -3
- {tinybird-0.0.1.dev238.dist-info → tinybird-0.0.1.dev240.dist-info}/RECORD +22 -10
- tinybird/tb/modules/agent.py +0 -80
- {tinybird-0.0.1.dev238.dist-info → tinybird-0.0.1.dev240.dist-info}/WHEEL +0 -0
- {tinybird-0.0.1.dev238.dist-info → tinybird-0.0.1.dev240.dist-info}/entry_points.txt +0 -0
- {tinybird-0.0.1.dev238.dist-info → tinybird-0.0.1.dev240.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.dev240'
|
|
8
|
+
__revision__ = '268407c'
|
tinybird/tb/client.py
CHANGED
|
@@ -1396,5 +1396,5 @@ class TinyB:
|
|
|
1396
1396
|
self._req(f"/v0/tags/{name}", method="DELETE")
|
|
1397
1397
|
|
|
1398
1398
|
def explore_data(self, prompt: str) -> str:
|
|
1399
|
-
params = urlencode({"prompt": prompt, "host": self.host})
|
|
1399
|
+
params = urlencode({"prompt": prompt, "host": self.host, "origin": "cli"})
|
|
1400
1400
|
return self._req(f"/v1/agents/explore?{params}")
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
|
|
4
|
+
import click
|
|
5
|
+
from prompt_toolkit import prompt
|
|
6
|
+
from prompt_toolkit.cursor_shapes import CursorShape
|
|
7
|
+
from prompt_toolkit.styles import Style
|
|
8
|
+
from pydantic_ai import Agent, Tool
|
|
9
|
+
from pydantic_ai.messages import ModelMessage
|
|
10
|
+
|
|
11
|
+
from tinybird.prompts import (
|
|
12
|
+
connection_instructions,
|
|
13
|
+
copy_pipe_instructions,
|
|
14
|
+
datasource_example,
|
|
15
|
+
datasource_instructions,
|
|
16
|
+
gcs_connection_example,
|
|
17
|
+
kafka_connection_example,
|
|
18
|
+
materialized_pipe_instructions,
|
|
19
|
+
pipe_example,
|
|
20
|
+
pipe_instructions,
|
|
21
|
+
s3_connection_example,
|
|
22
|
+
sink_pipe_instructions,
|
|
23
|
+
)
|
|
24
|
+
from tinybird.tb.modules.agent.banner import display_banner
|
|
25
|
+
from tinybird.tb.modules.agent.client import TinybirdClient
|
|
26
|
+
from tinybird.tb.modules.agent.memory import clear_history, load_history
|
|
27
|
+
from tinybird.tb.modules.agent.models import create_model
|
|
28
|
+
from tinybird.tb.modules.agent.prompts import datafile_instructions, plan_instructions, sql_instructions
|
|
29
|
+
from tinybird.tb.modules.agent.tools.create_datafile import create_datafile
|
|
30
|
+
from tinybird.tb.modules.agent.tools.explore import explore_data
|
|
31
|
+
from tinybird.tb.modules.agent.tools.plan import plan
|
|
32
|
+
from tinybird.tb.modules.agent.tools.preview_datafile import preview_datafile
|
|
33
|
+
from tinybird.tb.modules.agent.utils import TinybirdAgentContext
|
|
34
|
+
from tinybird.tb.modules.feedback_manager import FeedbackManager
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class TinybirdAgent:
|
|
38
|
+
def __init__(self, token: str, host: str, folder: str):
|
|
39
|
+
self.token = token
|
|
40
|
+
self.host = host
|
|
41
|
+
self.folder = folder
|
|
42
|
+
self.messages: list[ModelMessage] = []
|
|
43
|
+
self.agent = Agent(
|
|
44
|
+
model=create_model(token, host),
|
|
45
|
+
deps_type=TinybirdAgentContext,
|
|
46
|
+
system_prompt=f"""
|
|
47
|
+
You are a Tinybird Code, an agentic CLI that can help users to work with Tinybird.
|
|
48
|
+
|
|
49
|
+
You are an interactive CLI tool that helps users with data engineering tasks. Use the instructions below and the tools available to you to assist the user.
|
|
50
|
+
|
|
51
|
+
# Tone and style
|
|
52
|
+
You should be concise, direct, and to the point.
|
|
53
|
+
Remember that your output will be displayed on a command line interface. Your responses can use Github-flavored markdown for formatting.
|
|
54
|
+
Output text to communicate with the user; all text you output outside of tool use is displayed to the user. Only use tools to complete tasks. Never use tools like Bash or code comments as means to communicate with the user during the session.
|
|
55
|
+
If you cannot or will not help the user with something, please do not say why or what it could lead to, since this comes across as preachy and annoying. Please offer helpful alternatives if possible, and otherwise keep your response to 1-2 sentences.
|
|
56
|
+
IMPORTANT: You should minimize output tokens as much as possible while maintaining helpfulness, quality, and accuracy. Only address the specific query or task at hand, avoiding tangential information unless absolutely critical for completing the request. If you can answer in 1-3 sentences or a short paragraph, please do.
|
|
57
|
+
IMPORTANT: You should NOT answer with unnecessary preamble or postamble (such as explaining your code or summarizing your action), unless the user asks you to.
|
|
58
|
+
IMPORTANT: Keep your responses short, since they will be displayed on a command line interface. You MUST answer concisely with fewer than 4 lines (not including tool use or code generation), unless user asks for detail. Answer the user's question directly, without elaboration, explanation, or details. One word answers are best. Avoid introductions, conclusions, and explanations. You MUST avoid text before/after your response, such as "The answer is <answer>.", "Here is the content of the file..." or "Based on the information provided, the answer is..." or "Here is what I will do next...". Here are some examples to demonstrate appropriate verbosity:
|
|
59
|
+
|
|
60
|
+
# Proactiveness
|
|
61
|
+
You are allowed to be proactive, but only when the user asks you to do something. You should strive to strike a balance between:
|
|
62
|
+
Doing the right thing when asked, including taking actions and follow-up actions
|
|
63
|
+
Not surprising the user with actions you take without asking
|
|
64
|
+
For example, if the user asks you how to approach something, you should do your best to answer their question first, and not immediately jump into taking actions.
|
|
65
|
+
Do not add additional code explanation summary unless requested by the user. After working on a file, just stop, rather than providing an explanation of what you did.
|
|
66
|
+
|
|
67
|
+
# Code style
|
|
68
|
+
IMPORTANT: DO NOT ADD ANY COMMENTS unless asked by the user.
|
|
69
|
+
|
|
70
|
+
# Tools
|
|
71
|
+
You have access to the following tools:
|
|
72
|
+
1. `explore_data` - Explore data in the current workspace
|
|
73
|
+
2. `preview_datafile` - Preview the content of a datafile (datasource, endpoint, materialized, sink, copy, connection).
|
|
74
|
+
3. `create_datafile` - Create a file in the project folder. Confirmation will be asked by the tool before creating the file.
|
|
75
|
+
4. `plan` - Plan the creation or update of resources.
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# When creating or updating datafiles:
|
|
79
|
+
1. Use `plan` tool to plan the creation or update of resources.
|
|
80
|
+
2. If the user confirms the plan, go from 3 to 7 steps until all the resources are created, updated or skipped.
|
|
81
|
+
3. Use `preview_datafile` tool to preview the content of a datafile.
|
|
82
|
+
4. Without asking, use the `create_datafile` tool to create the datafile, because it will ask for confirmation before creating the file.
|
|
83
|
+
5. Check the result of the `create_datafile` tool to see if the datafile was created successfully.
|
|
84
|
+
6. If the datafile was created successfully, report the result to the user.
|
|
85
|
+
7. If the datafile was not created successfully, finish the process and just wait for a new user prompt.
|
|
86
|
+
|
|
87
|
+
IMPORTANT: If the user cancels some of the steps or there is an error, DO NOT continue with the plan. Stop the process and wait for the user before using any other tool.
|
|
88
|
+
|
|
89
|
+
# When planning the creation or update of resources:
|
|
90
|
+
{plan_instructions}
|
|
91
|
+
{datafile_instructions}
|
|
92
|
+
|
|
93
|
+
# Working with datasource files:
|
|
94
|
+
{datasource_instructions}
|
|
95
|
+
{datasource_example}
|
|
96
|
+
|
|
97
|
+
# Working with any type of pipe file:
|
|
98
|
+
{pipe_instructions}
|
|
99
|
+
{pipe_example}
|
|
100
|
+
|
|
101
|
+
# Working with materialized pipe files:
|
|
102
|
+
{materialized_pipe_instructions}
|
|
103
|
+
|
|
104
|
+
# Working with sink pipe files:
|
|
105
|
+
{sink_pipe_instructions}
|
|
106
|
+
|
|
107
|
+
# Working with copy pipe files:
|
|
108
|
+
{copy_pipe_instructions}
|
|
109
|
+
|
|
110
|
+
# Working with SQL queries:
|
|
111
|
+
{sql_instructions}
|
|
112
|
+
|
|
113
|
+
# Working with connections files:
|
|
114
|
+
{connection_instructions}
|
|
115
|
+
|
|
116
|
+
# Connection examples:
|
|
117
|
+
Kafka: {kafka_connection_example}
|
|
118
|
+
S3: {s3_connection_example}
|
|
119
|
+
GCS: {gcs_connection_example}
|
|
120
|
+
|
|
121
|
+
# Info
|
|
122
|
+
Today is {datetime.now().strftime("%Y-%m-%d")}
|
|
123
|
+
""",
|
|
124
|
+
tools=[
|
|
125
|
+
Tool(explore_data, docstring_format="google", require_parameter_descriptions=True, takes_ctx=True),
|
|
126
|
+
Tool(preview_datafile, docstring_format="google", require_parameter_descriptions=True, takes_ctx=False),
|
|
127
|
+
Tool(create_datafile, docstring_format="google", require_parameter_descriptions=True, takes_ctx=True),
|
|
128
|
+
Tool(plan, docstring_format="google", require_parameter_descriptions=True, takes_ctx=False),
|
|
129
|
+
],
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
def _keep_recent_messages(self) -> list[ModelMessage]:
|
|
133
|
+
"""Keep only the last 5 messages to manage token usage."""
|
|
134
|
+
return self.messages[-5:] if len(self.messages) > 5 else self.messages
|
|
135
|
+
|
|
136
|
+
def run(self, user_prompt: str) -> None:
|
|
137
|
+
result = self.agent.run_sync(
|
|
138
|
+
user_prompt,
|
|
139
|
+
deps=TinybirdAgentContext(
|
|
140
|
+
client=TinybirdClient(token=self.token, host=self.host),
|
|
141
|
+
folder=self.folder,
|
|
142
|
+
),
|
|
143
|
+
message_history=self.messages,
|
|
144
|
+
)
|
|
145
|
+
new_messages = result.new_messages()
|
|
146
|
+
self.messages.extend(new_messages)
|
|
147
|
+
click.echo("\n")
|
|
148
|
+
click.echo(result.output)
|
|
149
|
+
click.echo("\n")
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def run_agent(token: str, host: str, folder: str):
|
|
153
|
+
display_banner()
|
|
154
|
+
|
|
155
|
+
try:
|
|
156
|
+
agent = TinybirdAgent(token, host, folder)
|
|
157
|
+
click.echo()
|
|
158
|
+
click.echo(FeedbackManager.success(message="Welcome to Tinybird Code"))
|
|
159
|
+
click.echo(FeedbackManager.info(message="Describe what you want to create and I'll help you build it"))
|
|
160
|
+
click.echo(FeedbackManager.info(message="Commands: 'exit', 'quit', 'help', or Ctrl+C to exit"))
|
|
161
|
+
click.echo()
|
|
162
|
+
|
|
163
|
+
except Exception as e:
|
|
164
|
+
click.echo(FeedbackManager.error(message=f"Failed to initialize agent: {e}"))
|
|
165
|
+
return
|
|
166
|
+
|
|
167
|
+
# Interactive loop
|
|
168
|
+
try:
|
|
169
|
+
while True:
|
|
170
|
+
try:
|
|
171
|
+
user_input = prompt(
|
|
172
|
+
[("class:prompt", "tb » ")],
|
|
173
|
+
history=load_history(),
|
|
174
|
+
cursor=CursorShape.BLOCK,
|
|
175
|
+
style=Style.from_dict(
|
|
176
|
+
{
|
|
177
|
+
"prompt": "#40a8a8 bold",
|
|
178
|
+
"": "", # Normal color for user input
|
|
179
|
+
}
|
|
180
|
+
),
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
if user_input.lower() in ["exit", "quit"]:
|
|
184
|
+
click.echo(FeedbackManager.info(message="Goodbye!"))
|
|
185
|
+
break
|
|
186
|
+
elif user_input.lower() == "clear":
|
|
187
|
+
clear_history()
|
|
188
|
+
continue
|
|
189
|
+
elif user_input.lower() == "help":
|
|
190
|
+
click.echo()
|
|
191
|
+
click.echo(FeedbackManager.info(message="Tinybird Code Help:"))
|
|
192
|
+
click.echo("• Describe what you want to create: 'Create a user analytics system'")
|
|
193
|
+
click.echo("• Ask for specific resources: 'Create a pipe to aggregate daily clicks'")
|
|
194
|
+
click.echo("• Request data sources: 'Set up a Kafka connection for events'")
|
|
195
|
+
click.echo("• Type 'exit' or 'quit' to leave")
|
|
196
|
+
click.echo()
|
|
197
|
+
continue
|
|
198
|
+
elif user_input.strip() == "":
|
|
199
|
+
continue
|
|
200
|
+
else:
|
|
201
|
+
agent.run(user_input)
|
|
202
|
+
|
|
203
|
+
except KeyboardInterrupt:
|
|
204
|
+
click.echo(FeedbackManager.info(message="Goodbye!"))
|
|
205
|
+
break
|
|
206
|
+
except EOFError:
|
|
207
|
+
click.echo(FeedbackManager.info(message="Goodbye!"))
|
|
208
|
+
break
|
|
209
|
+
|
|
210
|
+
except Exception as e:
|
|
211
|
+
click.echo(FeedbackManager.error(message=f"Error: {e}"))
|
|
212
|
+
sys.exit(1)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import click
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def display_banner():
|
|
5
|
+
reset = "\033[0m"
|
|
6
|
+
|
|
7
|
+
click.echo("\n")
|
|
8
|
+
# The Tinybird Code ASCII art banner
|
|
9
|
+
banner = [
|
|
10
|
+
" ████████╗██╗███╗ ██╗██╗ ██╗██████╗ ██╗██████╗ ██████╗ ██████╗ ██████╗ ██████╗ ███████╗",
|
|
11
|
+
" ╚══██╔══╝██║████╗ ██║╚██╗ ██╔╝██╔══██╗██║██╔══██╗██╔══██╗ ██╔════╝██╔═══██╗██╔══██╗██╔════╝",
|
|
12
|
+
" ██║ ██║██╔██╗ ██║ ╚████╔╝ ██████╔╝██║██████╔╝██║ ██║ ██║ ██║ ██║██║ ██║█████╗ ",
|
|
13
|
+
" ██║ ██║██║╚██╗██║ ╚██╔╝ ██╔══██╗██║██╔══██╗██║ ██║ ██║ ██║ ██║██║ ██║██╔══╝ ",
|
|
14
|
+
" ██║ ██║██║ ╚████║ ██║ ██████╔╝██║██║ ██║██████╔╝ ╚██████╗╚██████╔╝██████╔╝███████╗",
|
|
15
|
+
" ╚═╝ ╚═╝╚═╝ ╚═══╝ ╚═╝ ╚═════╝ ╚═╝╚═╝ ╚═╝╚═════╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
def interpolate_color(start_rgb, end_rgb, factor):
|
|
19
|
+
"""Interpolate between two RGB colors"""
|
|
20
|
+
return [int(start_rgb[i] + (end_rgb[i] - start_rgb[i]) * factor) for i in range(3)]
|
|
21
|
+
|
|
22
|
+
def rgb_to_ansi(r, g, b):
|
|
23
|
+
"""Convert RGB values to ANSI escape code"""
|
|
24
|
+
return f"\033[38;2;{r};{g};{b}m"
|
|
25
|
+
|
|
26
|
+
# Define start and end colors for smooth gradient
|
|
27
|
+
start_color = [0, 128, 128] # Deep teal
|
|
28
|
+
end_color = [100, 200, 180] # Light turquoise
|
|
29
|
+
|
|
30
|
+
# Print each line with a very smooth horizontal gradient
|
|
31
|
+
for line in banner:
|
|
32
|
+
colored_line = ""
|
|
33
|
+
# Count non-space characters for gradient calculation
|
|
34
|
+
non_space_chars = sum(1 for char in line if char != " ")
|
|
35
|
+
char_count = 0
|
|
36
|
+
|
|
37
|
+
for char in line:
|
|
38
|
+
if char == " ":
|
|
39
|
+
colored_line += char
|
|
40
|
+
continue
|
|
41
|
+
|
|
42
|
+
# Calculate smooth gradient position (0.0 to 1.0)
|
|
43
|
+
if non_space_chars > 1:
|
|
44
|
+
gradient_position = char_count / (non_space_chars - 1)
|
|
45
|
+
else:
|
|
46
|
+
gradient_position = 0
|
|
47
|
+
|
|
48
|
+
# Interpolate color
|
|
49
|
+
current_rgb = interpolate_color(start_color, end_color, gradient_position)
|
|
50
|
+
color_code = rgb_to_ansi(*current_rgb)
|
|
51
|
+
|
|
52
|
+
colored_line += f"{color_code}{char}"
|
|
53
|
+
char_count += 1
|
|
54
|
+
|
|
55
|
+
click.echo(colored_line + reset)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Any, Dict, Optional
|
|
3
|
+
|
|
4
|
+
import httpx
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class TinybirdClient:
|
|
9
|
+
def __init__(self, host: str, token: str):
|
|
10
|
+
self.host = host
|
|
11
|
+
self.token = token
|
|
12
|
+
self.client = httpx.Client(
|
|
13
|
+
timeout=30.0,
|
|
14
|
+
headers={"Accept": "application/json", "User-Agent": "Python/APIClient"},
|
|
15
|
+
)
|
|
16
|
+
self.insights: list[str] = []
|
|
17
|
+
|
|
18
|
+
def __aenter__(self):
|
|
19
|
+
return self
|
|
20
|
+
|
|
21
|
+
def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
22
|
+
self.close()
|
|
23
|
+
|
|
24
|
+
def close(self):
|
|
25
|
+
"""Close the underlying HTTP client."""
|
|
26
|
+
self.client.close()
|
|
27
|
+
|
|
28
|
+
def _get(self, endpoint: str, params: Optional[Dict[str, Any]] = None) -> str:
|
|
29
|
+
if params is None:
|
|
30
|
+
params = {}
|
|
31
|
+
params["token"] = self.token
|
|
32
|
+
url = f"{self.host}{endpoint}"
|
|
33
|
+
response = self.client.get(url, params=params)
|
|
34
|
+
try:
|
|
35
|
+
response.raise_for_status()
|
|
36
|
+
except Exception as e:
|
|
37
|
+
raise Exception(response.json().get("error", str(e))) from e
|
|
38
|
+
return response.text
|
|
39
|
+
|
|
40
|
+
def explore_data(self, prompt: str) -> str:
|
|
41
|
+
params = {"prompt": prompt, "host": self.host, "origin": "cli"}
|
|
42
|
+
return self._get("/v1/agents/explore", params)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
from prompt_toolkit.history import FileHistory
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def get_history_file_path():
|
|
8
|
+
"""Get the history file path based on current working directory"""
|
|
9
|
+
# Get current working directory
|
|
10
|
+
cwd = Path.cwd()
|
|
11
|
+
|
|
12
|
+
# Get user's home directory
|
|
13
|
+
home = Path.home()
|
|
14
|
+
|
|
15
|
+
# Calculate relative path from home to current directory
|
|
16
|
+
try:
|
|
17
|
+
relative_path = cwd.relative_to(home)
|
|
18
|
+
except ValueError:
|
|
19
|
+
# If current directory is not under home, use absolute path components
|
|
20
|
+
relative_path = Path(*cwd.parts[1:]) if cwd.is_absolute() else cwd
|
|
21
|
+
|
|
22
|
+
# Create history directory structure
|
|
23
|
+
history_dir = home / ".tinybird" / "projects" / relative_path
|
|
24
|
+
history_dir.mkdir(parents=True, exist_ok=True)
|
|
25
|
+
|
|
26
|
+
# Return history file path
|
|
27
|
+
return history_dir / "history.txt"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def load_history() -> Optional[FileHistory]:
|
|
31
|
+
try:
|
|
32
|
+
history_file = get_history_file_path()
|
|
33
|
+
return FileHistory(str(history_file))
|
|
34
|
+
except Exception:
|
|
35
|
+
return None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def clear_history():
|
|
39
|
+
"""Clear the history file"""
|
|
40
|
+
history_file = get_history_file_path()
|
|
41
|
+
history_file.unlink(missing_ok=True)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from anthropic import AsyncAnthropic
|
|
2
|
+
from httpx import AsyncClient
|
|
3
|
+
from pydantic_ai.models.anthropic import AnthropicModel, AnthropicModelName
|
|
4
|
+
from pydantic_ai.providers.anthropic import AnthropicProvider
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def create_model(
|
|
8
|
+
token: str,
|
|
9
|
+
base_url: str = "https://api.wadus3.aws.tinybird.co",
|
|
10
|
+
model: AnthropicModelName = "claude-4-sonnet-20250514",
|
|
11
|
+
):
|
|
12
|
+
client = AsyncAnthropic(
|
|
13
|
+
base_url=base_url,
|
|
14
|
+
http_client=AsyncClient(params={"token": token}),
|
|
15
|
+
auth_token=token,
|
|
16
|
+
)
|
|
17
|
+
return AnthropicModel(
|
|
18
|
+
model_name=model,
|
|
19
|
+
provider=AnthropicProvider(anthropic_client=client),
|
|
20
|
+
)
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
plan_instructions = """
|
|
2
|
+
When asked to create a plan, you MUST respond with this EXACT format and NOTHING ELSE:
|
|
3
|
+
|
|
4
|
+
PLAN_DESCRIPTION: [One sentence describing what will be built]
|
|
5
|
+
|
|
6
|
+
STEPS:
|
|
7
|
+
1. CREATE_DATASOURCE: [name] - [description] - DEPENDS_ON: none
|
|
8
|
+
2. CREATE_ENDPOINT: [name] - [description] - DEPENDS_ON: [resource_name]
|
|
9
|
+
3. CREATE_MATERIALIZED_PIPE: [name] - [description] - DEPENDS_ON: [resource_name]
|
|
10
|
+
4. CREATE_MATERIALIZED_DATASOURCE: [name] - [description] - DEPENDS_ON: [resource_name]
|
|
11
|
+
5. CREATE_SINK: [name] - [description] - DEPENDS_ON: [resource_name]
|
|
12
|
+
6. CREATE_COPY: [name] - [description] - DEPENDS_ON: [resource_name]
|
|
13
|
+
7. CREATE_CONNECTION: [name] - [description] - DEPENDS_ON: none
|
|
14
|
+
|
|
15
|
+
You can skip steps where resources will not be created or updated.
|
|
16
|
+
|
|
17
|
+
ESTIMATED_RESOURCES: [total number of steps]
|
|
18
|
+
|
|
19
|
+
RESOURCE_DEPENDENCIES:
|
|
20
|
+
[resource_name]: [resource_name]
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
sql_instructions = """
|
|
25
|
+
<sql_instructions>
|
|
26
|
+
- The SQL query must be a valid ClickHouse SQL query that mixes ClickHouse syntax and Tinybird templating syntax (Tornado templating language under the hood).
|
|
27
|
+
- SQL queries with parameters must start with "%" character and a newline on top of every query to be able to use the parameters. Examples:
|
|
28
|
+
<invalid_query_with_parameters_no_%_on_top>
|
|
29
|
+
SELECT * FROM events WHERE session_id={{String(my_param, "default_value")}}
|
|
30
|
+
</invalid_query_with_parameters_no_%_on_top>
|
|
31
|
+
<valid_query_with_parameters_with_%_on_top>
|
|
32
|
+
%
|
|
33
|
+
SELECT * FROM events WHERE session_id={{String(my_param, "default_value")}}
|
|
34
|
+
</valid_query_with_parameters_with_%_on_top>
|
|
35
|
+
- The Parameter functions like this one {{String(my_param_name,default_value)}} can be one of the following: String, DateTime, Date, Float32, Float64, Int, Integer, UInt8, UInt16, UInt32, UInt64, UInt128, UInt256, Int8, Int16, Int32, Int64, Int128, Int256
|
|
36
|
+
- Parameter names must be different from column names. Pass always the param name and a default value to the function.
|
|
37
|
+
- Use ALWAYS hardcoded values for default values for parameters.
|
|
38
|
+
- Code inside the template {{template_expression}} follows the rules of Tornado templating language so no module is allowed to be imported. So for example you can't use now() as default value for a DateTime parameter. You need an if else block like this:
|
|
39
|
+
<invalid_condition_with_now>
|
|
40
|
+
AND timestamp BETWEEN {{DateTime(start_date, now() - interval 30 day)}} AND {{DateTime(end_date, now())}}
|
|
41
|
+
</invalid_condition_with_now>
|
|
42
|
+
<valid_condition_without_now>
|
|
43
|
+
{{%if not defined(start_date)%}}
|
|
44
|
+
timestamp BETWEEN now() - interval 30 day
|
|
45
|
+
{{%else%}}
|
|
46
|
+
timestamp BETWEEN {{DateTime(start_date)}}
|
|
47
|
+
{{%end%}}
|
|
48
|
+
{{%if not defined(end_date)%}}
|
|
49
|
+
AND now()
|
|
50
|
+
{{%else%}}
|
|
51
|
+
AND {{DateTime(end_date)}}
|
|
52
|
+
{{%end%}}
|
|
53
|
+
</valid_condition_without_now>
|
|
54
|
+
- Parameters must not be quoted.
|
|
55
|
+
- When you use defined function with a paremeter inside, do NOT add quotes around the parameter:
|
|
56
|
+
<invalid_defined_function_with_parameter>{{% if defined('my_param') %}}</invalid_defined_function_with_parameter>
|
|
57
|
+
<valid_defined_function_without_parameter>{{% if defined(my_param) %}}</valid_defined_function_without_parameter>
|
|
58
|
+
- Use datasource names as table names when doing SELECT statements.
|
|
59
|
+
- Do not use pipe names as table names.
|
|
60
|
+
- The available datasource names to use in the SQL are the ones present in the existing_resources section or the ones you will create.
|
|
61
|
+
- Use node names as table names only when nodes are present in the same file.
|
|
62
|
+
- Do not reference the current node name in the SQL.
|
|
63
|
+
- SQL queries only accept SELECT statements with conditions, aggregations, joins, etc.
|
|
64
|
+
- Do NOT use CREATE TABLE, INSERT INTO, CREATE DATABASE, etc.
|
|
65
|
+
- Use ONLY SELECT statements in the SQL section.
|
|
66
|
+
- INSERT INTO is not supported in SQL section.
|
|
67
|
+
- When using functions try always ClickHouse functions first, then SQL functions.
|
|
68
|
+
- Parameters are never quoted in any case.
|
|
69
|
+
- Use the following syntax in the SQL section for the iceberg table function: iceberg('s3://bucket/path/to/table', {{tb_secret('aws_access_key_id')}}, {{tb_secret('aws_secret_access_key')}})
|
|
70
|
+
- Use the following syntax in the SQL section for the postgres table function: postgresql('host:port', 'database', 'table', {{tb_secret('db_username')}}, {{tb_secret('db_password')}}), 'schema')
|
|
71
|
+
</sql_instructions>
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
datafile_instructions = """
|
|
75
|
+
<datafile_instructions>
|
|
76
|
+
- Endpoint files will be created under the `/endpoints` folder.
|
|
77
|
+
- Materialized pipe files will be created under the `/materialized` folder.
|
|
78
|
+
- Sink pipe files will be created under the `/sinks` folder.
|
|
79
|
+
- Copy pipe files will be created under the `/copies` folder.
|
|
80
|
+
- Connection files will be created under the `/connections` folder.
|
|
81
|
+
- Datasource files will be created under the `/datasources` folder.
|
|
82
|
+
</datafile_instructions>
|
|
83
|
+
"""
|
|
File without changes
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
from pydantic_ai import RunContext
|
|
5
|
+
|
|
6
|
+
from tinybird.tb.modules.agent.utils import Datafile, TinybirdAgentContext, show_options
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def get_resource_confirmation(resource: Datafile) -> bool:
|
|
10
|
+
"""Get user confirmation for creating a resource"""
|
|
11
|
+
while True:
|
|
12
|
+
result = show_options(
|
|
13
|
+
options=[f"Yes, create {resource.type} '{resource.name}'", "No, and tell Tinybird Code what to do"],
|
|
14
|
+
title=f"What would you like to do with {resource.type} '{resource.name}'?",
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
if result is None: # Cancelled
|
|
18
|
+
return False
|
|
19
|
+
|
|
20
|
+
if result.startswith("Yes"):
|
|
21
|
+
return True
|
|
22
|
+
elif result.startswith("No"):
|
|
23
|
+
return False
|
|
24
|
+
|
|
25
|
+
return False
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def create_datafile(ctx: RunContext[TinybirdAgentContext], resource: Datafile) -> str:
|
|
29
|
+
"""Given a resource representation, create a file in the project folder
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
resource (Datafile): The resource to create. Required.
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
str: If the resource was created or not.
|
|
36
|
+
"""
|
|
37
|
+
try:
|
|
38
|
+
click.echo()
|
|
39
|
+
click.echo(resource.content)
|
|
40
|
+
confirmation = get_resource_confirmation(resource)
|
|
41
|
+
|
|
42
|
+
if not confirmation:
|
|
43
|
+
return f"Resource {resource.pathname} was not created. User cancelled creation."
|
|
44
|
+
|
|
45
|
+
resource.pathname = resource.pathname.removeprefix("/")
|
|
46
|
+
|
|
47
|
+
path = Path(ctx.deps.folder) / resource.pathname
|
|
48
|
+
|
|
49
|
+
folder_path = path.parent
|
|
50
|
+
|
|
51
|
+
if not folder_path.exists():
|
|
52
|
+
folder_path.mkdir()
|
|
53
|
+
|
|
54
|
+
if not path.exists():
|
|
55
|
+
path.touch()
|
|
56
|
+
|
|
57
|
+
path.write_text(resource.content)
|
|
58
|
+
|
|
59
|
+
return f"Created {resource.pathname}"
|
|
60
|
+
|
|
61
|
+
except Exception as e:
|
|
62
|
+
return f"Error creating {resource.pathname}: {e}"
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from pydantic_ai import RunContext
|
|
2
|
+
|
|
3
|
+
from tinybird.tb.modules.agent.utils import TinybirdAgentContext
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def explore_data(ctx: RunContext[TinybirdAgentContext], prompt: str):
|
|
7
|
+
"""Explore production data in the current workspace
|
|
8
|
+
|
|
9
|
+
Args:
|
|
10
|
+
prompt (str): The prompt to explore data with. Required.
|
|
11
|
+
|
|
12
|
+
Returns:
|
|
13
|
+
str: The result of the exploration.
|
|
14
|
+
"""
|
|
15
|
+
return ctx.deps.client.explore_data(prompt)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import click
|
|
2
|
+
|
|
3
|
+
from tinybird.tb.modules.agent.utils import show_options
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def get_plan_confirmation() -> bool:
|
|
7
|
+
"""Get user confirmation for implementing a plan"""
|
|
8
|
+
while True:
|
|
9
|
+
result = show_options(
|
|
10
|
+
options=["Yes, implement the plan", "No, and tell Tinybird Code what to do"],
|
|
11
|
+
title="Do you want to implement the plan?",
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
if result is None: # Cancelled
|
|
15
|
+
return False
|
|
16
|
+
|
|
17
|
+
if result.startswith("Yes"):
|
|
18
|
+
return True
|
|
19
|
+
elif result.startswith("No"):
|
|
20
|
+
return False
|
|
21
|
+
|
|
22
|
+
return False
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def plan(plan: str) -> str:
|
|
26
|
+
"""Given a plan, ask the user for confirmation to implement it
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
plan (str): The plan to implement. Required.
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
str: If the plan was implemented or not.
|
|
33
|
+
"""
|
|
34
|
+
try:
|
|
35
|
+
click.echo()
|
|
36
|
+
click.echo(plan)
|
|
37
|
+
confirmation = get_plan_confirmation()
|
|
38
|
+
|
|
39
|
+
if not confirmation:
|
|
40
|
+
return "Plan was not implemented. User cancelled implementation."
|
|
41
|
+
|
|
42
|
+
return "User confirmed the plan. Implementing..."
|
|
43
|
+
|
|
44
|
+
except Exception as e:
|
|
45
|
+
return f"Error implementing the plan: {e}"
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from tinybird.tb.modules.agent.utils import Datafile
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def preview_datafile(name: str, type: str, description: str, content: str, pathname: str) -> Datafile:
|
|
5
|
+
"""Preview the content of a datafile before creating it
|
|
6
|
+
|
|
7
|
+
Args:
|
|
8
|
+
name (str): The name of the datafile. Required.
|
|
9
|
+
type (str): The type of the datafile. Options: datasource, endpoint, materialized, sink, copy, connection. Required.
|
|
10
|
+
description (str): The description of the datafile. Required.
|
|
11
|
+
content (str): The content of the datafile. Required.
|
|
12
|
+
pathname (str): The pathname of the datafile where the file will be created. Required.
|
|
13
|
+
|
|
14
|
+
Returns:
|
|
15
|
+
Datafile: The datafile preview.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
return Datafile(
|
|
19
|
+
type=type,
|
|
20
|
+
name=name,
|
|
21
|
+
content=content,
|
|
22
|
+
description=description,
|
|
23
|
+
pathname=pathname,
|
|
24
|
+
)
|
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from contextlib import contextmanager
|
|
3
|
+
from typing import Any, List, Optional
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
from prompt_toolkit.application import Application, get_app
|
|
7
|
+
from prompt_toolkit.filters import IsDone
|
|
8
|
+
from prompt_toolkit.key_binding import KeyBindings
|
|
9
|
+
from prompt_toolkit.layout import Layout
|
|
10
|
+
from prompt_toolkit.layout.containers import ConditionalContainer, HSplit, Window
|
|
11
|
+
from prompt_toolkit.layout.controls import FormattedTextControl
|
|
12
|
+
from prompt_toolkit.layout.dimension import LayoutDimension as D
|
|
13
|
+
from prompt_toolkit.mouse_events import MouseEventType
|
|
14
|
+
from prompt_toolkit.patch_stdout import patch_stdout as pt_patch_stdout
|
|
15
|
+
from prompt_toolkit.shortcuts import PromptSession
|
|
16
|
+
from prompt_toolkit.styles import Style
|
|
17
|
+
from pydantic import BaseModel, Field
|
|
18
|
+
|
|
19
|
+
from tinybird.tb.modules.agent.client import TinybirdClient
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class TinybirdAgentContext(BaseModel):
|
|
23
|
+
client: TinybirdClient
|
|
24
|
+
folder: str
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
default_style = Style.from_dict(
|
|
28
|
+
{
|
|
29
|
+
"separator": "#6C6C6C",
|
|
30
|
+
"questionmark": "#FF9D00 bold",
|
|
31
|
+
"selected": "#5F819D",
|
|
32
|
+
"pointer": "#FF9D00 bold",
|
|
33
|
+
"instruction": "", # default
|
|
34
|
+
"answer": "#5F819D bold",
|
|
35
|
+
"question": "",
|
|
36
|
+
}
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def if_mousedown(handler):
|
|
41
|
+
def handle_if_mouse_down(mouse_event):
|
|
42
|
+
if mouse_event.event_type == MouseEventType.MOUSE_DOWN:
|
|
43
|
+
return handler(mouse_event)
|
|
44
|
+
else:
|
|
45
|
+
return NotImplemented
|
|
46
|
+
|
|
47
|
+
return handle_if_mouse_down
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class Separator:
|
|
51
|
+
line = "-" * 15
|
|
52
|
+
|
|
53
|
+
def __init__(self, line=None):
|
|
54
|
+
if line:
|
|
55
|
+
self.line = line
|
|
56
|
+
|
|
57
|
+
def __str__(self):
|
|
58
|
+
return self.line
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class PromptParameterException(ValueError):
|
|
62
|
+
def __init__(self, message, errors=None):
|
|
63
|
+
# Call the base class constructor with the parameters it needs
|
|
64
|
+
super().__init__("You must provide a `%s` value" % message, errors)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class InquirerControl(FormattedTextControl):
|
|
68
|
+
def __init__(self, choices, default, **kwargs):
|
|
69
|
+
self.selected_option_index = 0
|
|
70
|
+
self.answered = False
|
|
71
|
+
self.choices = choices
|
|
72
|
+
self._init_choices(choices, default)
|
|
73
|
+
super().__init__(self._get_choice_tokens, **kwargs)
|
|
74
|
+
|
|
75
|
+
def _init_choices(self, choices, default):
|
|
76
|
+
# helper to convert from question format to internal format
|
|
77
|
+
self.choices = [] # list (name, value, disabled)
|
|
78
|
+
searching_first_choice = True
|
|
79
|
+
for i, c in enumerate(choices):
|
|
80
|
+
if isinstance(c, Separator):
|
|
81
|
+
self.choices.append((c, None, None))
|
|
82
|
+
else:
|
|
83
|
+
if isinstance(c, str):
|
|
84
|
+
self.choices.append((c, c, None))
|
|
85
|
+
else:
|
|
86
|
+
name = c.get("name")
|
|
87
|
+
value = c.get("value", name)
|
|
88
|
+
disabled = c.get("disabled", None)
|
|
89
|
+
self.choices.append((name, value, disabled))
|
|
90
|
+
if value == default:
|
|
91
|
+
self.selected_option_index = i
|
|
92
|
+
searching_first_choice = False
|
|
93
|
+
if searching_first_choice:
|
|
94
|
+
self.selected_option_index = i # found the first choice
|
|
95
|
+
searching_first_choice = False
|
|
96
|
+
if default and (default in (i, c)):
|
|
97
|
+
self.selected_option_index = i # default choice exists
|
|
98
|
+
searching_first_choice = False
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def choice_count(self):
|
|
102
|
+
return len(self.choices)
|
|
103
|
+
|
|
104
|
+
def _get_choice_tokens(self):
|
|
105
|
+
tokens: list[Any] = []
|
|
106
|
+
|
|
107
|
+
def append(index, choice):
|
|
108
|
+
selected = index == self.selected_option_index
|
|
109
|
+
|
|
110
|
+
@if_mousedown
|
|
111
|
+
def select_item(mouse_event):
|
|
112
|
+
# bind option with this index to mouse event
|
|
113
|
+
self.selected_option_index = index
|
|
114
|
+
self.answered = True
|
|
115
|
+
get_app().exit(result=self.get_selection()[0])
|
|
116
|
+
|
|
117
|
+
if isinstance(choice[0], Separator):
|
|
118
|
+
tokens.append(("class:separator", " %s\n" % choice[0]))
|
|
119
|
+
else:
|
|
120
|
+
tokens.append(
|
|
121
|
+
(
|
|
122
|
+
"",
|
|
123
|
+
" \u276f " if selected else " ",
|
|
124
|
+
)
|
|
125
|
+
)
|
|
126
|
+
if selected:
|
|
127
|
+
tokens.append(("[SetCursorPosition]", ""))
|
|
128
|
+
if choice[2]: # disabled
|
|
129
|
+
tokens.append(
|
|
130
|
+
(
|
|
131
|
+
"",
|
|
132
|
+
"- %s (%s)" % (choice[0], choice[2]),
|
|
133
|
+
)
|
|
134
|
+
)
|
|
135
|
+
else:
|
|
136
|
+
try:
|
|
137
|
+
tokens.append(
|
|
138
|
+
(
|
|
139
|
+
"",
|
|
140
|
+
str(choice[0]),
|
|
141
|
+
select_item,
|
|
142
|
+
)
|
|
143
|
+
)
|
|
144
|
+
except Exception:
|
|
145
|
+
tokens.append(
|
|
146
|
+
(
|
|
147
|
+
"",
|
|
148
|
+
choice[0],
|
|
149
|
+
select_item,
|
|
150
|
+
)
|
|
151
|
+
)
|
|
152
|
+
tokens.append(("", "\n"))
|
|
153
|
+
|
|
154
|
+
# prepare the select choices
|
|
155
|
+
for i, choice in enumerate(self.choices):
|
|
156
|
+
append(i, choice)
|
|
157
|
+
tokens.pop() # Remove last newline.
|
|
158
|
+
return tokens
|
|
159
|
+
|
|
160
|
+
def get_selection(self):
|
|
161
|
+
return self.choices[self.selected_option_index]
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def prompt_question(message, **kwargs):
|
|
165
|
+
# TODO disabled, dict choices
|
|
166
|
+
if "choices" not in kwargs:
|
|
167
|
+
raise PromptParameterException("choices")
|
|
168
|
+
|
|
169
|
+
choices = kwargs.pop("choices", None)
|
|
170
|
+
default = kwargs.pop("default", None)
|
|
171
|
+
style = kwargs.pop("style", default_style)
|
|
172
|
+
|
|
173
|
+
ic = InquirerControl(choices, default=default)
|
|
174
|
+
|
|
175
|
+
def get_prompt_tokens():
|
|
176
|
+
tokens = []
|
|
177
|
+
|
|
178
|
+
tokens.append(("class:question", " %s " % message))
|
|
179
|
+
if ic.answered:
|
|
180
|
+
tokens.append(("class:answer", " " + ic.get_selection()[0]))
|
|
181
|
+
else:
|
|
182
|
+
tokens.append(("class:instruction", " (Use arrow keys)"))
|
|
183
|
+
return tokens
|
|
184
|
+
|
|
185
|
+
# assemble layout
|
|
186
|
+
layout = HSplit(
|
|
187
|
+
[
|
|
188
|
+
Window(height=D.exact(1), content=FormattedTextControl(get_prompt_tokens)),
|
|
189
|
+
ConditionalContainer(Window(ic), filter=~IsDone()),
|
|
190
|
+
]
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
# key bindings
|
|
194
|
+
kb = KeyBindings()
|
|
195
|
+
|
|
196
|
+
@kb.add("c-q", eager=True)
|
|
197
|
+
@kb.add("c-c", eager=True)
|
|
198
|
+
def _(event):
|
|
199
|
+
raise KeyboardInterrupt()
|
|
200
|
+
# event.app.exit(result=None)
|
|
201
|
+
|
|
202
|
+
@kb.add("down", eager=True)
|
|
203
|
+
def move_cursor_down(event):
|
|
204
|
+
def _next():
|
|
205
|
+
ic.selected_option_index = (ic.selected_option_index + 1) % ic.choice_count
|
|
206
|
+
|
|
207
|
+
_next()
|
|
208
|
+
while isinstance(ic.choices[ic.selected_option_index][0], Separator) or ic.choices[ic.selected_option_index][2]:
|
|
209
|
+
_next()
|
|
210
|
+
|
|
211
|
+
@kb.add("up", eager=True)
|
|
212
|
+
def move_cursor_up(event):
|
|
213
|
+
def _prev():
|
|
214
|
+
ic.selected_option_index = (ic.selected_option_index - 1) % ic.choice_count
|
|
215
|
+
|
|
216
|
+
_prev()
|
|
217
|
+
while isinstance(ic.choices[ic.selected_option_index][0], Separator) or ic.choices[ic.selected_option_index][2]:
|
|
218
|
+
_prev()
|
|
219
|
+
|
|
220
|
+
@kb.add("enter", eager=True)
|
|
221
|
+
def set_answer(event):
|
|
222
|
+
ic.answered = True
|
|
223
|
+
event.app.exit(result=ic.get_selection()[1])
|
|
224
|
+
|
|
225
|
+
return Application(layout=Layout(layout), key_bindings=kb, mouse_support=True, style=style)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def prompt(questions, answers=None, **kwargs):
|
|
229
|
+
if isinstance(questions, dict):
|
|
230
|
+
questions = [questions]
|
|
231
|
+
answers = answers or {}
|
|
232
|
+
|
|
233
|
+
patch_stdout = kwargs.pop("patch_stdout", False)
|
|
234
|
+
kbi_msg = kwargs.pop("keyboard_interrupt_msg", "Cancelled by user")
|
|
235
|
+
raise_kbi = kwargs.pop("raise_keyboard_interrupt", False)
|
|
236
|
+
|
|
237
|
+
for question in questions:
|
|
238
|
+
# import the question
|
|
239
|
+
if "type" not in question:
|
|
240
|
+
raise PromptParameterException("type")
|
|
241
|
+
if "name" not in question:
|
|
242
|
+
raise PromptParameterException("name")
|
|
243
|
+
if "message" not in question:
|
|
244
|
+
raise PromptParameterException("message")
|
|
245
|
+
try:
|
|
246
|
+
choices = question.get("choices")
|
|
247
|
+
if choices is not None and callable(choices):
|
|
248
|
+
question["choices"] = choices(answers)
|
|
249
|
+
|
|
250
|
+
_kwargs = {}
|
|
251
|
+
_kwargs.update(kwargs)
|
|
252
|
+
_kwargs.update(question)
|
|
253
|
+
type_ = _kwargs.pop("type")
|
|
254
|
+
name = _kwargs.pop("name")
|
|
255
|
+
message = _kwargs.pop("message")
|
|
256
|
+
when = _kwargs.pop("when", None)
|
|
257
|
+
filter = _kwargs.pop("filter", None)
|
|
258
|
+
|
|
259
|
+
if when:
|
|
260
|
+
# at least a little sanity check!
|
|
261
|
+
if callable(question["when"]):
|
|
262
|
+
try:
|
|
263
|
+
if not question["when"](answers):
|
|
264
|
+
continue
|
|
265
|
+
except Exception as e:
|
|
266
|
+
raise ValueError("Problem in 'when' check of %s question: %s" % (name, e))
|
|
267
|
+
else:
|
|
268
|
+
raise ValueError("'when' needs to be function that accepts a dict argument")
|
|
269
|
+
if filter and not callable(question["filter"]):
|
|
270
|
+
raise ValueError("'filter' needs to be function that accepts an argument")
|
|
271
|
+
|
|
272
|
+
if callable(question.get("default")):
|
|
273
|
+
_kwargs["default"] = question["default"](answers)
|
|
274
|
+
|
|
275
|
+
with pt_patch_stdout() if patch_stdout else _dummy_context_manager():
|
|
276
|
+
result = prompt_question(message, **_kwargs)
|
|
277
|
+
|
|
278
|
+
if isinstance(result, PromptSession):
|
|
279
|
+
answer = result.prompt()
|
|
280
|
+
elif isinstance(result, Application):
|
|
281
|
+
answer = result.run()
|
|
282
|
+
else:
|
|
283
|
+
# assert isinstance(answer, str)
|
|
284
|
+
answer = result
|
|
285
|
+
|
|
286
|
+
# answer = application.run(
|
|
287
|
+
# return_asyncio_coroutine=return_asyncio_coroutine,
|
|
288
|
+
# true_color=true_color,
|
|
289
|
+
# refresh_interval=refresh_interval)
|
|
290
|
+
|
|
291
|
+
if answer is not None:
|
|
292
|
+
if filter:
|
|
293
|
+
try:
|
|
294
|
+
answer = question["filter"](answer)
|
|
295
|
+
except Exception as e:
|
|
296
|
+
raise ValueError("Problem processing 'filter' of %s question: %s" % (name, e))
|
|
297
|
+
answers[name] = answer
|
|
298
|
+
except AttributeError:
|
|
299
|
+
raise ValueError("No question type '%s'" % type_)
|
|
300
|
+
except KeyboardInterrupt as exc:
|
|
301
|
+
if raise_kbi:
|
|
302
|
+
raise exc from None
|
|
303
|
+
if kbi_msg:
|
|
304
|
+
click.echo(kbi_msg)
|
|
305
|
+
return {}
|
|
306
|
+
return answers
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
@contextmanager
|
|
310
|
+
def _dummy_context_manager():
|
|
311
|
+
yield
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def show_options(options: List[str], title: str = "Select an option") -> Optional[str]:
|
|
315
|
+
questions = [
|
|
316
|
+
{
|
|
317
|
+
"type": "list",
|
|
318
|
+
"name": "option",
|
|
319
|
+
"message": title,
|
|
320
|
+
"choices": options,
|
|
321
|
+
}
|
|
322
|
+
]
|
|
323
|
+
result = prompt(questions)
|
|
324
|
+
|
|
325
|
+
if "option" not in result:
|
|
326
|
+
return None
|
|
327
|
+
|
|
328
|
+
return result["option"]
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def load_existing_resources(folder: str) -> str:
|
|
332
|
+
"""Load existing Tinybird resources from the workspace"""
|
|
333
|
+
existing_resources = []
|
|
334
|
+
|
|
335
|
+
# Check for datasources
|
|
336
|
+
datasources_dir = os.path.join(folder, "datasources")
|
|
337
|
+
if os.path.exists(datasources_dir):
|
|
338
|
+
for file in os.listdir(datasources_dir):
|
|
339
|
+
if file.endswith(".datasource"):
|
|
340
|
+
file_path = os.path.join(datasources_dir, file)
|
|
341
|
+
try:
|
|
342
|
+
with open(file_path, "r") as f:
|
|
343
|
+
existing_resources.append(f"DATASOURCE {file}:\n{f.read()}\n")
|
|
344
|
+
except Exception as e:
|
|
345
|
+
click.echo(f"Warning: Could not read {file_path}: {e}")
|
|
346
|
+
|
|
347
|
+
# Check for pipes
|
|
348
|
+
pipes_dir = os.path.join(folder, "pipes")
|
|
349
|
+
if os.path.exists(pipes_dir):
|
|
350
|
+
for file in os.listdir(pipes_dir):
|
|
351
|
+
if file.endswith(".pipe"):
|
|
352
|
+
file_path = os.path.join(pipes_dir, file)
|
|
353
|
+
try:
|
|
354
|
+
with open(file_path, "r") as f:
|
|
355
|
+
existing_resources.append(f"PIPE {file}:\n{f.read()}\n")
|
|
356
|
+
except Exception as e:
|
|
357
|
+
click.echo(f"Warning: Could not read {file_path}: {e}")
|
|
358
|
+
|
|
359
|
+
# Check for connections
|
|
360
|
+
connections_dir = os.path.join(folder, "connections")
|
|
361
|
+
if os.path.exists(connections_dir):
|
|
362
|
+
for file in os.listdir(connections_dir):
|
|
363
|
+
if file.endswith(".connection"):
|
|
364
|
+
file_path = os.path.join(connections_dir, file)
|
|
365
|
+
try:
|
|
366
|
+
with open(file_path, "r") as f:
|
|
367
|
+
existing_resources.append(f"CONNECTION {file}:\n{f.read()}\n")
|
|
368
|
+
except Exception as e:
|
|
369
|
+
click.echo(f"Warning: Could not read {file_path}: {e}")
|
|
370
|
+
|
|
371
|
+
return "\n".join(existing_resources)
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
class Datafile(BaseModel):
|
|
375
|
+
"""Represents a generated Tinybird datafile"""
|
|
376
|
+
|
|
377
|
+
type: str
|
|
378
|
+
name: str
|
|
379
|
+
content: str
|
|
380
|
+
description: str
|
|
381
|
+
pathname: str
|
|
382
|
+
dependencies: List[str] = Field(default_factory=list)
|
tinybird/tb/modules/cli.py
CHANGED
tinybird/tb/modules/local.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import subprocess
|
|
2
|
+
from pathlib import Path
|
|
2
3
|
|
|
3
4
|
import click
|
|
4
5
|
import requests
|
|
@@ -148,6 +149,11 @@ def remove() -> None:
|
|
|
148
149
|
)
|
|
149
150
|
def start(use_aws_creds: bool, volumes_path: str) -> None:
|
|
150
151
|
"""Start Tinybird Local"""
|
|
152
|
+
if volumes_path is not None:
|
|
153
|
+
absolute_path = Path(volumes_path).absolute()
|
|
154
|
+
absolute_path.mkdir(parents=True, exist_ok=True)
|
|
155
|
+
volumes_path = str(absolute_path)
|
|
156
|
+
|
|
151
157
|
click.echo(FeedbackManager.highlight(message="» Starting Tinybird Local..."))
|
|
152
158
|
docker_client = get_docker_client()
|
|
153
159
|
start_tinybird_local(docker_client, use_aws_creds, volumes_path)
|
|
@@ -168,6 +174,11 @@ def start(use_aws_creds: bool, volumes_path: str) -> None:
|
|
|
168
174
|
)
|
|
169
175
|
def restart(use_aws_creds: bool, volumes_path: str) -> None:
|
|
170
176
|
"""Restart Tinybird Local"""
|
|
177
|
+
if volumes_path is not None:
|
|
178
|
+
absolute_path = Path(volumes_path).absolute()
|
|
179
|
+
absolute_path.mkdir(parents=True, exist_ok=True)
|
|
180
|
+
volumes_path = str(absolute_path)
|
|
181
|
+
|
|
171
182
|
click.echo(FeedbackManager.highlight(message="» Restarting Tinybird Local..."))
|
|
172
183
|
docker_client = get_docker_client()
|
|
173
184
|
remove_tinybird_local(docker_client, volumes_path is not None)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: tinybird
|
|
3
|
-
Version: 0.0.1.
|
|
3
|
+
Version: 0.0.1.dev240
|
|
4
4
|
Summary: Tinybird Command Line Tool
|
|
5
5
|
Home-page: https://www.tinybird.co/docs/forward/commands
|
|
6
6
|
Author: Tinybird
|
|
@@ -8,7 +8,7 @@ Author-email: support@tinybird.co
|
|
|
8
8
|
Requires-Python: >=3.9, <3.14
|
|
9
9
|
Description-Content-Type: text/x-rst
|
|
10
10
|
Requires-Dist: aiofiles==24.1.0
|
|
11
|
-
Requires-Dist: anthropic==0.
|
|
11
|
+
Requires-Dist: anthropic==0.55.0
|
|
12
12
|
Requires-Dist: boto3
|
|
13
13
|
Requires-Dist: click<8.2,>=8.1.6
|
|
14
14
|
Requires-Dist: clickhouse-toolset==0.34.dev0
|
|
@@ -20,7 +20,8 @@ Requires-Dist: docker==7.1.0
|
|
|
20
20
|
Requires-Dist: GitPython~=3.1.32
|
|
21
21
|
Requires-Dist: humanfriendly~=8.2
|
|
22
22
|
Requires-Dist: prompt_toolkit==3.0.48
|
|
23
|
-
Requires-Dist: pydantic~=2.
|
|
23
|
+
Requires-Dist: pydantic~=2.11.7
|
|
24
|
+
Requires-Dist: pydantic-ai-slim[anthropic]~=0.3.1
|
|
24
25
|
Requires-Dist: pyperclip==1.8.2
|
|
25
26
|
Requires-Dist: pyyaml<6.1,>=6.0
|
|
26
27
|
Requires-Dist: requests<3,>=2.28.1
|
|
@@ -17,15 +17,14 @@ tinybird/datafile/exceptions.py,sha256=8rw2umdZjtby85QbuRKFO5ETz_eRHwUY5l7eHsy1w
|
|
|
17
17
|
tinybird/datafile/parse_connection.py,sha256=tRyn2Rpr1TeWet5BXmMoQgaotbGdYep1qiTak_OqC5E,1825
|
|
18
18
|
tinybird/datafile/parse_datasource.py,sha256=ssW8QeFSgglVFi3sDZj_HgkJiTJ2069v2JgqnH3CkDE,1825
|
|
19
19
|
tinybird/datafile/parse_pipe.py,sha256=xf4m0Tw44QWJzHzAm7Z7FwUoUUtr7noMYjU1NiWnX0k,3880
|
|
20
|
-
tinybird/tb/__cli__.py,sha256=
|
|
20
|
+
tinybird/tb/__cli__.py,sha256=OB0M6HqPur6rsd93Mf9BRpLf5zvrZ_aq4HvYd7nAacc,247
|
|
21
21
|
tinybird/tb/check_pypi.py,sha256=Gp0HkHHDFMSDL6nxKlOY51z7z1Uv-2LRexNTZSHHGmM,552
|
|
22
22
|
tinybird/tb/cli.py,sha256=FdDFEIayjmsZEVsVSSvRiVYn_FHOVg_zWQzchnzfWho,1008
|
|
23
|
-
tinybird/tb/client.py,sha256=
|
|
23
|
+
tinybird/tb/client.py,sha256=pJbdkWMXGAqKseNAvdsRRnl_c7I-DCMB0dWCQnG82nU,54146
|
|
24
24
|
tinybird/tb/config.py,sha256=mhMTGnMB5KcxGoh3dewIr2Jjsa6pHE183gCPAQWyp6o,3973
|
|
25
|
-
tinybird/tb/modules/agent.py,sha256=RE_QlGSq6MByEaOBUbr1dSr3X2bi09-QvzoxpxQtyPs,3900
|
|
26
25
|
tinybird/tb/modules/build.py,sha256=MxGQY-KII4lYut-yOWGBIHFpg-m_cfHgpE7lwOzZCeQ,19871
|
|
27
26
|
tinybird/tb/modules/cicd.py,sha256=0KLKccha9IP749QvlXBmzdWv1On3mFwMY4DUcJlBxiE,7326
|
|
28
|
-
tinybird/tb/modules/cli.py,sha256=
|
|
27
|
+
tinybird/tb/modules/cli.py,sha256=_-kj4PSN_OPYk__WYgFfzLsDJfy4cQfveZg6tTOc220,16389
|
|
29
28
|
tinybird/tb/modules/common.py,sha256=jTTaDDHrZREt--032XhP6GkbfFwC79YJ5aH1Sl7bmbo,81925
|
|
30
29
|
tinybird/tb/modules/config.py,sha256=gK7rgaWTDd4ZKCrNEg_Uemr26EQjqWt6TjyQKujxOws,11462
|
|
31
30
|
tinybird/tb/modules/connection.py,sha256=-MY56NUAai6EMC4-wpi7bT0_nz_SA8QzTmHkV7HB1IQ,17810
|
|
@@ -43,7 +42,7 @@ tinybird/tb/modules/infra.py,sha256=JE9oLIyF4bi_JBoe-BgZ5HhKp_lQgSihuSV1KIS02Qs,
|
|
|
43
42
|
tinybird/tb/modules/job.py,sha256=wBsnu8UPTOha2rkLvucgmw4xYv73ubmui3eeSIF68ZM,3107
|
|
44
43
|
tinybird/tb/modules/llm.py,sha256=QbHRcMLgFmLKEh4zVb2ctR_5tIGUGdFJrAiRCDtMxDw,1572
|
|
45
44
|
tinybird/tb/modules/llm_utils.py,sha256=nS9r4FAElJw8yXtmdYrx-rtI2zXR8qXfi1QqUDCfxvg,3469
|
|
46
|
-
tinybird/tb/modules/local.py,sha256=
|
|
45
|
+
tinybird/tb/modules/local.py,sha256=tpiw_F_qOIp42h3kTBwTm5GQDyuVLF0QNF1jmB0zR94,6845
|
|
47
46
|
tinybird/tb/modules/local_common.py,sha256=_WODjW3oPshgsZ1jDFFx2nr0zrLi3Gxz5dlahWPobM8,17464
|
|
48
47
|
tinybird/tb/modules/login.py,sha256=glqj5RWH26AseEoBl8XfrSDEjQTdko17i_pVWOIMoGc,12497
|
|
49
48
|
tinybird/tb/modules/logout.py,sha256=sniI4JNxpTrVeRCp0oGJuQ3yRerG4hH5uz6oBmjv724,1009
|
|
@@ -63,6 +62,19 @@ tinybird/tb/modules/token.py,sha256=DkXW9FNCLGBisXewfk195jTJ6B1Iz7zq3cEEac48aAs,
|
|
|
63
62
|
tinybird/tb/modules/watch.py,sha256=No0bK1M1_3CYuMaIgylxf7vYFJ72lTJe3brz6xQ-mJo,8819
|
|
64
63
|
tinybird/tb/modules/workspace.py,sha256=Q_8HcxMsNg8QG9aBlwcWS2umrDP5IkTIHqqz3sfmGuc,11341
|
|
65
64
|
tinybird/tb/modules/workspace_members.py,sha256=5JdkJgfuEwbq-t6vxkBhYwgsiTDxF790wsa6Xfif9nk,8608
|
|
65
|
+
tinybird/tb/modules/agent/__init__.py,sha256=i3oe3vDIWWPaicdCM0zs7D7BJ1W0k7th93ooskHAV00,54
|
|
66
|
+
tinybird/tb/modules/agent/agent.py,sha256=98X8rqUszArhOnrWWGAJRNwAK-me6Ql2UGNGFxcuc2E,10205
|
|
67
|
+
tinybird/tb/modules/agent/banner.py,sha256=KX_e467uiy1gWOZ4ofTZt0GCFGQqHQ_8Ob27XLQqda0,3053
|
|
68
|
+
tinybird/tb/modules/agent/client.py,sha256=ZNE_0xtbrfhXXRZ7nx3Ze0gHb1IK7zXRLTlGudAAuUg,1264
|
|
69
|
+
tinybird/tb/modules/agent/memory.py,sha256=H6SJK--2L5C87B7AJd_jMqsq3sCvFvZwZXmajuT0GBE,1171
|
|
70
|
+
tinybird/tb/modules/agent/models.py,sha256=jUEO7i_JwlQVZi6QEUeGyFJMFkPZ4ikyDZdei6s6f3M,628
|
|
71
|
+
tinybird/tb/modules/agent/prompts.py,sha256=j7nkx1E2BnPP6Ra718FvSDGRWucyFgSX07V9QAiaiGg,4864
|
|
72
|
+
tinybird/tb/modules/agent/utils.py,sha256=E785Sq_TLVBi1gaBR53YlHvyP_Kbh4TmRouYl3KHFpo,13080
|
|
73
|
+
tinybird/tb/modules/agent/tools/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
74
|
+
tinybird/tb/modules/agent/tools/create_datafile.py,sha256=06HK33h86T1d54zvnJXJz_1su3zIS7UJDPyBqhgo81c,1771
|
|
75
|
+
tinybird/tb/modules/agent/tools/explore.py,sha256=iEVsi85CRPCPhWcl-fC1J4hYK22TfunxyqNu5pDxdDU,419
|
|
76
|
+
tinybird/tb/modules/agent/tools/plan.py,sha256=acMbyjqI6cN7IwmMAC8-MPNSg0PEZVKGEk2NOY4gMi8,1162
|
|
77
|
+
tinybird/tb/modules/agent/tools/preview_datafile.py,sha256=e9q5fR0afApcrntzFrnuHmd10ex7MG_GM6T0Pwc9bRI,850
|
|
66
78
|
tinybird/tb/modules/datafile/build.py,sha256=NFKBrusFLU0WJNCXePAFWiEDuTaXpwc0lHlOQWEJ43s,51117
|
|
67
79
|
tinybird/tb/modules/datafile/build_common.py,sha256=2yNdxe49IMA9wNvl25NemY2Iaz8L66snjOdT64dm1is,4511
|
|
68
80
|
tinybird/tb/modules/datafile/build_datasource.py,sha256=Ra8pVQBDafbFRUKlhpgohhTsRyp_ADKZJVG8Gd69idY,17227
|
|
@@ -83,8 +95,8 @@ tinybird/tb_cli_modules/config.py,sha256=IsgdtFRnUrkY8-Zo32lmk6O7u3bHie1QCxLwgp4
|
|
|
83
95
|
tinybird/tb_cli_modules/exceptions.py,sha256=pmucP4kTF4irIt7dXiG-FcnI-o3mvDusPmch1L8RCWk,3367
|
|
84
96
|
tinybird/tb_cli_modules/regions.py,sha256=QjsL5H6Kg-qr0aYVLrvb1STeJ5Sx_sjvbOYO0LrEGMk,166
|
|
85
97
|
tinybird/tb_cli_modules/telemetry.py,sha256=Hh2Io8ZPROSunbOLuMvuIFU4TqwWPmQTqal4WS09K1A,10449
|
|
86
|
-
tinybird-0.0.1.
|
|
87
|
-
tinybird-0.0.1.
|
|
88
|
-
tinybird-0.0.1.
|
|
89
|
-
tinybird-0.0.1.
|
|
90
|
-
tinybird-0.0.1.
|
|
98
|
+
tinybird-0.0.1.dev240.dist-info/METADATA,sha256=XT_HpNQ7KOTyQt5ICua7m_CWsFaKQGB0tMvg43YO4XM,1733
|
|
99
|
+
tinybird-0.0.1.dev240.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
|
100
|
+
tinybird-0.0.1.dev240.dist-info/entry_points.txt,sha256=LwdHU6TfKx4Qs7BqqtaczEZbImgU7Abe9Lp920zb_fo,43
|
|
101
|
+
tinybird-0.0.1.dev240.dist-info/top_level.txt,sha256=VqqqEmkAy7UNaD8-V51FCoMMWXjLUlR0IstvK7tJYVY,54
|
|
102
|
+
tinybird-0.0.1.dev240.dist-info/RECORD,,
|
tinybird/tb/modules/agent.py
DELETED
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
import sys
|
|
3
|
-
from typing import Any, Dict, Optional
|
|
4
|
-
|
|
5
|
-
import click
|
|
6
|
-
from prompt_toolkit import PromptSession
|
|
7
|
-
from prompt_toolkit.history import FileHistory
|
|
8
|
-
from prompt_toolkit.styles import Style
|
|
9
|
-
|
|
10
|
-
from tinybird.tb.client import TinyB
|
|
11
|
-
from tinybird.tb.modules.common import _get_tb_client
|
|
12
|
-
from tinybird.tb.modules.feedback_manager import FeedbackManager
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def agent_banner():
|
|
16
|
-
# Define gradient colors (teal/turquoise range)
|
|
17
|
-
colors = [
|
|
18
|
-
"\033[38;2;0;128;128m", # Teal
|
|
19
|
-
"\033[38;2;0;150;136m", # Teal-ish
|
|
20
|
-
"\033[38;2;20;160;145m", # Turquoise blend
|
|
21
|
-
"\033[38;2;40;170;155m", # Light turquoise
|
|
22
|
-
"\033[38;2;60;180;165m", # Lighter turquoise
|
|
23
|
-
"\033[38;2;80;190;175m", # Very light turquoise
|
|
24
|
-
]
|
|
25
|
-
reset = "\033[0m"
|
|
26
|
-
|
|
27
|
-
# The Tinybird Code ASCII art banner
|
|
28
|
-
banner = [
|
|
29
|
-
" ████████╗██╗███╗ ██╗██╗ ██╗██████╗ ██╗██████╗ ██████╗ ██████╗ ██████╗ ██████╗ ███████╗",
|
|
30
|
-
" ╚══██╔══╝██║████╗ ██║╚██╗ ██╔╝██╔══██╗██║██╔══██╗██╔══██╗ ██╔════╝██╔═══██╗██╔══██╗██╔════╝",
|
|
31
|
-
" ██║ ██║██╔██╗ ██║ ╚████╔╝ ██████╔╝██║██████╔╝██║ ██║ ██║ ██║ ██║██║ ██║█████╗ ",
|
|
32
|
-
" ██║ ██║██║╚██╗██║ ╚██╔╝ ██╔══██╗██║██╔══██╗██║ ██║ ██║ ██║ ██║██║ ██║██╔══╝ ",
|
|
33
|
-
" ██║ ██║██║ ╚████║ ██║ ██████╔╝██║██║ ██║██████╔╝ ╚██████╗╚██████╔╝██████╔╝███████╗",
|
|
34
|
-
" ╚═╝ ╚═╝╚═╝ ╚═══╝ ╚═╝ ╚═════╝ ╚═╝╚═╝ ╚═╝╚═════╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝",
|
|
35
|
-
]
|
|
36
|
-
|
|
37
|
-
# Print each line with a smooth horizontal gradient
|
|
38
|
-
for line in banner:
|
|
39
|
-
colored_line = ""
|
|
40
|
-
for j, char in enumerate(line):
|
|
41
|
-
# Skip coloring spaces
|
|
42
|
-
if char == " ":
|
|
43
|
-
colored_line += char
|
|
44
|
-
continue
|
|
45
|
-
|
|
46
|
-
# Calculate color index for a smooth gradient
|
|
47
|
-
color_index = min(int(j * len(colors) / len(line)), len(colors) - 1)
|
|
48
|
-
colored_line += f"{colors[color_index]}{char}"
|
|
49
|
-
|
|
50
|
-
click.echo(colored_line + reset)
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
def explore_data(client: TinyB, prompt: str):
|
|
54
|
-
click.echo(FeedbackManager.highlight(message="\nExploring data...\n"))
|
|
55
|
-
result = client.explore_data(prompt)
|
|
56
|
-
click.echo(result)
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
def run_agent_shell(config: Dict[str, Any]):
|
|
60
|
-
style = Style.from_dict({"prompt": "fg:#34D399 bold"})
|
|
61
|
-
history: Optional[FileHistory] = None
|
|
62
|
-
try:
|
|
63
|
-
history_file = os.path.expanduser("~/.tb_agent_history")
|
|
64
|
-
history = FileHistory(history_file)
|
|
65
|
-
except Exception:
|
|
66
|
-
pass
|
|
67
|
-
workspace_name = config.get("name", "No workspace found")
|
|
68
|
-
session: PromptSession = PromptSession(history=history)
|
|
69
|
-
user_input = session.prompt([("class:prompt", f"\ntb ({workspace_name}) » ")], style=style)
|
|
70
|
-
if user_input == "exit":
|
|
71
|
-
sys.exit(0)
|
|
72
|
-
else:
|
|
73
|
-
client = _get_tb_client(config.get("token", None), config["host"])
|
|
74
|
-
explore_data(client, user_input)
|
|
75
|
-
return run_agent_shell(config)
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
def run_agent(config: Dict[str, Any]):
|
|
79
|
-
agent_banner()
|
|
80
|
-
run_agent_shell(config)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|