Skip to content

Commit 33af20d

Browse files
committed
feat: add docker dev environment, fix agent execution, and improve thread UI
- Add docker-compose.dev.yml with Dramatiq worker for background task processing - Add backend Dockerfile.dev for development with hot reload - Fix agent status propagation to LayoutContext for auto-opening preview panel - Fix tool call buttons to display on single line with proper flexbox properties - Add message grouping and streaming content components for better thread rendering - Update billing components and settings pages - Various UI/UX improvements across thread, sidebar, and settings components
1 parent 5c5d41b commit 33af20d

File tree

173 files changed

+6650
-7545
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

173 files changed

+6650
-7545
lines changed

backend/Dockerfile.dev

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
FROM ghcr.io/astral-sh/uv:python3.11-alpine
2+
3+
ENV ENV_MODE=development
4+
WORKDIR /app
5+
6+
RUN apk add --no-cache curl git build-base linux-headers rust cargo
7+
8+
COPY pyproject.toml ./
9+
ENV UV_LINK_MODE=copy
10+
RUN --mount=type=cache,target=/root/.cache/uv uv lock
11+
RUN --mount=type=cache,target=/root/.cache/uv uv sync --locked --quiet
12+
13+
COPY . .
14+
15+
ENV PYTHONPATH=/app
16+
ENV PYTHONUNBUFFERED=1
17+
EXPOSE 8000
18+
19+
CMD ["uv", "run", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"]

backend/agent/api.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,10 @@ async def cleanup():
169169

170170

171171
async def get_agent_run_with_access_check(client, agent_run_id: str, user_id: str):
172-
agent_run = await client.table('agent_runs').select('*').eq('run_id', agent_run_id).execute()
172+
# Only select columns needed for access check and response
173+
agent_run = await client.table('agent_runs').select(
174+
'run_id, thread_id, status, started_at, completed_at, error, metadata, created_at, updated_at'
175+
).eq('run_id', agent_run_id).execute()
173176
if not agent_run.data:
174177
raise HTTPException(status_code=404, detail="Agent run not found")
175178

@@ -225,7 +228,9 @@ async def start_agent(
225228
).eq('user_id', account_id).eq('is_active', True).eq('is_default_for_dashboard', True).execute()
226229
) if project_id else None
227230
project_task = asyncio.create_task(
228-
client.table('projects').select('*').eq('project_id', project_id).execute()
231+
client.table('projects').select(
232+
'project_id, name, user_id, sandbox, app_type, model_name'
233+
).eq('project_id', project_id).execute()
229234
) if project_id else None
230235

231236
# Await token status first (needed for quota check)
@@ -349,7 +354,9 @@ async def start_agent(
349354

350355
try:
351356
# Use pre-fetched project data from parallel query above
352-
project_result = await project_task if project_task else await client.table('projects').select('*').eq('project_id', project_id).execute()
357+
project_result = await project_task if project_task else await client.table('projects').select(
358+
'project_id, name, user_id, sandbox, app_type, model_name'
359+
).eq('project_id', project_id).execute()
353360
if not project_result.data:
354361
raise HTTPException(status_code=404, detail="Project not found")
355362

backend/projects_threads_api.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,9 @@ async def get_projects(
5858
return []
5959

6060
# Query projects for this account with pagination and ordering
61+
# Only select columns needed for the API response to reduce data transfer
6162
result = await client.table('projects')\
62-
.select('*')\
63+
.select('project_id, name, description, user_id, created_at, updated_at, sandbox, is_public, app_type')\
6364
.eq('user_id', account_id)\
6465
.order('created_at', desc=True)\
6566
.range(offset, offset + limit - 1)\
@@ -106,8 +107,10 @@ async def get_project(
106107
try:
107108
client = await db.client
108109

109-
# Fetch the project row
110-
result = await client.table('projects').select('*').eq('project_id', project_id).execute()
110+
# Fetch the project row - only select needed columns
111+
result = await client.table('projects').select(
112+
'project_id, name, description, user_id, created_at, updated_at, sandbox, is_public, app_type'
113+
).eq('project_id', project_id).execute()
111114

112115
if not result.data:
113116
raise HTTPException(status_code=404, detail="Project not found")
@@ -167,8 +170,10 @@ async def get_threads(
167170
return []
168171

169172
# Build query with server-side filtering for agent builder threads
170-
# and pagination for performance
171-
query = client.table('threads').select('*').eq('user_id', account_id)
173+
# and pagination for performance - only select needed columns
174+
query = client.table('threads').select(
175+
'thread_id, user_id, project_id, is_public, created_at, updated_at, metadata'
176+
).eq('user_id', account_id)
172177

173178
if project_id:
174179
query = query.eq('project_id', project_id)

backend/services/billing.py

Lines changed: 42 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -150,11 +150,9 @@ async def get_billing_status(
150150
free_tokens = free_plan['token_quota']
151151
free_credits = free_plan['display_credits']
152152

153-
# Get deployment count
154-
from deployments.api import _count_deployed_projects_for_account
153+
# Get deployment count with caching
155154
from utils.constants import get_plan_deployment_limit
156-
async with db.get_async_client() as client:
157-
deployments_used = await _count_deployed_projects_for_account(client, account_id)
155+
deployments_used = await get_deployment_count_cached(account_id)
158156
deployments_total = get_plan_deployment_limit('free')
159157

160158
return SubscriptionStatusResponse(
@@ -177,12 +175,9 @@ async def get_billing_status(
177175
if not plan_config:
178176
raise HTTPException(status_code=500, detail="Invalid plan configuration")
179177

180-
# Get deployment count
181-
from deployments.api import _count_deployed_projects_for_account
178+
# Get deployment count with caching
182179
from utils.constants import get_plan_deployment_limit
183-
db = DBConnection()
184-
async with db.get_async_client() as client:
185-
deployments_used = await _count_deployed_projects_for_account(client, subscription['account_id'])
180+
deployments_used = await get_deployment_count_cached(subscription['account_id'])
186181
deployments_total = get_plan_deployment_limit(subscription['plan'])
187182

188183
return SubscriptionStatusResponse(
@@ -462,6 +457,44 @@ async def get_quota_status(
462457
# Billing status cache TTL in seconds (30 seconds for quick updates while reducing DB load)
463458
BILLING_STATUS_CACHE_TTL = 30
464459

460+
# Deployment count cache TTL (60 seconds - deployments change less frequently)
461+
DEPLOYMENT_COUNT_CACHE_TTL = 60
462+
463+
464+
async def get_deployment_count_cached(account_id: str) -> int:
465+
"""Get deployment count with Redis caching to avoid redundant queries.
466+
467+
Caches the result for 60 seconds since deployments change infrequently.
468+
"""
469+
import json
470+
from services import redis as redis_service
471+
from deployments.api import _count_deployed_projects_for_account
472+
473+
cache_key = f"deployment_count:{account_id}"
474+
475+
# Try cache first
476+
try:
477+
cached_count = await redis_service.get_value(cache_key)
478+
if cached_count is not None:
479+
logger.debug(f"Cache HIT for deployment count: {account_id}")
480+
return int(cached_count)
481+
except Exception as e:
482+
logger.debug(f"Cache miss for deployment count {account_id}: {e}")
483+
484+
# Cache miss - query database
485+
db = DBConnection()
486+
async with db.get_async_client() as client:
487+
count = await _count_deployed_projects_for_account(client, account_id)
488+
489+
# Cache the result
490+
try:
491+
await redis_service.set_value(cache_key, str(count), ttl=DEPLOYMENT_COUNT_CACHE_TTL)
492+
logger.debug(f"Cached deployment count for {account_id}: {count}")
493+
except Exception as e:
494+
logger.warning(f"Failed to cache deployment count: {e}")
495+
496+
return count
497+
465498
async def can_use_model(client: Client, user_id: str, model_name: str):
466499
"""Check if user can use a specific model based on their subscription.
467500

docker-compose.dev.yml

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
services:
2+
# Backend API with hot reload
3+
backend:
4+
build:
5+
context: ./backend
6+
dockerfile: Dockerfile.dev
7+
ports:
8+
- "8000:8000"
9+
env_file:
10+
- ./backend/.env
11+
volumes:
12+
- ./backend:/app
13+
- /app/.venv
14+
environment:
15+
- ENV_MODE=development
16+
- REDIS_URL=redis://redis:6379
17+
- PYTHONUNBUFFERED=1
18+
depends_on:
19+
redis:
20+
condition: service_healthy
21+
networks:
22+
- dev-network
23+
dns:
24+
- 8.8.8.8
25+
- 8.8.4.4
26+
27+
# Dramatiq worker for background tasks
28+
worker:
29+
build:
30+
context: ./backend
31+
dockerfile: Dockerfile.dev
32+
command: uv run dramatiq --skip-logging --processes 1 --threads 2 run_agent_background
33+
env_file:
34+
- ./backend/.env
35+
volumes:
36+
- ./backend:/app
37+
- /app/.venv
38+
environment:
39+
- ENV_MODE=development
40+
- REDIS_URL=redis://redis:6379
41+
- PYTHONUNBUFFERED=1
42+
depends_on:
43+
redis:
44+
condition: service_healthy
45+
networks:
46+
- dev-network
47+
dns:
48+
- 8.8.8.8
49+
- 8.8.4.4
50+
restart: unless-stopped
51+
52+
# Frontend with hot reload
53+
frontend:
54+
build:
55+
context: ./frontend
56+
dockerfile: Dockerfile
57+
target: development
58+
ports:
59+
- "3000:3000"
60+
env_file:
61+
- ./frontend/.env
62+
volumes:
63+
- ./frontend:/app
64+
- /app/node_modules
65+
- /app/.next
66+
environment:
67+
- NODE_ENV=development
68+
- WATCHPACK_POLLING=true
69+
networks:
70+
- dev-network
71+
dns:
72+
- 8.8.8.8
73+
- 8.8.4.4
74+
75+
# Redis
76+
redis:
77+
image: redis:8-alpine
78+
ports:
79+
- "127.0.0.1:6380:6379"
80+
volumes:
81+
- redis_data:/data
82+
networks:
83+
- dev-network
84+
command: redis-server --appendonly yes --bind 0.0.0.0 --protected-mode no
85+
healthcheck:
86+
test: ["CMD", "redis-cli", "ping"]
87+
interval: 10s
88+
timeout: 5s
89+
retries: 5
90+
start_period: 5s
91+
92+
networks:
93+
dev-network:
94+
driver: bridge
95+
96+
volumes:
97+
redis_data:

frontend/eslint.config.mjs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,14 @@ const eslintConfig = [
1313
...compat.extends('next/core-web-vitals', 'next/typescript'),
1414
{
1515
rules: {
16-
'@typescript-eslint/no-unused-vars': 'off',
17-
'@typescript-eslint/no-explicit-any': 'off',
16+
'@typescript-eslint/no-unused-vars': 'error',
17+
'@typescript-eslint/no-explicit-any': 'error',
1818
'react/no-unescaped-entities': 'off',
19-
'react-hooks/exhaustive-deps': 'warn',
20-
'@next/next/no-img-element': 'warn',
19+
'react-hooks/exhaustive-deps': 'error',
20+
'@next/next/no-img-element': 'error',
2121
'@typescript-eslint/no-empty-object-type': 'off',
22-
'prefer-const': 'warn',
22+
'prefer-const': 'error',
23+
'no-console': 'error',
2324
},
2425
},
2526
];

0 commit comments

Comments
 (0)