feat: refactor user configuration table

This commit is contained in:
Abhishek Kumar 2026-06-12 22:00:51 +05:30
parent 03daaba7a1
commit e5cc1308ed
31 changed files with 932 additions and 419 deletions

View file

@ -141,10 +141,6 @@ def mask_user_config(config: EffectiveAIModelConfiguration) -> Dict[str, Any]:
"is_realtime": config.is_realtime,
"test_phone_number": config.test_phone_number,
"timezone": config.timezone,
# Onboarding gate flags (not secrets) — surfaced so the UI can decide
# whether to show the post-signup onboarding form on boot.
"onboarding_completed_at": config.onboarding_completed_at,
"onboarding_skipped": config.onboarding_skipped,
}

View file

@ -113,13 +113,6 @@ def merge_user_configurations(
if "timezone" in incoming_partial:
merged["timezone"] = incoming_partial["timezone"]
# Onboarding gate flags: overwrite only when supplied.
if "onboarding_completed_at" in incoming_partial:
merged["onboarding_completed_at"] = incoming_partial["onboarding_completed_at"]
if "onboarding_skipped" in incoming_partial:
merged["onboarding_skipped"] = incoming_partial["onboarding_skipped"]
return EffectiveAIModelConfiguration.model_validate(merged)

View file

@ -257,12 +257,12 @@ SPEACHES_PROVIDER_MODEL_CONFIG = provider_model_config(
)
AZURE_SPEECH_PROVIDER_MODEL_CONFIG = provider_model_config(
"Azure Speech Services",
description="Azure Cognitive Services Speech - TTS and STT via the Azure Speech SDK.",
description="Azure Cognitive Services Speech TTS and STT via the Azure Speech SDK.",
provider_docs_url="https://learn.microsoft.com/en-us/azure/ai-services/speech-service/",
)
AZURE_REALTIME_PROVIDER_MODEL_CONFIG = provider_model_config(
"Azure OpenAI Realtime",
description="Azure OpenAI Realtime API - low-latency speech-to-speech conversations.",
description="Azure OpenAI Realtime API low-latency speech-to-speech conversations.",
provider_docs_url="https://learn.microsoft.com/en-us/azure/ai-services/openai/how-to/realtime-audio-quickstart",
)
@ -360,7 +360,7 @@ class GoogleVertexLLMConfiguration(BaseLLMConfiguration):
api_key: str | list[str] | None = Field(
default=None,
description=(
"Not used for Vertex AI - authentication is via the service account "
"Not used for Vertex AI authentication is via the service account "
"in `credentials` (or ADC). Leave blank."
),
)
@ -425,7 +425,7 @@ class AWSBedrockLLMConfiguration(BaseLLMConfiguration):
provider: Literal[ServiceProviders.AWS_BEDROCK] = ServiceProviders.AWS_BEDROCK
model: str = Field(
default="us.amazon.nova-pro-v1:0",
description="Bedrock model ID - include the region inference-profile prefix (e.g. 'us.').",
description="Bedrock model ID include the region inference-profile prefix (e.g. 'us.').",
json_schema_extra={"examples": AWS_BEDROCK_MODELS, "allow_custom_input": True},
)
aws_access_key: str = Field(
@ -442,7 +442,7 @@ class AWSBedrockLLMConfiguration(BaseLLMConfiguration):
)
api_key: str | list[str] | None = Field(
default=None,
description="Not used for Bedrock - authentication is via the AWS credentials above. Leave blank.",
description="Not used for Bedrock authentication is via the AWS credentials above. Leave blank.",
)
@ -682,7 +682,7 @@ class GoogleVertexRealtimeLLMConfiguration(BaseLLMConfiguration):
api_key: str | list[str] | None = Field(
default=None,
description=(
"Not used for Vertex AI - authentication is via the service account "
"Not used for Vertex AI authentication is via the service account "
"in `credentials` (or ADC). Leave blank."
),
)

View file

@ -0,0 +1,37 @@
from loguru import logger
from pydantic import ValidationError
from api.db import db_client
from api.enums import UserConfigurationKey
from api.schemas.onboarding_state import OnboardingState, OnboardingStateUpdate
async def get_onboarding_state(user_id: int) -> OnboardingState:
value = await db_client.get_user_configuration_value(
user_id, UserConfigurationKey.ONBOARDING.value
)
return _parse_state(value, user_id)
async def update_onboarding_state(
user_id: int, update: OnboardingStateUpdate
) -> OnboardingState:
state = update.apply_to(await get_onboarding_state(user_id))
await db_client.upsert_user_configuration_value(
user_id,
UserConfigurationKey.ONBOARDING.value,
state.model_dump(mode="json", exclude_none=True),
)
return state
def _parse_state(value, user_id: int) -> OnboardingState:
if not value or not isinstance(value, dict):
return OnboardingState()
try:
return OnboardingState.model_validate(value)
except ValidationError as exc:
logger.warning(
f"Invalid onboarding state for user {user_id}: {exc}. Returning defaults."
)
return OnboardingState()

View file

@ -176,7 +176,7 @@ class _ToolDocumentRefsMixin(BaseModel):
@node_spec(
name="startCall",
display_name="Start Call",
description="Entry point of the workflow - plays a greeting and opens the conversation.",
description="Entry point of the workflow plays a greeting and opens the conversation.",
llm_hint=(
"The entry point of every workflow (exactly one required). Plays an "
"optional greeting, can fetch context from an external API before the "
@ -344,7 +344,7 @@ class StartCallNodeData(
@node_spec(
name="agentNode",
display_name="Agent Node",
description="Conversational step - the LLM runs one focused exchange.",
description="Conversational step the LLM runs one focused exchange.",
llm_hint=(
"Mid-call step executed by the LLM. Most workflows are a chain of agent "
"nodes connected by edges that describe transition conditions. Each agent "
@ -613,9 +613,9 @@ class GlobalNodeData(BaseNodeData, _PromptedNodeDataMixin):
"description": (
"Path segment that uniquely identifies "
"this trigger. Used in both URLs:\n"
" • Production: `/api/v1/public/agent/<trigger_path>` - executes "
" • Production: `/api/v1/public/agent/<trigger_path>` executes "
"the published agent.\n"
" • Test: `/api/v1/public/agent/test/<trigger_path>` - executes "
" • Test: `/api/v1/public/agent/test/<trigger_path>` executes "
"the latest draft.\n"
"Can be customized to a descriptive value up to 36 characters "
"using letters, numbers, hyphens, or underscores."
@ -708,7 +708,7 @@ class TriggerNodeData(BaseNodeData):
"display_name": "Payload Template",
"description": (
"JSON body of the request. Values are Jinja-rendered against the "
"run context - `{{workflow_run_id}}`, `{{gathered_context.foo}}`, "
"run context `{{workflow_run_id}}`, `{{gathered_context.foo}}`, "
"`{{annotations.qa_xxx}}`, etc."
),
"ui_type": PropertyType.json,

View file

@ -229,7 +229,7 @@ class WorkflowGraph:
kind=ItemKind.workflow,
id=None,
field=None,
message="Workflow has no start node - exactly one is required",
message="Workflow has no start node exactly one is required",
)
)
elif len(start_nodes) > 1:
@ -239,7 +239,7 @@ class WorkflowGraph:
id=None,
field=None,
message=(
f"Workflow has {len(start_nodes)} start nodes - "
f"Workflow has {len(start_nodes)} start nodes "
f"exactly one is required"
),
)