mirror of
https://github.com/dograh-hq/dograh.git
synced 2026-07-25 12:01:04 +02:00
fix: track org config validation timestamp
This commit is contained in:
parent
d22c073cb5
commit
c32b0ccb2f
5 changed files with 101 additions and 13 deletions
|
|
@ -0,0 +1,29 @@
|
|||
"""add last_validated_at on org config
|
||||
|
||||
Revision ID: c52dc3cccfb0
|
||||
Revises: b7e3c9a1d2f4
|
||||
Create Date: 2026-07-09 19:18:29.550267
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "c52dc3cccfb0"
|
||||
down_revision: Union[str, None] = "b7e3c9a1d2f4"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.add_column(
|
||||
"organization_configurations",
|
||||
sa.Column("last_validated_at", sa.DateTime(timezone=True), nullable=True),
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_column("organization_configurations", "last_validated_at")
|
||||
|
|
@ -205,11 +205,8 @@ class OrganizationConfigurationModel(Base):
|
|||
key = Column(String, nullable=False)
|
||||
value = Column(JSON, nullable=False, default=dict)
|
||||
created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(UTC))
|
||||
updated_at = Column(
|
||||
DateTime(timezone=True),
|
||||
default=lambda: datetime.now(UTC),
|
||||
onupdate=lambda: datetime.now(UTC),
|
||||
)
|
||||
updated_at = Column(DateTime(timezone=True), default=lambda: datetime.now(UTC))
|
||||
last_validated_at = Column(DateTime(timezone=True), nullable=True)
|
||||
|
||||
# Relationships
|
||||
organization = relationship("OrganizationModel", back_populates="configurations")
|
||||
|
|
|
|||
|
|
@ -34,10 +34,15 @@ class OrganizationConfigurationClient(BaseDBClient):
|
|||
return result.scalars().all()
|
||||
|
||||
async def upsert_configuration(
|
||||
self, organization_id: int, key: str, value: Any
|
||||
self,
|
||||
organization_id: int,
|
||||
key: str,
|
||||
value: Any,
|
||||
last_validated_at: datetime | None = None,
|
||||
) -> OrganizationConfigurationModel:
|
||||
"""Create or update a configuration for an organization."""
|
||||
async with self.async_session() as session:
|
||||
now = datetime.now(UTC)
|
||||
# First try to get existing configuration
|
||||
result = await session.execute(
|
||||
select(OrganizationConfigurationModel).where(
|
||||
|
|
@ -50,12 +55,16 @@ class OrganizationConfigurationClient(BaseDBClient):
|
|||
if config:
|
||||
# Update existing configuration
|
||||
config.value = value
|
||||
config.updated_at = now
|
||||
config.last_validated_at = last_validated_at
|
||||
else:
|
||||
# Create new configuration
|
||||
config = OrganizationConfigurationModel(
|
||||
organization_id=organization_id,
|
||||
key=key,
|
||||
value=value,
|
||||
updated_at=now,
|
||||
last_validated_at=last_validated_at,
|
||||
)
|
||||
session.add(config)
|
||||
|
||||
|
|
@ -67,10 +76,10 @@ class OrganizationConfigurationClient(BaseDBClient):
|
|||
await session.refresh(config)
|
||||
return config
|
||||
|
||||
async def touch_configuration(
|
||||
async def mark_configuration_validated(
|
||||
self, organization_id: int, key: str
|
||||
) -> Optional[OrganizationConfigurationModel]:
|
||||
"""Update the timestamp for an existing organization configuration."""
|
||||
"""Update the validation timestamp for an existing organization configuration."""
|
||||
async with self.async_session() as session:
|
||||
result = await session.execute(
|
||||
select(OrganizationConfigurationModel).where(
|
||||
|
|
@ -82,7 +91,7 @@ class OrganizationConfigurationClient(BaseDBClient):
|
|||
if not config:
|
||||
return None
|
||||
|
||||
config.updated_at = datetime.now(UTC)
|
||||
config.last_validated_at = datetime.now(UTC)
|
||||
try:
|
||||
await session.commit()
|
||||
except Exception as e:
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ from __future__ import annotations
|
|||
|
||||
import copy
|
||||
from dataclasses import dataclass
|
||||
from datetime import UTC, datetime
|
||||
from typing import Literal
|
||||
|
||||
from loguru import logger
|
||||
|
|
@ -72,7 +73,9 @@ async def get_resolved_ai_model_configuration(
|
|||
if organization_configuration is not None:
|
||||
effective = compile_ai_model_configuration_v2(organization_configuration)
|
||||
if organization_configuration_row is not None:
|
||||
effective.last_validated_at = organization_configuration_row.updated_at
|
||||
effective.last_validated_at = (
|
||||
organization_configuration_row.last_validated_at
|
||||
)
|
||||
return ResolvedAIModelConfiguration(
|
||||
effective=effective,
|
||||
source="organization_v2",
|
||||
|
|
@ -118,7 +121,7 @@ async def get_organization_ai_model_configuration_v2(
|
|||
async def update_organization_ai_model_configuration_last_validated_at(
|
||||
organization_id: int,
|
||||
) -> None:
|
||||
await db_client.touch_configuration(
|
||||
await db_client.mark_configuration_validated(
|
||||
organization_id,
|
||||
OrganizationConfigurationKey.MODEL_CONFIGURATION_V2.value,
|
||||
)
|
||||
|
|
@ -159,6 +162,7 @@ async def upsert_organization_ai_model_configuration_v2(
|
|||
organization_id,
|
||||
OrganizationConfigurationKey.MODEL_CONFIGURATION_V2.value,
|
||||
configuration.model_dump(mode="json", exclude_none=True),
|
||||
last_validated_at=datetime.now(UTC),
|
||||
)
|
||||
return configuration
|
||||
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ from api.services.configuration.ai_model_configuration import (
|
|||
mask_ai_model_configuration_v2,
|
||||
merge_ai_model_configuration_v2_secrets,
|
||||
migrate_workflow_configuration_model_override_to_v2,
|
||||
upsert_organization_ai_model_configuration_v2,
|
||||
)
|
||||
from api.services.configuration.check_validity import UserConfigurationValidator
|
||||
from api.services.configuration.masking import mask_key
|
||||
|
|
@ -150,7 +151,7 @@ async def test_byok_realtime_validator_does_not_require_stt_or_tts():
|
|||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_resolved_org_v2_uses_configuration_updated_at_as_validation_cache(
|
||||
async def test_resolved_org_v2_uses_last_validated_at_as_validation_cache(
|
||||
monkeypatch,
|
||||
):
|
||||
from datetime import UTC, datetime
|
||||
|
|
@ -164,7 +165,7 @@ async def test_resolved_org_v2_uses_configuration_updated_at_as_validation_cache
|
|||
)
|
||||
row = SimpleNamespace(
|
||||
value=config.model_dump(mode="json", exclude_none=True),
|
||||
updated_at=last_validated_at,
|
||||
last_validated_at=last_validated_at,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
ai_model_configuration.db_client,
|
||||
|
|
@ -178,6 +179,54 @@ async def test_resolved_org_v2_uses_configuration_updated_at_as_validation_cache
|
|||
assert resolved.effective.last_validated_at == last_validated_at
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_upsert_org_v2_marks_configuration_validated(monkeypatch):
|
||||
from api.services.configuration import ai_model_configuration
|
||||
|
||||
config = OrganizationAIModelConfigurationV2(
|
||||
mode="dograh",
|
||||
dograh=DograhManagedAIModelConfiguration(api_key="mps-secret"),
|
||||
)
|
||||
upsert = AsyncMock()
|
||||
monkeypatch.setattr(
|
||||
ai_model_configuration.db_client,
|
||||
"upsert_configuration",
|
||||
upsert,
|
||||
)
|
||||
|
||||
result = await upsert_organization_ai_model_configuration_v2(42, config)
|
||||
|
||||
assert result is config
|
||||
upsert.assert_awaited_once()
|
||||
args = upsert.await_args.args
|
||||
kwargs = upsert.await_args.kwargs
|
||||
assert args == (
|
||||
42,
|
||||
"MODEL_CONFIGURATION_V2",
|
||||
config.model_dump(mode="json", exclude_none=True),
|
||||
)
|
||||
assert kwargs["last_validated_at"].tzinfo is not None
|
||||
|
||||
|
||||
def test_org_config_validation_timestamp_update_does_not_touch_updated_at():
|
||||
from datetime import UTC, datetime
|
||||
|
||||
from sqlalchemy import update
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
from api.db.models import OrganizationConfigurationModel
|
||||
|
||||
stmt = (
|
||||
update(OrganizationConfigurationModel)
|
||||
.where(OrganizationConfigurationModel.id == 1)
|
||||
.values(last_validated_at=datetime.now(UTC))
|
||||
)
|
||||
|
||||
compiled = str(stmt.compile(dialect=postgresql.dialect()))
|
||||
assert "last_validated_at" in compiled
|
||||
assert "updated_at" not in compiled
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_pipeline_validator_requires_stt_and_tts_when_not_realtime():
|
||||
effective = EffectiveAIModelConfiguration(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue