Skip to content

Commit 8bb6137

Browse files
committed
Merge branch 'develop' into xyq/feat_add_model_timeout
2 parents 2bfd1f8 + f87b1cb commit 8bb6137

95 files changed

Lines changed: 9353 additions & 2737 deletions

File tree

Some content is hidden

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

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ docker/uploads
1919
docker/openssh-server
2020
docker/volumes/db/data
2121
docker/.env
22+
docker/monitoring/monitoring.env
2223
docker/.run
2324
docker/deploy.options
2425
k8s/helm/.deploy.options

backend/apps/agent_app.py

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,17 +38,13 @@
3838
)
3939
from utils.auth_utils import get_current_user_info, get_current_user_id
4040

41-
# Import monitoring utilities
42-
from utils.monitoring import monitoring_manager
43-
4441
agent_runtime_router = APIRouter(prefix="/agent")
4542
agent_config_router = APIRouter(prefix="/agent")
4643
logger = logging.getLogger("agent_app")
4744

4845

4946
# Define API route
5047
@agent_runtime_router.post("/run")
51-
@monitoring_manager.monitor_endpoint("agent.run", exclude_params=["authorization"])
5248
async def agent_run_api(agent_request: AgentRequest, http_request: Request, authorization: str = Header(None)):
5349
"""
5450
Agent execution API endpoint
@@ -555,4 +551,3 @@ async def list_published_agents_api(
555551
raise HTTPException(
556552
status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail="Published agents list error."
557553
)
558-

backend/apps/monitoring_app.py

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,16 @@
77

88
import logging
99
from http import HTTPStatus
10-
from typing import Annotated, Optional
10+
from typing import Annotated, Any
1111

1212
from fastapi import APIRouter, Header, HTTPException, Query
1313
from sqlalchemy import text
1414

15+
from consts.const import (
16+
ENABLE_TELEMETRY,
17+
MONITORING_DASHBOARD_URL,
18+
MONITORING_PROVIDER,
19+
)
1520
from consts.model import ConversationResponse
1621
from database.client import get_monitoring_db_session
1722
from utils.auth_utils import get_current_user_id
@@ -21,19 +26,38 @@
2126
router = APIRouter(prefix="/monitoring")
2227

2328

29+
def _normalize_monitoring_provider(value: str | None) -> str:
30+
return str(value or "otlp").strip().lower()
31+
32+
33+
def get_monitoring_status() -> dict[str, Any]:
34+
"""Return telemetry state and the monitoring UI entrypoint for frontend use."""
35+
telemetry_enabled = ENABLE_TELEMETRY
36+
provider = _normalize_monitoring_provider(MONITORING_PROVIDER)
37+
dashboard_url = MONITORING_DASHBOARD_URL.strip() or None
38+
39+
return {
40+
"telemetry_enabled": telemetry_enabled,
41+
"provider": provider,
42+
"dashboard_url": dashboard_url,
43+
"dashboard_port": None,
44+
"dashboard_path": None,
45+
}
46+
47+
2448
def _compute_time_range_filter(time_range: str) -> str:
2549
"""Convert time_range parameter to SQL timestamp condition."""
2650
hours = {"24h": 24, "7d": 168, "30d": 720}.get(time_range, 24)
2751
return f"m.create_time >= NOW() - INTERVAL '{hours} hours'"
2852

2953

3054
def _query_model_metrics_from_db(
31-
time_range: str, tenant_id: Optional[str] = None
32-
) -> list[dict]:
55+
time_range: str, tenant_id: str | None = None
56+
) -> list[dict[str, Any]]:
3357
time_filter = _compute_time_range_filter(time_range)
3458

3559
tenant_filter = ""
36-
params = {}
60+
params: dict[str, str] = {}
3761
if tenant_id:
3862
tenant_filter = "AND m.tenant_id = :tenant_id"
3963
params["tenant_id"] = tenant_id
@@ -96,7 +120,7 @@ async def list_models_endpoint(
96120
page: Annotated[int, Query(ge=1, description="Page number")] = 1,
97121
page_size: Annotated[int, Query(
98122
ge=1, le=100, description="Items per page")] = 20,
99-
authorization: Annotated[Optional[str], Header()] = None,
123+
authorization: Annotated[str | None, Header()] = None,
100124
):
101125
"""List all models with aggregated monitoring metrics from database."""
102126
try:
@@ -113,3 +137,13 @@ async def list_models_endpoint(
113137
logger.error(f"Failed to list monitoring models: {str(e)}")
114138
raise HTTPException(
115139
status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(e))
140+
141+
142+
@router.get("/status", response_model=ConversationResponse)
143+
async def get_monitoring_status_endpoint():
144+
"""Return whether monitoring UI should be shown in the frontend."""
145+
return ConversationResponse(
146+
code=0,
147+
message="success",
148+
data=get_monitoring_status(),
149+
)

backend/apps/oauth_app.py

Lines changed: 110 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,36 @@
11
import logging
22

3-
from fastapi import APIRouter, Header, HTTPException
3+
from fastapi import APIRouter, Header, HTTPException, Request
44
from fastapi.responses import JSONResponse, RedirectResponse
55
from http import HTTPStatus
66
from typing import Optional
77

8+
from pydantic import ValidationError as PydanticValidationError
9+
10+
from consts.model import OAuthCompleteRequest
811
from consts.exceptions import OAuthLinkError, OAuthProviderError, UnauthorizedError
912
from consts.oauth_providers import get_all_provider_definitions
1013
from database.oauth_account_db import get_oauth_account_by_provider
1114
from services.oauth_service import (
15+
complete_pending_oauth_account,
1216
create_or_update_oauth_account,
1317
ensure_user_tenant_exists,
1418
exchange_code_for_provider_token,
19+
find_supabase_user_id_by_email,
20+
generate_pending_oauth_token,
1521
get_authorize_url,
1622
get_enabled_providers,
23+
get_pending_oauth_info,
1724
get_provider_user_info,
1825
list_linked_accounts,
19-
unlink_account, parse_state,
26+
parse_state,
27+
unlink_account,
2028
)
2129
from utils.auth_utils import (
2230
calculate_expires_at,
2331
generate_session_jwt,
24-
get_current_user_id, get_supabase_admin_client,
32+
get_current_user_id,
33+
get_supabase_admin_client,
2534
)
2635

2736
logger = logging.getLogger(__name__)
@@ -142,44 +151,37 @@ async def callback(
142151
if existing_binding:
143152
supabase_user_id = existing_binding["user_id"]
144153
else:
145-
# No binding found, search/create user by email in Supabase
146-
admin_client = get_supabase_admin_client()
147-
if not admin_client:
148-
raise RuntimeError("Supabase admin client not available")
149-
150154
supabase_user_id = None
151-
page = 1
152-
while True:
153-
users_resp = admin_client.auth.admin.list_users(
154-
page=page, per_page=100
155+
if email:
156+
admin_client = get_supabase_admin_client()
157+
if not admin_client:
158+
raise RuntimeError("Supabase admin client not available")
159+
supabase_user_id = find_supabase_user_id_by_email(
160+
admin_client,
161+
email,
155162
)
156-
users = users_resp if len(users_resp) > 0 else []
157-
if not users:
158-
break
159-
for u in users:
160-
if u.email and u.email.lower() == email.lower():
161-
supabase_user_id = u.id
162-
break
163-
if supabase_user_id:
164-
break
165-
if len(users) < 100:
166-
break
167-
page += 1
168163

169164
if not supabase_user_id:
170-
if not email:
171-
email = f"{provider}_{provider_user_id}@oauth.nexent"
172-
create_resp = admin_client.auth.admin.create_user(
173-
{
174-
"email": email,
175-
"email_confirm": True,
176-
"user_metadata": {
177-
"full_name": username,
165+
pending_token = generate_pending_oauth_token(
166+
provider=provider,
167+
provider_user_id=provider_user_id,
168+
provider_email=email,
169+
provider_username=username,
170+
)
171+
return JSONResponse(
172+
status_code=HTTPStatus.OK,
173+
content={
174+
"message": "OAuth account information required",
175+
"data": {
176+
"requires_account_completion": True,
177+
"pending_token": pending_token,
178178
"provider": provider,
179+
"provider_username": username,
180+
"provider_email": email,
181+
"email_required": not bool(email),
179182
},
180-
}
183+
},
181184
)
182-
supabase_user_id = create_resp.user.id
183185

184186
ensure_user_tenant_exists(user_id=supabase_user_id, email=email)
185187

@@ -214,6 +216,18 @@ async def callback(
214216
},
215217
)
216218

219+
except OAuthLinkError as e:
220+
logger.warning(f"OAuth callback link failed for provider={provider}: {e}")
221+
return JSONResponse(
222+
status_code=HTTPStatus.BAD_REQUEST,
223+
content={
224+
"message": "OAuth account link failed",
225+
"data": {
226+
"oauth_error": "oauth_account_already_bound",
227+
"oauth_error_description": "OAuth account is already bound to another user",
228+
},
229+
},
230+
)
217231
except Exception as e:
218232
logger.error(f"OAuth callback failed for provider={provider}: {e}")
219233
return JSONResponse(
@@ -228,6 +242,67 @@ async def callback(
228242
)
229243

230244

245+
@router.get("/pending")
246+
async def get_pending(
247+
pending_token: Optional[str] = Header(None, alias="X-OAuth-Pending-Token"),
248+
):
249+
try:
250+
pending = get_pending_oauth_info(pending_token or "")
251+
return JSONResponse(
252+
status_code=HTTPStatus.OK,
253+
content={"message": "success", "data": pending},
254+
)
255+
except OAuthLinkError as e:
256+
raise HTTPException(status_code=HTTPStatus.UNAUTHORIZED, detail=str(e))
257+
except OAuthProviderError as e:
258+
raise HTTPException(status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(e))
259+
except Exception as e:
260+
logger.error(f"Failed to get pending OAuth info: {e}")
261+
raise HTTPException(
262+
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
263+
detail="Failed to get pending OAuth info",
264+
)
265+
266+
267+
@router.post("/complete")
268+
async def complete(
269+
request: Request,
270+
pending_token: Optional[str] = Header(None, alias="X-OAuth-Pending-Token"),
271+
):
272+
try:
273+
request_data = OAuthCompleteRequest(**(await request.json()))
274+
result = await complete_pending_oauth_account(
275+
pending_token=pending_token or "",
276+
email=str(request_data.email) if request_data.email else None,
277+
password=request_data.password,
278+
invite_code=request_data.invite_code,
279+
)
280+
return JSONResponse(
281+
status_code=HTTPStatus.OK,
282+
content={"message": "OAuth account completed", "data": result},
283+
)
284+
except OAuthLinkError as e:
285+
status_code = (
286+
HTTPStatus.CONFLICT
287+
if "Email already exists" in str(e)
288+
else HTTPStatus.BAD_REQUEST
289+
)
290+
raise HTTPException(status_code=status_code, detail=str(e))
291+
except PydanticValidationError as e:
292+
raise HTTPException(
293+
status_code=HTTPStatus.UNPROCESSABLE_ENTITY,
294+
detail=e.errors(),
295+
)
296+
except OAuthProviderError as e:
297+
raise HTTPException(status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(e))
298+
except Exception as e:
299+
logger.error(f"Failed to complete OAuth account: {e}")
300+
raise HTTPException(
301+
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
302+
detail="Failed to complete OAuth account",
303+
)
304+
305+
231306
@router.get("/accounts")
232307
async def get_accounts(authorization: Optional[str] = Header(None)):
233308
if not authorization:
@@ -257,20 +332,7 @@ async def delete_account(provider: str, authorization: Optional[str] = Header(No
257332

258333
try:
259334
user_id, _ = get_current_user_id(authorization)
260-
261-
has_password_auth = False
262-
263-
admin_client = get_supabase_admin_client()
264-
if admin_client:
265-
try:
266-
user_resp = admin_client.auth.admin.get_user_by_id(user_id)
267-
user_metadata = getattr(user_resp.user, "user_metadata", {}) or {}
268-
signup_provider = user_metadata.get("provider", "email")
269-
has_password_auth = signup_provider == "email"
270-
except Exception as e:
271-
logger.warning(f"Failed to check user identities for {user_id}: {e}")
272-
273-
unlink_account(user_id, provider, has_password_auth=has_password_auth)
335+
unlink_account(user_id, provider)
274336
return JSONResponse(
275337
status_code=HTTPStatus.OK,
276338
content={

0 commit comments

Comments
 (0)