Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion backend/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ dependencies = [
"psycopg2-binary==2.9.11",
"Faker==40.1.0",
"gunicorn==23.0.0",
"uvicorn[standard]==0.40.0",
"uvicorn[standard]==0.52.4",
"websockets==16.0.0",
"requests==2.34.2",
"itsdangerous==2.2.0",
Expand Down
30 changes: 18 additions & 12 deletions backend/uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion e2e-tests/fixtures/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export async function createUser(
): Promise<User> {
const password = faker.internet.password();
const response: any = await getClient().post("user/", {
name: faker.name.fullName(),
name: faker.person.fullName(),
email: faker.internet.email(),
password,
language: "en",
Expand Down
2 changes: 1 addition & 1 deletion e2e-tests/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"codegen": "playwright codegen"
},
"dependencies": {
"@faker-js/faker": "7.6.0",
"@faker-js/faker": "10.5.0",
"@nuxt/test-utils": "^3.21.0",
"@playwright/test": "^1.48.0",
"axios": "1.18.0",
Expand Down
8 changes: 4 additions & 4 deletions e2e-tests/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,10 @@
picocolors "^1.0.0"
sisteransi "^1.0.5"

"@faker-js/faker@7.6.0":
version "7.6.0"
resolved "https://registry.yarnpkg.com/@faker-js/faker/-/faker-7.6.0.tgz#9ea331766084288634a9247fcd8b84f16ff4ba07"
integrity sha512-XK6BTq1NDMo9Xqw/YkYyGjSsg44fbNwYRx7QK2CuoQgyy+f1rrTDHoExVM5PsyXCtfl2vs2vVJ0MN0yN6LppRw==
"@faker-js/faker@10.5.0":
version "10.5.0"
resolved "https://registry.yarnpkg.com/@faker-js/faker/-/faker-10.5.0.tgz#d2f6a8c7f08d087ac5f077d6babd0821edf24c03"
integrity sha512-bsxD8WLS5lIj7aaoCx1YJkktqYj5vlBUE6HWzu2Q51ksrGJ0H737ECCKlFU7Yf8Br45z9t99frBp/J7kzbMPAg==

"@jridgewell/gen-mapping@^0.3.5":
version "0.3.13"
Expand Down
19 changes: 16 additions & 3 deletions enterprise/backend/src/baserow_enterprise/assistant/assistant.py
Original file line number Diff line number Diff line change
Expand Up @@ -363,7 +363,10 @@ async def _save_ai_response(
await AssistantChatPrediction.objects.acreate(
human_message=human_msg,
ai_response=ai_msg,
prediction={"answer": answer},
prediction={
"answer": answer,
"posthog_trace_id": self._telemetry.trace_id,
},
)
return AiMessage(
id=ai_msg.id,
Expand Down Expand Up @@ -469,7 +472,11 @@ async def _run_agent(
"""

try:
with self._telemetry.trace(self._chat, user_prompt) as tracer:
with self._telemetry.trace(
self._chat,
user_prompt,
cancelled_by_user=lambda: self._tool_helpers.is_cancelled,
) as tracer:
answer, run_result = await self._run_agent_with_retries(
user_prompt, message_history, queue
)
Expand Down Expand Up @@ -635,10 +642,16 @@ def _looks_like_json_tool_call(text: str) -> bool:
"""Return True if *text* looks like a tool call dumped as JSON.

Checks for ``{"name": ..., "arguments": ...}`` pattern in the first
200 chars. Does not require valid JSON (the output may be truncated).
200 chars after an optional code fence. Does not require valid JSON
because the output may be truncated.

:param text: The final text returned by the agent.
:return: Whether the text appears to contain an unexecuted tool call.
"""

stripped = text.strip()
if stripped.startswith("```"):
stripped = stripped.split("\n", 1)[-1].strip()
return (
bool(stripped)
and stripped[0] == "{"
Expand Down
25 changes: 16 additions & 9 deletions enterprise/backend/src/baserow_enterprise/assistant/prompts.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,24 @@
"""

RULES = """\
<contracts>
Three invariants. A tool call that breaks one is invalid — do not send it.
A. IDs. Every `*_id` argument must carry a real ID you have in hand: returned by a tool call in this conversation, present in `<ui_context>`, or given to you by the user. Never invent, guess, or carry over an ID from a different resource. Baserow IDs start at 1, so 0 is never an ID. If you do not have the ID yet, call the list_*/create_* tool that returns it, then pass back the exact value it returned.
B. Modes. Have tools → call them. Each tool is owned by exactly one mode, and `<available_tools>` is the authority: it names what the current `<mode>` can call and what each other mode owns. To use a tool owned by another mode, call switch_mode first. If a tool call comes back rejected as an unknown name, that means wrong mode, not missing feature — re-read `<available_tools>`, switch to the owning mode, and retry it once. Only describe manual UI steps once you have confirmed no mode owns a tool for the action; `<limitations>` lists what genuinely cannot be done in any mode.
C. Payloads. Send every required argument on the first attempt, not only the ones you are confident about. For create_*, update_* and setup_* tools the payload is the point of the call: one carrying just IDs and a `thought` is always incomplete.
</contracts>
<rules>
1. Use the `thought` parameter on EVERY tool call. It is shown to the user, so write it as a brief user-facing status (e.g. "Checking existing pages" not "Calling list_pages to get page IDs"). Never use tool names or internal references.
2. Have tools → call them. No tools in current mode → check other modes before saying something is not possible. If another mode has the tool, switch_mode and use it. Only explain manual UI steps if no mode covers the action.
3. One tool per turn. Wait for the result. Never reply and call a tool in same turn.
4. Request priority: action > follow-up (reuse prior IDs, never search docs) > question. When a tool result contains next_steps, act on them immediately — do not ask for permission to continue.
5. You start in the mode matching your UI context (database/application/automation). If the user asks a how-to or feature question, call switch_mode("explain"), then search_user_docs.
6. After finishing the tool calls in a different mode (not just after switching — after the actual work is done and results received), switch back to the original domain mode (check <mode> and <ui_context>).
7. Reply in concise Markdown. Never expose raw JSON or internal IDs unless asked.
8. Before starting work, use list_* to understand what exists and avoid duplicates. But don't list resources you just created — create_* tools already return IDs and refs. When a request references resources by name/ID, verify they exist before building on them. If not found, ask — don't guess. But when the task *requires* creating resources in another domain (e.g. building an app that needs new tables), switch_mode and create them yourself — don't ask the user to do it manually.
9. Before responding to the user, verify ALL parts of `<current_task>` are addressed. If anything is missing, continue working.
10. At the start, verify the request fits the current UI context (e.g. don't add "Inquiries" table to a "Project Management" DB). If it doesn't match and not explicitly requested, ask the user which target to use.
2. One tool per turn. Wait for the result. Never reply and call a tool in same turn.
3. Request priority: action > follow-up (reuse prior IDs, never search docs) > question. When a tool result contains next_steps, act on them immediately — do not ask for permission to continue.
4. You start in the mode matching your UI context (database/application/automation). If the user asks a how-to or feature question, call switch_mode("explain"), then search_user_docs.
5. After finishing the tool calls in a different mode (not just after switching — after the actual work is done and results received), switch back to the original domain mode (check <mode> and <ui_context>).
6. Reply in concise Markdown. Never expose raw JSON or internal IDs unless asked.
7. Before starting work, use list_* to understand what exists and avoid duplicates. But don't list resources you just created — create_* tools already return IDs and refs. When a request references resources by name/ID, verify they exist before building on them. If not found, ask — don't guess. But when the task *requires* creating resources in another domain (e.g. building an app that needs new tables), switch_mode and create them yourself — don't ask the user to do it manually.
8. Before responding to the user, verify ALL parts of `<current_task>` are addressed. If anything is missing, continue working.
9. At the start, verify the request fits the current UI context (e.g. don't add "Inquiries" table to a "Project Management" DB). If it doesn't match and not explicitly requested, ask the user which target to use.
10. When a task needs a database, application, or automation that does not exist yet, call create_builders first and build on the ID it returns (contract A).
11. For database formula creation or repair, call generate_formula so the result is validated. Never return or save a handwritten formula. Use save_to_field=true when the user asks to create, fix, save, or apply it; use false only when they explicitly want formula text without changing the table.
</rules>
"""

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@
def _is_transient_provider_error(exc: Exception) -> bool:
"""Return True for provider errors that are transient and safe to retry."""

if isinstance(exc, ModelHTTPError) and exc.status_code == 429:
return True
msg = str(exc)
return any(needle in msg for needle in _RETRYABLE_MESSAGES)

Expand Down Expand Up @@ -413,6 +415,8 @@ async def request(
):
raise
delay = self._delay_for(attempt)
if isinstance(exc, ModelHTTPError) and exc.retry_after is not None:
delay = min(exc.retry_after, self.max_delay)
logger.warning(
"[assistant] Model request failed (attempt {}/{}), "
"retrying in {:.1f}s: {}",
Expand Down
Loading
Loading