mirror of
https://github.com/dograh-hq/dograh.git
synced 2026-06-10 08:05:22 +02:00
fix: decouple org preference and ai model preferences
This commit is contained in:
parent
e26d902425
commit
01d898fc72
21 changed files with 460 additions and 238 deletions
|
|
@ -447,11 +447,21 @@ class OrganizationUsageClient(BaseDBClient):
|
|||
pref_result = await session.execute(
|
||||
select(OrganizationConfigurationModel).where(
|
||||
OrganizationConfigurationModel.organization_id == organization_id,
|
||||
OrganizationConfigurationModel.key
|
||||
== OrganizationConfigurationKey.MODEL_CONFIGURATION_PREFERENCES.value,
|
||||
OrganizationConfigurationModel.key.in_(
|
||||
[
|
||||
OrganizationConfigurationKey.ORGANIZATION_PREFERENCES.value,
|
||||
OrganizationConfigurationKey.MODEL_CONFIGURATION_PREFERENCES.value,
|
||||
]
|
||||
),
|
||||
)
|
||||
)
|
||||
pref_obj = pref_result.scalar_one_or_none()
|
||||
pref_rows = pref_result.scalars().all()
|
||||
pref_by_key = {pref.key: pref for pref in pref_rows}
|
||||
pref_obj = pref_by_key.get(
|
||||
OrganizationConfigurationKey.ORGANIZATION_PREFERENCES.value
|
||||
) or pref_by_key.get(
|
||||
OrganizationConfigurationKey.MODEL_CONFIGURATION_PREFERENCES.value
|
||||
)
|
||||
if pref_obj and pref_obj.value:
|
||||
user_timezone = pref_obj.value.get("timezone") or user_timezone
|
||||
|
||||
|
|
|
|||
|
|
@ -92,9 +92,8 @@ class OrganizationConfigurationKey(Enum):
|
|||
MODEL_CONFIGURATION_V2 = (
|
||||
"MODEL_CONFIGURATION_V2" # Org-level v2 AI model configuration
|
||||
)
|
||||
MODEL_CONFIGURATION_PREFERENCES = (
|
||||
"MODEL_CONFIGURATION_PREFERENCES" # Org-level model configuration preferences
|
||||
)
|
||||
ORGANIZATION_PREFERENCES = "ORGANIZATION_PREFERENCES" # Org-level defaults such as timezone/test call number
|
||||
MODEL_CONFIGURATION_PREFERENCES = "MODEL_CONFIGURATION_PREFERENCES" # Deprecated; read fallback for old org preferences
|
||||
|
||||
|
||||
class WorkflowStatus(Enum):
|
||||
|
|
|
|||
|
|
@ -14,10 +14,10 @@ from api.schemas.ai_model_configuration import (
|
|||
DOGRAH_DEFAULT_LANGUAGE,
|
||||
DOGRAH_DEFAULT_VOICE,
|
||||
DOGRAH_SPEED_OPTIONS,
|
||||
OrganizationAIModelConfigurationPreferences,
|
||||
OrganizationAIModelConfigurationResponse,
|
||||
OrganizationAIModelConfigurationV2,
|
||||
)
|
||||
from api.schemas.organization_preferences import OrganizationPreferences
|
||||
from api.schemas.telephony_config import (
|
||||
TelephonyConfigRequest,
|
||||
TelephonyConfigurationCreateRequest,
|
||||
|
|
@ -39,13 +39,11 @@ from api.services.configuration.ai_model_configuration import (
|
|||
check_for_masked_keys_in_ai_model_configuration_v2,
|
||||
compile_ai_model_configuration_v2,
|
||||
convert_legacy_ai_model_configuration_to_v2,
|
||||
get_organization_ai_model_configuration_preferences,
|
||||
get_organization_ai_model_configuration_v2,
|
||||
get_resolved_ai_model_configuration,
|
||||
mask_ai_model_configuration_v2,
|
||||
merge_ai_model_configuration_v2_secrets,
|
||||
migrate_workflow_model_configurations_to_v2,
|
||||
upsert_organization_ai_model_configuration_preferences,
|
||||
upsert_organization_ai_model_configuration_v2,
|
||||
)
|
||||
from api.services.configuration.check_validity import UserConfigurationValidator
|
||||
|
|
@ -57,6 +55,10 @@ from api.services.configuration.registry import (
|
|||
ServiceProviders,
|
||||
ServiceType,
|
||||
)
|
||||
from api.services.organization_preferences import (
|
||||
get_organization_preferences,
|
||||
upsert_organization_preferences,
|
||||
)
|
||||
from api.services.posthog_client import capture_event
|
||||
from api.services.telephony import registry as telephony_registry
|
||||
from api.services.telephony.factory import get_telephony_provider_by_id
|
||||
|
|
@ -218,8 +220,6 @@ async def _model_configuration_v2_response(
|
|||
return OrganizationAIModelConfigurationResponse(
|
||||
configuration=mask_ai_model_configuration_v2(raw_configuration),
|
||||
effective_configuration=mask_user_config(resolved.effective),
|
||||
preferences=resolved.preferences
|
||||
or OrganizationAIModelConfigurationPreferences(),
|
||||
source=resolved.source,
|
||||
)
|
||||
|
||||
|
|
@ -363,30 +363,47 @@ async def migrate_model_configuration_v2(
|
|||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/model-configurations/preferences",
|
||||
response_model=OrganizationAIModelConfigurationPreferences,
|
||||
)
|
||||
async def get_model_configuration_preferences(
|
||||
@router.get("/preferences", response_model=OrganizationPreferences)
|
||||
async def get_preferences(
|
||||
user: UserModel = Depends(get_user_with_selected_organization),
|
||||
):
|
||||
organization_id = user.selected_organization_id
|
||||
return await get_organization_ai_model_configuration_preferences(organization_id)
|
||||
return await get_organization_preferences(organization_id)
|
||||
|
||||
|
||||
@router.put("/preferences", response_model=OrganizationPreferences)
|
||||
async def save_preferences(
|
||||
request: OrganizationPreferences,
|
||||
user: UserModel = Depends(get_user_with_selected_organization),
|
||||
):
|
||||
organization_id = user.selected_organization_id
|
||||
return await upsert_organization_preferences(
|
||||
organization_id,
|
||||
request,
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/model-configurations/preferences",
|
||||
response_model=OrganizationPreferences,
|
||||
include_in_schema=False,
|
||||
)
|
||||
async def get_model_configuration_preferences_legacy(
|
||||
user: UserModel = Depends(get_user_with_selected_organization),
|
||||
):
|
||||
return await get_preferences(user=user)
|
||||
|
||||
|
||||
@router.put(
|
||||
"/model-configurations/preferences",
|
||||
response_model=OrganizationAIModelConfigurationPreferences,
|
||||
response_model=OrganizationPreferences,
|
||||
include_in_schema=False,
|
||||
)
|
||||
async def save_model_configuration_preferences(
|
||||
request: OrganizationAIModelConfigurationPreferences,
|
||||
async def save_model_configuration_preferences_legacy(
|
||||
request: OrganizationPreferences,
|
||||
user: UserModel = Depends(get_user_with_selected_organization),
|
||||
):
|
||||
organization_id = user.selected_organization_id
|
||||
return await upsert_organization_ai_model_configuration_preferences(
|
||||
organization_id,
|
||||
request,
|
||||
)
|
||||
return await save_preferences(request=request, user=user)
|
||||
|
||||
|
||||
def preserve_masked_fields(provider: str, request_dict: dict, existing: dict):
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ class InitiateCallRequest(BaseModel):
|
|||
workflow_run_id: int | None = None
|
||||
phone_number: str | None = None
|
||||
# Optional explicit telephony config to use for the test call. If omitted,
|
||||
# falls back to the user's per-user default (when set), then the org default.
|
||||
# falls back to the org default.
|
||||
telephony_configuration_id: int | None = None
|
||||
# Optional caller-ID phone number to dial out from. Must belong to the
|
||||
# resolved telephony configuration; otherwise the provider picks one.
|
||||
|
|
@ -82,12 +82,9 @@ async def initiate_call(
|
|||
"""Initiate a call using the configured telephony provider from web browser. This is
|
||||
supposed to be a test call method for the draft version of the agent."""
|
||||
|
||||
from api.services.configuration.ai_model_configuration import (
|
||||
get_organization_ai_model_configuration_preferences,
|
||||
)
|
||||
from api.services.organization_preferences import get_organization_preferences
|
||||
|
||||
user_configuration = await db_client.get_user_configurations(user.id)
|
||||
preferences = await get_organization_ai_model_configuration_preferences(
|
||||
preferences = await get_organization_preferences(
|
||||
user.selected_organization_id,
|
||||
db=db_client,
|
||||
)
|
||||
|
|
@ -124,17 +121,12 @@ async def initiate_call(
|
|||
detail="telephony_not_configured",
|
||||
)
|
||||
|
||||
phone_number = (
|
||||
request.phone_number
|
||||
or preferences.test_phone_number
|
||||
or user_configuration.test_phone_number
|
||||
)
|
||||
phone_number = request.phone_number or preferences.test_phone_number
|
||||
|
||||
if not phone_number:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Phone number must be provided in request or set in user "
|
||||
"configuration",
|
||||
detail="Phone number must be provided in request or set in organization preferences",
|
||||
)
|
||||
|
||||
workflow = await db_client.get_workflow(
|
||||
|
|
|
|||
|
|
@ -11,9 +11,7 @@ from api.db.models import (
|
|||
)
|
||||
from api.services.auth.depends import get_user
|
||||
from api.services.configuration.ai_model_configuration import (
|
||||
get_organization_ai_model_configuration_preferences,
|
||||
get_resolved_ai_model_configuration,
|
||||
upsert_organization_ai_model_configuration_preferences,
|
||||
)
|
||||
from api.services.configuration.check_validity import (
|
||||
APIKeyStatusResponse,
|
||||
|
|
@ -24,6 +22,10 @@ from api.services.configuration.masking import check_for_masked_keys, mask_user_
|
|||
from api.services.configuration.merge import merge_user_configurations
|
||||
from api.services.configuration.registry import REGISTRY, ServiceType
|
||||
from api.services.mps_service_key_client import mps_service_key_client
|
||||
from api.services.organization_preferences import (
|
||||
get_organization_preferences,
|
||||
upsert_organization_preferences,
|
||||
)
|
||||
|
||||
router = APIRouter(prefix="/user")
|
||||
|
||||
|
|
@ -101,13 +103,12 @@ async def get_user_configurations(
|
|||
organization_id=user.selected_organization_id,
|
||||
)
|
||||
masked_config = mask_user_config(resolved_config.effective)
|
||||
if resolved_config.preferences:
|
||||
if resolved_config.preferences.test_phone_number is not None:
|
||||
masked_config["test_phone_number"] = (
|
||||
resolved_config.preferences.test_phone_number
|
||||
)
|
||||
if resolved_config.preferences.timezone is not None:
|
||||
masked_config["timezone"] = resolved_config.preferences.timezone
|
||||
if user.selected_organization_id:
|
||||
preferences = await get_organization_preferences(user.selected_organization_id)
|
||||
if preferences.test_phone_number is not None:
|
||||
masked_config["test_phone_number"] = preferences.test_phone_number
|
||||
if preferences.timezone is not None:
|
||||
masked_config["timezone"] = preferences.timezone
|
||||
|
||||
# Add organization pricing info if available
|
||||
if user.selected_organization_id:
|
||||
|
|
@ -133,43 +134,49 @@ async def update_user_configurations(
|
|||
|
||||
# Remove organization_pricing from incoming dict as it's read-only
|
||||
incoming_dict.pop("organization_pricing", None)
|
||||
preferences_update = {
|
||||
key: incoming_dict.pop(key)
|
||||
for key in ("test_phone_number", "timezone")
|
||||
if key in incoming_dict
|
||||
}
|
||||
|
||||
# Merge via helper
|
||||
try:
|
||||
user_configurations = merge_user_configurations(existing_config, incoming_dict)
|
||||
except ValidationError as e:
|
||||
raise HTTPException(status_code=422, detail=str(e))
|
||||
if incoming_dict:
|
||||
# Merge via helper
|
||||
try:
|
||||
user_configurations = merge_user_configurations(
|
||||
existing_config, incoming_dict
|
||||
)
|
||||
except ValidationError as e:
|
||||
raise HTTPException(status_code=422, detail=str(e))
|
||||
|
||||
try:
|
||||
check_for_masked_keys(user_configurations)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
try:
|
||||
check_for_masked_keys(user_configurations)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
try:
|
||||
validator = UserConfigurationValidator()
|
||||
await validator.validate(
|
||||
user_configurations,
|
||||
organization_id=user.selected_organization_id,
|
||||
created_by=user.provider_id,
|
||||
try:
|
||||
validator = UserConfigurationValidator()
|
||||
await validator.validate(
|
||||
user_configurations,
|
||||
organization_id=user.selected_organization_id,
|
||||
created_by=user.provider_id,
|
||||
)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=422, detail=e.args[0])
|
||||
|
||||
user_configurations = await db_client.update_user_configuration(
|
||||
user.id, user_configurations
|
||||
)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=422, detail=e.args[0])
|
||||
else:
|
||||
user_configurations = existing_config
|
||||
|
||||
user_configurations = await db_client.update_user_configuration(
|
||||
user.id, user_configurations
|
||||
)
|
||||
|
||||
if user.selected_organization_id and (
|
||||
request.test_phone_number is not None or request.timezone is not None
|
||||
):
|
||||
preferences = await get_organization_ai_model_configuration_preferences(
|
||||
user.selected_organization_id
|
||||
)
|
||||
if request.test_phone_number is not None:
|
||||
preferences.test_phone_number = request.test_phone_number
|
||||
if request.timezone is not None:
|
||||
preferences.timezone = request.timezone
|
||||
await upsert_organization_ai_model_configuration_preferences(
|
||||
if user.selected_organization_id and preferences_update:
|
||||
preferences = await get_organization_preferences(user.selected_organization_id)
|
||||
if "test_phone_number" in preferences_update:
|
||||
preferences.test_phone_number = preferences_update["test_phone_number"]
|
||||
if "timezone" in preferences_update:
|
||||
preferences.timezone = preferences_update["timezone"]
|
||||
await upsert_organization_preferences(
|
||||
user.selected_organization_id,
|
||||
preferences,
|
||||
)
|
||||
|
|
@ -177,9 +184,7 @@ async def update_user_configurations(
|
|||
# Return masked version of updated config
|
||||
masked_config = mask_user_config(user_configurations)
|
||||
if user.selected_organization_id:
|
||||
preferences = await get_organization_ai_model_configuration_preferences(
|
||||
user.selected_organization_id
|
||||
)
|
||||
preferences = await get_organization_preferences(user.selected_organization_id)
|
||||
if preferences.test_phone_number is not None:
|
||||
masked_config["test_phone_number"] = preferences.test_phone_number
|
||||
if preferences.timezone is not None:
|
||||
|
|
|
|||
|
|
@ -93,15 +93,9 @@ class OrganizationAIModelConfigurationV2(BaseModel):
|
|||
return self
|
||||
|
||||
|
||||
class OrganizationAIModelConfigurationPreferences(BaseModel):
|
||||
test_phone_number: str | None = None
|
||||
timezone: str | None = None
|
||||
|
||||
|
||||
class OrganizationAIModelConfigurationResponse(BaseModel):
|
||||
configuration: dict | None
|
||||
effective_configuration: dict
|
||||
preferences: OrganizationAIModelConfigurationPreferences
|
||||
source: Literal["organization_v2", "legacy_user_v1", "empty"]
|
||||
|
||||
|
||||
|
|
|
|||
6
api/schemas/organization_preferences.py
Normal file
6
api/schemas/organization_preferences.py
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class OrganizationPreferences(BaseModel):
|
||||
test_phone_number: str | None = None
|
||||
timezone: str | None = None
|
||||
|
|
@ -2,7 +2,6 @@ from __future__ import annotations
|
|||
|
||||
import copy
|
||||
from dataclasses import dataclass
|
||||
from inspect import isawaitable
|
||||
from typing import Literal
|
||||
|
||||
from loguru import logger
|
||||
|
|
@ -22,7 +21,6 @@ from api.schemas.ai_model_configuration import (
|
|||
BYOKPipelineAIModelConfiguration,
|
||||
BYOKRealtimeAIModelConfiguration,
|
||||
DograhManagedAIModelConfiguration,
|
||||
OrganizationAIModelConfigurationPreferences,
|
||||
OrganizationAIModelConfigurationV2,
|
||||
compile_ai_model_configuration_v2,
|
||||
)
|
||||
|
|
@ -45,7 +43,6 @@ class ResolvedAIModelConfiguration:
|
|||
effective: EffectiveAIModelConfiguration
|
||||
source: AIModelConfigurationSource
|
||||
organization_configuration: OrganizationAIModelConfigurationV2 | None = None
|
||||
preferences: OrganizationAIModelConfigurationPreferences | None = None
|
||||
|
||||
|
||||
@dataclass
|
||||
|
|
@ -60,9 +57,6 @@ async def get_resolved_ai_model_configuration(
|
|||
user_id: int | None,
|
||||
organization_id: int | None,
|
||||
) -> ResolvedAIModelConfiguration:
|
||||
preferences = await get_organization_ai_model_configuration_preferences(
|
||||
organization_id
|
||||
)
|
||||
organization_configuration = await get_organization_ai_model_configuration_v2(
|
||||
organization_id
|
||||
)
|
||||
|
|
@ -71,21 +65,18 @@ async def get_resolved_ai_model_configuration(
|
|||
effective=compile_ai_model_configuration_v2(organization_configuration),
|
||||
source="organization_v2",
|
||||
organization_configuration=organization_configuration,
|
||||
preferences=preferences,
|
||||
)
|
||||
|
||||
if user_id is None:
|
||||
return ResolvedAIModelConfiguration(
|
||||
effective=EffectiveAIModelConfiguration(),
|
||||
source="empty",
|
||||
preferences=preferences,
|
||||
)
|
||||
|
||||
legacy = await db_client.get_user_configurations(user_id)
|
||||
return ResolvedAIModelConfiguration(
|
||||
effective=legacy,
|
||||
source="legacy_user_v1" if _has_model_services(legacy) else "empty",
|
||||
preferences=preferences,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -135,33 +126,6 @@ async def get_organization_ai_model_configuration_v2(
|
|||
return None
|
||||
|
||||
|
||||
async def get_organization_ai_model_configuration_preferences(
|
||||
organization_id: int | None,
|
||||
db=None,
|
||||
) -> OrganizationAIModelConfigurationPreferences:
|
||||
if organization_id is None:
|
||||
return OrganizationAIModelConfigurationPreferences()
|
||||
db = db or db_client
|
||||
row = db.get_configuration(
|
||||
organization_id,
|
||||
OrganizationConfigurationKey.MODEL_CONFIGURATION_PREFERENCES.value,
|
||||
)
|
||||
if isawaitable(row):
|
||||
row = await row
|
||||
if row is None or not row.value:
|
||||
return OrganizationAIModelConfigurationPreferences()
|
||||
if not isinstance(row.value, dict):
|
||||
return OrganizationAIModelConfigurationPreferences()
|
||||
try:
|
||||
return OrganizationAIModelConfigurationPreferences.model_validate(row.value)
|
||||
except ValidationError as exc:
|
||||
logger.warning(
|
||||
"Invalid org AI model configuration preferences for organization "
|
||||
f"{organization_id}: {exc}. Returning defaults."
|
||||
)
|
||||
return OrganizationAIModelConfigurationPreferences()
|
||||
|
||||
|
||||
async def upsert_organization_ai_model_configuration_v2(
|
||||
organization_id: int,
|
||||
configuration: OrganizationAIModelConfigurationV2,
|
||||
|
|
@ -174,18 +138,6 @@ async def upsert_organization_ai_model_configuration_v2(
|
|||
return configuration
|
||||
|
||||
|
||||
async def upsert_organization_ai_model_configuration_preferences(
|
||||
organization_id: int,
|
||||
preferences: OrganizationAIModelConfigurationPreferences,
|
||||
) -> OrganizationAIModelConfigurationPreferences:
|
||||
await db_client.upsert_configuration(
|
||||
organization_id,
|
||||
OrganizationConfigurationKey.MODEL_CONFIGURATION_PREFERENCES.value,
|
||||
preferences.model_dump(mode="json", exclude_none=True),
|
||||
)
|
||||
return preferences
|
||||
|
||||
|
||||
async def migrate_workflow_model_configurations_to_v2(
|
||||
*,
|
||||
organization_id: int,
|
||||
|
|
|
|||
62
api/services/organization_preferences.py
Normal file
62
api/services/organization_preferences.py
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
from inspect import isawaitable
|
||||
|
||||
from loguru import logger
|
||||
from pydantic import ValidationError
|
||||
|
||||
from api.db import db_client
|
||||
from api.enums import OrganizationConfigurationKey
|
||||
from api.schemas.organization_preferences import OrganizationPreferences
|
||||
|
||||
|
||||
async def get_organization_preferences(
|
||||
organization_id: int | None,
|
||||
db=None,
|
||||
) -> OrganizationPreferences:
|
||||
if organization_id is None:
|
||||
return OrganizationPreferences()
|
||||
|
||||
db = db or db_client
|
||||
row = await _get_configuration(
|
||||
db,
|
||||
organization_id,
|
||||
OrganizationConfigurationKey.ORGANIZATION_PREFERENCES.value,
|
||||
)
|
||||
if row is None:
|
||||
row = await _get_configuration(
|
||||
db,
|
||||
organization_id,
|
||||
OrganizationConfigurationKey.MODEL_CONFIGURATION_PREFERENCES.value,
|
||||
)
|
||||
return _parse_preferences(row.value if row is not None else None, organization_id)
|
||||
|
||||
|
||||
async def upsert_organization_preferences(
|
||||
organization_id: int,
|
||||
preferences: OrganizationPreferences,
|
||||
) -> OrganizationPreferences:
|
||||
await db_client.upsert_configuration(
|
||||
organization_id,
|
||||
OrganizationConfigurationKey.ORGANIZATION_PREFERENCES.value,
|
||||
preferences.model_dump(mode="json", exclude_none=True),
|
||||
)
|
||||
return preferences
|
||||
|
||||
|
||||
async def _get_configuration(db, organization_id: int, key: str):
|
||||
row = db.get_configuration(organization_id, key)
|
||||
if isawaitable(row):
|
||||
row = await row
|
||||
return row
|
||||
|
||||
|
||||
def _parse_preferences(value, organization_id: int) -> OrganizationPreferences:
|
||||
if not value or not isinstance(value, dict):
|
||||
return OrganizationPreferences()
|
||||
try:
|
||||
return OrganizationPreferences.model_validate(value)
|
||||
except ValidationError as exc:
|
||||
logger.warning(
|
||||
"Invalid organization preferences for organization "
|
||||
f"{organization_id}: {exc}. Returning defaults."
|
||||
)
|
||||
return OrganizationPreferences()
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
from fastapi import FastAPI
|
||||
|
|
@ -14,14 +15,14 @@ from api.services.configuration.registry import (
|
|||
)
|
||||
|
||||
|
||||
def _make_test_app():
|
||||
def _make_test_app(selected_organization_id=None):
|
||||
app = FastAPI()
|
||||
app.include_router(router)
|
||||
|
||||
mock_user = MagicMock()
|
||||
mock_user.id = 1
|
||||
mock_user.is_superuser = False
|
||||
mock_user.selected_organization_id = None
|
||||
mock_user.selected_organization_id = selected_organization_id
|
||||
|
||||
app.dependency_overrides[get_user] = lambda: mock_user
|
||||
return app
|
||||
|
|
@ -210,3 +211,38 @@ class TestMaskedKeyRejection:
|
|||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
def test_preference_only_update_does_not_validate_or_save_model_config(self):
|
||||
"""Saving a test phone number through the legacy endpoint must not touch models."""
|
||||
app = _make_test_app(selected_organization_id=11)
|
||||
client = TestClient(app)
|
||||
preferences = SimpleNamespace(test_phone_number=None, timezone=None)
|
||||
|
||||
with (
|
||||
patch("api.routes.user.db_client") as mock_db,
|
||||
patch("api.routes.user.UserConfigurationValidator") as mock_validator,
|
||||
patch(
|
||||
"api.routes.user.get_organization_preferences",
|
||||
new=AsyncMock(return_value=preferences),
|
||||
),
|
||||
patch(
|
||||
"api.routes.user.upsert_organization_preferences",
|
||||
new=AsyncMock(return_value=preferences),
|
||||
) as upsert_preferences,
|
||||
):
|
||||
existing = _existing_openai_config()
|
||||
mock_db.get_user_configurations = AsyncMock(return_value=existing)
|
||||
mock_db.update_user_configuration = AsyncMock()
|
||||
mock_db.get_organization_by_id = AsyncMock(return_value=None)
|
||||
mock_validator.return_value.validate = AsyncMock()
|
||||
|
||||
response = client.put(
|
||||
"/user/configurations/user",
|
||||
json={"test_phone_number": "+15551234567"},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json()["test_phone_number"] == "+15551234567"
|
||||
mock_db.update_user_configuration.assert_not_called()
|
||||
mock_validator.return_value.validate.assert_not_called()
|
||||
upsert_preferences.assert_awaited_once()
|
||||
|
|
|
|||
|
|
@ -103,6 +103,61 @@ def test_initiate_call_executes_as_workflow_owner_for_shared_org_workflow():
|
|||
assert initiate_kwargs["workflow_id"] == workflow.id
|
||||
assert initiate_kwargs["user_id"] == workflow.user_id
|
||||
assert "user_id=99" in initiate_kwargs["webhook_url"]
|
||||
mock_db.get_user_configurations.assert_not_called()
|
||||
|
||||
|
||||
def test_initiate_call_uses_organization_preference_phone_number():
|
||||
app = _make_test_app()
|
||||
client = TestClient(app)
|
||||
|
||||
workflow = _workflow()
|
||||
provider = _provider()
|
||||
quota_mock = AsyncMock(
|
||||
return_value=SimpleNamespace(has_quota=True, error_message="")
|
||||
)
|
||||
|
||||
with (
|
||||
patch("api.routes.telephony.db_client") as mock_db,
|
||||
patch(
|
||||
"api.routes.telephony.check_dograh_quota_by_user_id",
|
||||
new=quota_mock,
|
||||
),
|
||||
patch(
|
||||
"api.routes.telephony.get_default_telephony_provider",
|
||||
new=AsyncMock(return_value=provider),
|
||||
),
|
||||
patch(
|
||||
"api.routes.telephony.get_backend_endpoints",
|
||||
new=AsyncMock(return_value=("https://api.example.com", "wss://ignored")),
|
||||
),
|
||||
):
|
||||
mock_db.get_user_configurations = AsyncMock(
|
||||
return_value=SimpleNamespace(test_phone_number="+15550000000")
|
||||
)
|
||||
mock_db.get_configuration = Mock(
|
||||
return_value=SimpleNamespace(value={"test_phone_number": "+15557654321"})
|
||||
)
|
||||
mock_db.get_default_telephony_configuration = AsyncMock(
|
||||
return_value=SimpleNamespace(id=55)
|
||||
)
|
||||
mock_db.get_workflow = AsyncMock(return_value=workflow)
|
||||
mock_db.create_workflow_run = AsyncMock(
|
||||
return_value=SimpleNamespace(
|
||||
id=501,
|
||||
name="WR-TEL-OUT-00000001",
|
||||
initial_context={},
|
||||
)
|
||||
)
|
||||
mock_db.update_workflow_run = AsyncMock()
|
||||
|
||||
response = client.post(
|
||||
"/telephony/initiate-call",
|
||||
json={"workflow_id": workflow.id},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert provider.initiate_call.await_args.kwargs["to_number"] == "+15557654321"
|
||||
mock_db.get_user_configurations.assert_not_called()
|
||||
|
||||
|
||||
def test_initiate_call_rejects_existing_run_for_different_workflow():
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -1,6 +1,6 @@
|
|||
# generated by datamodel-codegen:
|
||||
# filename: dograh-openapi-uraOZf.json
|
||||
# timestamp: 2026-06-03T11:53:30+00:00
|
||||
# filename: dograh-openapi-XXXXXX.json.mKgFDhNhca
|
||||
# timestamp: 2026-06-09T10:10:10+00:00
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { useEffect,useState } from 'react';
|
|||
import {
|
||||
getDailyReportApiV1OrganizationsReportsDailyGet,
|
||||
getDailyRunsDetailApiV1OrganizationsReportsDailyRunsGet,
|
||||
getPreferencesApiV1OrganizationsPreferencesGet,
|
||||
getWorkflowOptionsApiV1OrganizationsReportsWorkflowsGet
|
||||
} from '@/client/sdk.gen';
|
||||
import type { WorkflowRunDetail } from '@/client/types.gen';
|
||||
|
|
@ -16,7 +17,6 @@ import { Card } from '@/components/ui/card';
|
|||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import { useUserConfig } from '@/context/UserConfigContext';
|
||||
import { useAuth } from '@/lib/auth';
|
||||
|
||||
import { DispositionChart } from './components/DispositionChart';
|
||||
|
|
@ -57,11 +57,9 @@ export default function ReportsPage() {
|
|||
const [report, setReport] = useState<DailyReport | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const { userConfig } = useUserConfig();
|
||||
const [timezone, setTimezone] = useState('America/New_York');
|
||||
const auth = useAuth();
|
||||
|
||||
const timezone = userConfig?.timezone || 'America/New_York';
|
||||
|
||||
// Fetch workflows on mount
|
||||
useEffect(() => {
|
||||
const fetchWorkflows = async () => {
|
||||
|
|
@ -80,6 +78,22 @@ export default function ReportsPage() {
|
|||
fetchWorkflows();
|
||||
}, [auth.isAuthenticated]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchPreferences = async () => {
|
||||
if (!auth.isAuthenticated) return;
|
||||
|
||||
try {
|
||||
const response = await getPreferencesApiV1OrganizationsPreferencesGet();
|
||||
if (response.data?.timezone) {
|
||||
setTimezone(response.data.timezone);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch organization preferences:', err);
|
||||
}
|
||||
};
|
||||
fetchPreferences();
|
||||
}, [auth.isAuthenticated]);
|
||||
|
||||
// Fetch report data when date or workflow changes
|
||||
useEffect(() => {
|
||||
const fetchReport = async () => {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
import { ExternalLink } from "lucide-react";
|
||||
|
||||
import { MCPSection } from "@/components/MCPSection";
|
||||
import { ModelConfigurationPreferencesSection } from "@/components/ModelConfigurationPreferencesSection";
|
||||
import { OrganizationPreferencesSection } from "@/components/OrganizationPreferencesSection";
|
||||
import { TelemetrySection } from "@/components/TelemetrySection";
|
||||
import {
|
||||
Card,
|
||||
|
|
@ -33,7 +33,7 @@ export default function SettingsPage() {
|
|||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ModelConfigurationPreferencesSection />
|
||||
<OrganizationPreferencesSection />
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ import { useCallback, useEffect, useId, useState } from 'react';
|
|||
import TimezoneSelect, { type ITimezoneOption } from 'react-timezone-select';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
import { downloadUsageRunsReportApiV1OrganizationsUsageRunsReportGet, getDailyUsageBreakdownApiV1OrganizationsUsageDailyBreakdownGet, getMpsCreditsApiV1OrganizationsUsageMpsCreditsGet, getUsageHistoryApiV1OrganizationsUsageRunsGet } from '@/client/sdk.gen';
|
||||
import type { DailyUsageBreakdownResponse, MpsCreditsResponse, UsageHistoryResponse, WorkflowRunUsageResponse } from '@/client/types.gen';
|
||||
import { downloadUsageRunsReportApiV1OrganizationsUsageRunsReportGet, getDailyUsageBreakdownApiV1OrganizationsUsageDailyBreakdownGet, getMpsCreditsApiV1OrganizationsUsageMpsCreditsGet, getPreferencesApiV1OrganizationsPreferencesGet, getUsageHistoryApiV1OrganizationsUsageRunsGet, savePreferencesApiV1OrganizationsPreferencesPut } from '@/client/sdk.gen';
|
||||
import type { DailyUsageBreakdownResponse, MpsCreditsResponse, OrganizationPreferences, UsageHistoryResponse, WorkflowRunUsageResponse } from '@/client/types.gen';
|
||||
import { CallTypeCell } from '@/components/CallTypeCell';
|
||||
import { DailyUsageTable } from '@/components/DailyUsageTable';
|
||||
import { FilterBuilder } from '@/components/filters/FilterBuilder';
|
||||
|
|
@ -36,7 +36,7 @@ const getLocalTimezone = () => Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|||
export default function UsagePage() {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const { userConfig, saveUserConfig, loading: userConfigLoading, organizationPricing } = useUserConfig();
|
||||
const { organizationPricing } = useUserConfig();
|
||||
const auth = useAuth();
|
||||
|
||||
// MPS credits state
|
||||
|
|
@ -74,6 +74,8 @@ export default function UsagePage() {
|
|||
const localTimezone = getLocalTimezone();
|
||||
const [selectedTimezone, setSelectedTimezone] = useState<ITimezoneOption | string>('');
|
||||
const [savingTimezone, setSavingTimezone] = useState(false);
|
||||
const [preferences, setPreferences] = useState<OrganizationPreferences>({});
|
||||
const [preferencesLoading, setPreferencesLoading] = useState(true);
|
||||
const timezoneSelectId = useId(); // Stable ID for react-select to prevent hydration mismatch
|
||||
|
||||
// Fetch MPS credits
|
||||
|
|
@ -168,6 +170,23 @@ export default function UsagePage() {
|
|||
}
|
||||
}, [auth.isAuthenticated, organizationPricing]);
|
||||
|
||||
const fetchPreferences = useCallback(async () => {
|
||||
if (!auth.isAuthenticated) return;
|
||||
|
||||
setPreferencesLoading(true);
|
||||
try {
|
||||
const response = await getPreferencesApiV1OrganizationsPreferencesGet();
|
||||
const nextPreferences = response.data || {};
|
||||
setPreferences(nextPreferences);
|
||||
setSelectedTimezone(nextPreferences.timezone || localTimezone);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch organization preferences:', error);
|
||||
setSelectedTimezone(localTimezone);
|
||||
} finally {
|
||||
setPreferencesLoading(false);
|
||||
}
|
||||
}, [auth.isAuthenticated, localTimezone]);
|
||||
|
||||
// Download a CSV of all runs matching the current filters.
|
||||
const handleDownloadReport = async () => {
|
||||
if (!auth.isAuthenticated) return;
|
||||
|
|
@ -203,31 +222,31 @@ export default function UsagePage() {
|
|||
const handleTimezoneChange = async (timezone: ITimezoneOption | string) => {
|
||||
setSelectedTimezone(timezone);
|
||||
setSavingTimezone(true);
|
||||
const previousTimezone = preferences.timezone || localTimezone;
|
||||
try {
|
||||
const tzValue = typeof timezone === 'string' ? timezone : timezone.value;
|
||||
await saveUserConfig({ timezone: tzValue });
|
||||
const response = await savePreferencesApiV1OrganizationsPreferencesPut({
|
||||
body: {
|
||||
...preferences,
|
||||
timezone: tzValue,
|
||||
},
|
||||
});
|
||||
if (response.error) {
|
||||
throw new Error('Failed to save timezone');
|
||||
}
|
||||
setPreferences(response.data || { ...preferences, timezone: tzValue });
|
||||
} catch (error) {
|
||||
console.error('Failed to save timezone:', error);
|
||||
// Revert to previous timezone on error
|
||||
const prevTz = userConfig?.timezone || localTimezone;
|
||||
setSelectedTimezone(prevTz);
|
||||
setSelectedTimezone(previousTimezone);
|
||||
} finally {
|
||||
setSavingTimezone(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Update timezone when userConfig loads
|
||||
// Update timezone when organization preferences load.
|
||||
useEffect(() => {
|
||||
if (!userConfigLoading) {
|
||||
// Config has loaded - set the timezone
|
||||
if (userConfig?.timezone) {
|
||||
setSelectedTimezone(userConfig.timezone);
|
||||
} else {
|
||||
// No saved timezone, use local
|
||||
setSelectedTimezone(localTimezone);
|
||||
}
|
||||
}
|
||||
}, [userConfig, userConfigLoading, localTimezone]);
|
||||
fetchPreferences();
|
||||
}, [fetchPreferences]);
|
||||
|
||||
// Initial load - fetch when auth becomes available
|
||||
useEffect(() => {
|
||||
|
|
@ -340,8 +359,8 @@ export default function UsagePage() {
|
|||
instanceId={timezoneSelectId}
|
||||
value={selectedTimezone}
|
||||
onChange={handleTimezoneChange}
|
||||
isDisabled={savingTimezone || userConfigLoading}
|
||||
placeholder={userConfigLoading ? "Loading..." : "Select timezone"}
|
||||
isDisabled={savingTimezone || preferencesLoading}
|
||||
placeholder={preferencesLoading ? "Loading..." : "Select timezone"}
|
||||
styles={{
|
||||
control: (base, state) => ({
|
||||
...base,
|
||||
|
|
|
|||
|
|
@ -4,15 +4,21 @@ import 'react-international-phone/style.css';
|
|||
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { PhoneInput } from 'react-international-phone';
|
||||
|
||||
import {
|
||||
getPreferencesApiV1OrganizationsPreferencesGet,
|
||||
initiateCallApiV1TelephonyInitiateCallPost,
|
||||
listPhoneNumbersApiV1OrganizationsTelephonyConfigsConfigIdPhoneNumbersGet,
|
||||
listTelephonyConfigurationsApiV1OrganizationsTelephonyConfigsGet
|
||||
listTelephonyConfigurationsApiV1OrganizationsTelephonyConfigsGet,
|
||||
savePreferencesApiV1OrganizationsPreferencesPut,
|
||||
} from '@/client/sdk.gen';
|
||||
import type { PhoneNumberResponse, TelephonyConfigurationListItem } from '@/client/types.gen';
|
||||
import type {
|
||||
OrganizationPreferences,
|
||||
PhoneNumberResponse,
|
||||
TelephonyConfigurationListItem,
|
||||
} from '@/client/types.gen';
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
|
|
@ -33,6 +39,7 @@ import {
|
|||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { useUserConfig } from "@/context/UserConfigContext";
|
||||
import { detailFromError } from "@/lib/apiError";
|
||||
|
||||
interface PhoneCallDialogProps {
|
||||
open: boolean;
|
||||
|
|
@ -48,21 +55,40 @@ export const PhoneCallDialog = ({
|
|||
user,
|
||||
}: PhoneCallDialogProps) => {
|
||||
const router = useRouter();
|
||||
const { userConfig, saveUserConfig } = useUserConfig();
|
||||
const [phoneNumber, setPhoneNumber] = useState(userConfig?.test_phone_number || "");
|
||||
const { refreshConfig } = useUserConfig();
|
||||
const [preferences, setPreferences] = useState<OrganizationPreferences>({});
|
||||
const [preferencesLoaded, setPreferencesLoaded] = useState(false);
|
||||
const [phoneNumber, setPhoneNumber] = useState("");
|
||||
const [callLoading, setCallLoading] = useState(false);
|
||||
const [callError, setCallError] = useState<string | null>(null);
|
||||
const [callSuccessMsg, setCallSuccessMsg] = useState<string | null>(null);
|
||||
const [phoneChanged, setPhoneChanged] = useState(false);
|
||||
const [checkingConfig, setCheckingConfig] = useState(false);
|
||||
const [needsConfiguration, setNeedsConfiguration] = useState<boolean | null>(null);
|
||||
const [sipMode, setSipMode] = useState(() => /^(PJSIP|SIP)\//i.test(userConfig?.test_phone_number || ""));
|
||||
const [sipMode, setSipMode] = useState(false);
|
||||
const [telephonyConfigs, setTelephonyConfigs] = useState<TelephonyConfigurationListItem[]>([]);
|
||||
const [selectedConfigId, setSelectedConfigId] = useState<string>("");
|
||||
const [fromPhoneNumbers, setFromPhoneNumbers] = useState<PhoneNumberResponse[]>([]);
|
||||
const [selectedFromPhoneNumberId, setSelectedFromPhoneNumberId] = useState<string>("");
|
||||
const [loadingPhoneNumbers, setLoadingPhoneNumbers] = useState(false);
|
||||
|
||||
const fetchPreferences = useCallback(async () => {
|
||||
const result =
|
||||
await getPreferencesApiV1OrganizationsPreferencesGet();
|
||||
if (result.error) {
|
||||
throw new Error(detailFromError(result.error, "Failed to load phone preferences"));
|
||||
}
|
||||
return result.data || {};
|
||||
}, []);
|
||||
|
||||
const applyPreferences = useCallback((nextPreferences: OrganizationPreferences) => {
|
||||
const saved = nextPreferences.test_phone_number || "";
|
||||
setPreferences(nextPreferences);
|
||||
setPhoneNumber(saved);
|
||||
setSipMode(/^(PJSIP|SIP)\//i.test(saved));
|
||||
setPhoneChanged(false);
|
||||
}, []);
|
||||
|
||||
// Check telephony configuration when dialog opens
|
||||
useEffect(() => {
|
||||
const checkConfig = async () => {
|
||||
|
|
@ -97,6 +123,33 @@ export const PhoneCallDialog = ({
|
|||
checkConfig();
|
||||
}, [open]);
|
||||
|
||||
// Load organization-scoped call preferences when dialog opens.
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
|
||||
let cancelled = false;
|
||||
setPreferencesLoaded(false);
|
||||
|
||||
const loadPreferences = async () => {
|
||||
try {
|
||||
const nextPreferences = await fetchPreferences();
|
||||
if (cancelled) return;
|
||||
applyPreferences(nextPreferences);
|
||||
setPreferencesLoaded(true);
|
||||
} catch (err) {
|
||||
if (cancelled) return;
|
||||
applyPreferences({});
|
||||
setPreferencesLoaded(false);
|
||||
setCallError(err instanceof Error ? err.message : "Failed to load phone preferences");
|
||||
}
|
||||
};
|
||||
|
||||
loadPreferences();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [applyPreferences, fetchPreferences, open]);
|
||||
|
||||
// Reset state when dialog closes
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
|
|
@ -149,22 +202,9 @@ export const PhoneCallDialog = ({
|
|||
};
|
||||
}, [open, selectedConfigId]);
|
||||
|
||||
// Keep phoneNumber in sync with userConfig when dialog opens
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
const saved = userConfig?.test_phone_number || "";
|
||||
setPhoneNumber(saved);
|
||||
setSipMode(/^(PJSIP|SIP)\//i.test(saved));
|
||||
setPhoneChanged(false);
|
||||
setCallError(null);
|
||||
setCallSuccessMsg(null);
|
||||
setCallLoading(false);
|
||||
}
|
||||
}, [open, userConfig?.test_phone_number]);
|
||||
|
||||
const handlePhoneInputChange = (formattedValue: string) => {
|
||||
setPhoneNumber(formattedValue);
|
||||
setPhoneChanged(formattedValue !== userConfig?.test_phone_number);
|
||||
setPhoneChanged(formattedValue !== (preferences.test_phone_number || ""));
|
||||
setCallError(null);
|
||||
setCallSuccessMsg(null);
|
||||
};
|
||||
|
|
@ -174,17 +214,39 @@ export const PhoneCallDialog = ({
|
|||
router.push('/telephony-configurations');
|
||||
};
|
||||
|
||||
const savePhoneNumberPreference = async () => {
|
||||
const currentPreferences = preferencesLoaded ? preferences : await fetchPreferences();
|
||||
const result =
|
||||
await savePreferencesApiV1OrganizationsPreferencesPut({
|
||||
body: {
|
||||
...currentPreferences,
|
||||
test_phone_number: phoneNumber || null,
|
||||
},
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
throw new Error(detailFromError(result.error, "Failed to save phone preferences"));
|
||||
}
|
||||
if (!result.data) {
|
||||
throw new Error("Failed to save phone preferences");
|
||||
}
|
||||
|
||||
setPreferences(result.data);
|
||||
setPreferencesLoaded(true);
|
||||
setPhoneChanged(false);
|
||||
await refreshConfig();
|
||||
};
|
||||
|
||||
const handleStartCall = async () => {
|
||||
setCallLoading(true);
|
||||
setCallError(null);
|
||||
setCallSuccessMsg(null);
|
||||
try {
|
||||
if (!user || !userConfig) return;
|
||||
if (!user) return;
|
||||
|
||||
// Save phone number if it has changed
|
||||
if (phoneChanged) {
|
||||
await saveUserConfig({ ...userConfig, test_phone_number: phoneNumber });
|
||||
setPhoneChanged(false);
|
||||
await savePhoneNumberPreference();
|
||||
}
|
||||
|
||||
const response = await initiateCallApiV1TelephonyInitiateCallPost({
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -3580,20 +3580,6 @@ export type OpenRouterLlmConfiguration = {
|
|||
base_url?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* OrganizationAIModelConfigurationPreferences
|
||||
*/
|
||||
export type OrganizationAiModelConfigurationPreferences = {
|
||||
/**
|
||||
* Test Phone Number
|
||||
*/
|
||||
test_phone_number?: string | null;
|
||||
/**
|
||||
* Timezone
|
||||
*/
|
||||
timezone?: string | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* OrganizationAIModelConfigurationResponse
|
||||
*/
|
||||
|
|
@ -3610,7 +3596,6 @@ export type OrganizationAiModelConfigurationResponse = {
|
|||
effective_configuration: {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
preferences: OrganizationAiModelConfigurationPreferences;
|
||||
/**
|
||||
* Source
|
||||
*/
|
||||
|
|
@ -3633,6 +3618,20 @@ export type OrganizationAiModelConfigurationV2 = {
|
|||
byok?: ByokaiModelConfiguration | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* OrganizationPreferences
|
||||
*/
|
||||
export type OrganizationPreferences = {
|
||||
/**
|
||||
* Test Phone Number
|
||||
*/
|
||||
test_phone_number?: string | null;
|
||||
/**
|
||||
* Timezone
|
||||
*/
|
||||
timezone?: string | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* PhoneNumberCreateRequest
|
||||
*
|
||||
|
|
@ -10025,7 +10024,7 @@ export type MigrateModelConfigurationV2ApiV1OrganizationsModelConfigurationsV2Mi
|
|||
|
||||
export type MigrateModelConfigurationV2ApiV1OrganizationsModelConfigurationsV2MigratePostResponse = MigrateModelConfigurationV2ApiV1OrganizationsModelConfigurationsV2MigratePostResponses[keyof MigrateModelConfigurationV2ApiV1OrganizationsModelConfigurationsV2MigratePostResponses];
|
||||
|
||||
export type GetModelConfigurationPreferencesApiV1OrganizationsModelConfigurationsPreferencesGetData = {
|
||||
export type GetPreferencesApiV1OrganizationsPreferencesGetData = {
|
||||
body?: never;
|
||||
headers?: {
|
||||
/**
|
||||
|
|
@ -10039,10 +10038,10 @@ export type GetModelConfigurationPreferencesApiV1OrganizationsModelConfiguration
|
|||
};
|
||||
path?: never;
|
||||
query?: never;
|
||||
url: '/api/v1/organizations/model-configurations/preferences';
|
||||
url: '/api/v1/organizations/preferences';
|
||||
};
|
||||
|
||||
export type GetModelConfigurationPreferencesApiV1OrganizationsModelConfigurationsPreferencesGetErrors = {
|
||||
export type GetPreferencesApiV1OrganizationsPreferencesGetErrors = {
|
||||
/**
|
||||
* Not found
|
||||
*/
|
||||
|
|
@ -10053,19 +10052,19 @@ export type GetModelConfigurationPreferencesApiV1OrganizationsModelConfiguration
|
|||
422: HttpValidationError;
|
||||
};
|
||||
|
||||
export type GetModelConfigurationPreferencesApiV1OrganizationsModelConfigurationsPreferencesGetError = GetModelConfigurationPreferencesApiV1OrganizationsModelConfigurationsPreferencesGetErrors[keyof GetModelConfigurationPreferencesApiV1OrganizationsModelConfigurationsPreferencesGetErrors];
|
||||
export type GetPreferencesApiV1OrganizationsPreferencesGetError = GetPreferencesApiV1OrganizationsPreferencesGetErrors[keyof GetPreferencesApiV1OrganizationsPreferencesGetErrors];
|
||||
|
||||
export type GetModelConfigurationPreferencesApiV1OrganizationsModelConfigurationsPreferencesGetResponses = {
|
||||
export type GetPreferencesApiV1OrganizationsPreferencesGetResponses = {
|
||||
/**
|
||||
* Successful Response
|
||||
*/
|
||||
200: OrganizationAiModelConfigurationPreferences;
|
||||
200: OrganizationPreferences;
|
||||
};
|
||||
|
||||
export type GetModelConfigurationPreferencesApiV1OrganizationsModelConfigurationsPreferencesGetResponse = GetModelConfigurationPreferencesApiV1OrganizationsModelConfigurationsPreferencesGetResponses[keyof GetModelConfigurationPreferencesApiV1OrganizationsModelConfigurationsPreferencesGetResponses];
|
||||
export type GetPreferencesApiV1OrganizationsPreferencesGetResponse = GetPreferencesApiV1OrganizationsPreferencesGetResponses[keyof GetPreferencesApiV1OrganizationsPreferencesGetResponses];
|
||||
|
||||
export type SaveModelConfigurationPreferencesApiV1OrganizationsModelConfigurationsPreferencesPutData = {
|
||||
body: OrganizationAiModelConfigurationPreferences;
|
||||
export type SavePreferencesApiV1OrganizationsPreferencesPutData = {
|
||||
body: OrganizationPreferences;
|
||||
headers?: {
|
||||
/**
|
||||
* Authorization
|
||||
|
|
@ -10078,10 +10077,10 @@ export type SaveModelConfigurationPreferencesApiV1OrganizationsModelConfiguratio
|
|||
};
|
||||
path?: never;
|
||||
query?: never;
|
||||
url: '/api/v1/organizations/model-configurations/preferences';
|
||||
url: '/api/v1/organizations/preferences';
|
||||
};
|
||||
|
||||
export type SaveModelConfigurationPreferencesApiV1OrganizationsModelConfigurationsPreferencesPutErrors = {
|
||||
export type SavePreferencesApiV1OrganizationsPreferencesPutErrors = {
|
||||
/**
|
||||
* Not found
|
||||
*/
|
||||
|
|
@ -10092,16 +10091,16 @@ export type SaveModelConfigurationPreferencesApiV1OrganizationsModelConfiguratio
|
|||
422: HttpValidationError;
|
||||
};
|
||||
|
||||
export type SaveModelConfigurationPreferencesApiV1OrganizationsModelConfigurationsPreferencesPutError = SaveModelConfigurationPreferencesApiV1OrganizationsModelConfigurationsPreferencesPutErrors[keyof SaveModelConfigurationPreferencesApiV1OrganizationsModelConfigurationsPreferencesPutErrors];
|
||||
export type SavePreferencesApiV1OrganizationsPreferencesPutError = SavePreferencesApiV1OrganizationsPreferencesPutErrors[keyof SavePreferencesApiV1OrganizationsPreferencesPutErrors];
|
||||
|
||||
export type SaveModelConfigurationPreferencesApiV1OrganizationsModelConfigurationsPreferencesPutResponses = {
|
||||
export type SavePreferencesApiV1OrganizationsPreferencesPutResponses = {
|
||||
/**
|
||||
* Successful Response
|
||||
*/
|
||||
200: OrganizationAiModelConfigurationPreferences;
|
||||
200: OrganizationPreferences;
|
||||
};
|
||||
|
||||
export type SaveModelConfigurationPreferencesApiV1OrganizationsModelConfigurationsPreferencesPutResponse = SaveModelConfigurationPreferencesApiV1OrganizationsModelConfigurationsPreferencesPutResponses[keyof SaveModelConfigurationPreferencesApiV1OrganizationsModelConfigurationsPreferencesPutResponses];
|
||||
export type SavePreferencesApiV1OrganizationsPreferencesPutResponse = SavePreferencesApiV1OrganizationsPreferencesPutResponses[keyof SavePreferencesApiV1OrganizationsPreferencesPutResponses];
|
||||
|
||||
export type ListTelephonyConfigurationsApiV1OrganizationsTelephonyConfigsGetData = {
|
||||
body?: never;
|
||||
|
|
|
|||
|
|
@ -6,10 +6,10 @@ import TimezoneSelect, { type ITimezoneOption } from "react-timezone-select";
|
|||
import { toast } from "sonner";
|
||||
|
||||
import {
|
||||
getModelConfigurationPreferencesApiV1OrganizationsModelConfigurationsPreferencesGet,
|
||||
saveModelConfigurationPreferencesApiV1OrganizationsModelConfigurationsPreferencesPut,
|
||||
getPreferencesApiV1OrganizationsPreferencesGet,
|
||||
savePreferencesApiV1OrganizationsPreferencesPut,
|
||||
} from "@/client/sdk.gen";
|
||||
import type { OrganizationAiModelConfigurationPreferences } from "@/client/types.gen";
|
||||
import type { OrganizationPreferences } from "@/client/types.gen";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
|
|
@ -17,7 +17,7 @@ import { useUserConfig } from "@/context/UserConfigContext";
|
|||
import { detailFromError } from "@/lib/apiError";
|
||||
import { useAuth } from "@/lib/auth";
|
||||
|
||||
const emptyPreferences: OrganizationAiModelConfigurationPreferences = {
|
||||
const emptyPreferences: OrganizationPreferences = {
|
||||
test_phone_number: "",
|
||||
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC",
|
||||
};
|
||||
|
|
@ -88,14 +88,14 @@ function getTimezoneValue(tz: ITimezoneOption | string): string {
|
|||
return typeof tz === "string" ? tz : tz.value;
|
||||
}
|
||||
|
||||
export function ModelConfigurationPreferencesSection() {
|
||||
export function OrganizationPreferencesSection() {
|
||||
const { user, loading: authLoading } = useAuth();
|
||||
const { refreshConfig } = useUserConfig();
|
||||
const timezoneSelectId = useId();
|
||||
const hasFetched = useRef(false);
|
||||
|
||||
const [preferences, setPreferences] =
|
||||
useState<OrganizationAiModelConfigurationPreferences>(emptyPreferences);
|
||||
useState<OrganizationPreferences>(emptyPreferences);
|
||||
const [timezone, setTimezone] = useState<ITimezoneOption | string>(
|
||||
emptyPreferences.timezone || "UTC",
|
||||
);
|
||||
|
|
@ -114,13 +114,13 @@ export function ModelConfigurationPreferencesSection() {
|
|||
setLoading(true);
|
||||
try {
|
||||
const result =
|
||||
await getModelConfigurationPreferencesApiV1OrganizationsModelConfigurationsPreferencesGet();
|
||||
await getPreferencesApiV1OrganizationsPreferencesGet();
|
||||
|
||||
if (result.error) {
|
||||
toast.error(
|
||||
detailFromError(
|
||||
result.error,
|
||||
"Failed to load model configuration preferences",
|
||||
"Failed to load organization preferences",
|
||||
),
|
||||
);
|
||||
return;
|
||||
|
|
@ -135,7 +135,7 @@ export function ModelConfigurationPreferencesSection() {
|
|||
nextPreferences.timezone || emptyPreferences.timezone || "UTC",
|
||||
);
|
||||
} catch {
|
||||
toast.error("Failed to load model configuration preferences");
|
||||
toast.error("Failed to load organization preferences");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
|
|
@ -146,7 +146,7 @@ export function ModelConfigurationPreferencesSection() {
|
|||
setSaving(true);
|
||||
try {
|
||||
const result =
|
||||
await saveModelConfigurationPreferencesApiV1OrganizationsModelConfigurationsPreferencesPut(
|
||||
await savePreferencesApiV1OrganizationsPreferencesPut(
|
||||
{
|
||||
body: {
|
||||
test_phone_number: preferences.test_phone_number || null,
|
||||
Loading…
Add table
Add a link
Reference in a new issue