diff --git a/api/oss/src/apis/fastapi/applications/models.py b/api/oss/src/apis/fastapi/applications/models.py index ad6f37b755..e69015a663 100644 --- a/api/oss/src/apis/fastapi/applications/models.py +++ b/api/oss/src/apis/fastapi/applications/models.py @@ -16,6 +16,7 @@ ApplicationEdit, ApplicationQuery, ApplicationFork, + ApplicationVariantFork, ApplicationRevisionsLog, # ApplicationVariant, @@ -157,6 +158,19 @@ class ApplicationForkRequest(BaseModel): ) +class ApplicationVariantForkRequest(BaseModel): + application_variant: ApplicationVariantFork = Field( + description="Config for the new variant (slug, name, description, flags).", + ) + application_variant_ref: Reference = Field( + description="Source variant to fork from.", + ) + application_revision_ref: Optional[Reference] = Field( + default=None, + description="Pin the fork to this revision; defaults to the source variant's head.", + ) + + class ApplicationRevisionsLogRequest(BaseModel): """Request body for `POST /applications/revisions/log`. @@ -164,7 +178,7 @@ class ApplicationRevisionsLogRequest(BaseModel): Each entry carries commit metadata and the full revision record. """ - application: ApplicationRevisionsLog = Field( + application_revisions: ApplicationRevisionsLog = Field( description=( "Filter for the log. Typically set `application_variant_id` to list " "the revision history of a single variant; optionally set " @@ -332,14 +346,6 @@ class ApplicationRevisionQueryRequest(BaseModel): default=None, description="Cursor pagination and time-range controls.", ) - resolve: Optional[bool] = Field( - default=None, - description=( - "When `true`, resolve embedded references in each returned " - "revision's `data` (for example, snippet references). " - "Defaults to `false`." - ), - ) class ApplicationRevisionCommitRequest(BaseModel): @@ -350,7 +356,7 @@ class ApplicationRevisionCommitRequest(BaseModel): See [Versioning](/reference/api-guide/versioning#committing-a-revision). """ - application_revision_commit: ApplicationRevisionCommit = Field( + application_revision: ApplicationRevisionCommit = Field( description=( "Commit payload. Must include `application_variant_id` and `data`. " "`message` is a human-readable commit message. `slug` is optional; " @@ -648,6 +654,14 @@ class ApplicationRevisionResolveRequest(BaseModel): description="Revision reference; resolves that exact revision.", ) # + application_revision: Optional[ApplicationRevision] = Field( + default=None, + description=( + "Resolve the references embedded in this revision payload directly, " + "without fetching it first. Only `data` is used; id and metadata are ignored." + ), + ) + # max_depth: Optional[int] = Field( default=10, description=( diff --git a/api/oss/src/apis/fastapi/applications/router.py b/api/oss/src/apis/fastapi/applications/router.py index c02f2e1ff6..12695e2505 100644 --- a/api/oss/src/apis/fastapi/applications/router.py +++ b/api/oss/src/apis/fastapi/applications/router.py @@ -31,14 +31,13 @@ ApplicationCatalogTemplate, ApplicationCatalogPreset, ApplicationRevisionCommit, - ApplicationRevisionData, ) from oss.src.apis.fastapi.applications.models import ( ApplicationCreateRequest, ApplicationEditRequest, ApplicationQueryRequest, - ApplicationForkRequest, + ApplicationVariantForkRequest, ApplicationRevisionsLogRequest, ApplicationResponse, ApplicationsResponse, @@ -1041,7 +1040,7 @@ async def fork_application_variant( *, application_variant_id: Optional[UUID] = None, # - application_variant_fork_request: ApplicationForkRequest, + application_variant_fork_request: ApplicationVariantForkRequest, ) -> ApplicationVariantResponse: """Fork an existing variant into a new variant on the same application. @@ -1062,23 +1061,25 @@ async def fork_application_variant( ): raise FORBIDDEN_EXCEPTION # type: ignore - fork_request = application_variant_fork_request.application - + application_variant_ref = ( + application_variant_fork_request.application_variant_ref + ) if application_variant_id: if ( - fork_request.application_variant_id - and fork_request.application_variant_id != application_variant_id + application_variant_ref.id + and application_variant_ref.id != application_variant_id ): return ApplicationVariantResponse() - - if not fork_request.application_variant_id: - fork_request.application_variant_id = application_variant_id + if not application_variant_ref.id: + application_variant_ref = Reference(id=application_variant_id) application_variant = await self.applications_service.fork_application_variant( project_id=UUID(request.state.project_id), user_id=UUID(request.state.user_id), # - application_fork=fork_request, + application_variant_fork=application_variant_fork_request.application_variant, + application_variant_ref=application_variant_ref, + application_revision_ref=application_variant_fork_request.application_revision_ref, ) application_variant_response = ApplicationVariantResponse( @@ -1603,8 +1604,6 @@ async def query_application_revisions( query, or filter on commit metadata (`author`, `date`, `message`) via the `application_revision` object. For the ordered history of a single variant, `POST /applications/revisions/log` is more direct. - Set `resolve: true` to inline embedded references in each revision's - `data`. """ if is_ee(): if not await check_action_access( # type: ignore @@ -1628,23 +1627,6 @@ async def query_application_revisions( windowing=application_revision_query_request.windowing, ) - # Optionally resolve embeds for all revisions if requested - if application_revisions and application_revision_query_request.resolve: - embeds_service = self.applications_service.embeds_service - - for revision in application_revisions: - if revision and revision.data: - try: - resolved_config, _ = await embeds_service.resolve_configuration( - project_id=UUID(request.state.project_id), - configuration=revision.data.model_dump(), - ) - revision.data = ApplicationRevisionData(**resolved_config) - except Exception as e: - log.error( - f"Failed to resolve embeds for revision {revision.id}: {e}" - ) - response = ApplicationRevisionsResponse( count=len(application_revisions), application_revisions=application_revisions, @@ -1686,7 +1668,7 @@ async def commit_application_revision( project_id=UUID(request.state.project_id), user_id=UUID(request.state.user_id), # - application_revision_commit=application_revision_commit_request.application_revision_commit, + application_revision_commit=application_revision_commit_request.application_revision, ) response = ApplicationRevisionResponse( @@ -1720,12 +1702,10 @@ async def log_application_revisions( ): raise FORBIDDEN_EXCEPTION # type: ignore - application_revisions = ( - await self.applications_service.log_application_revisions( - project_id=UUID(request.state.project_id), - # - application_revisions_log=application_revisions_log_request.application, - ) + application_revisions = await self.applications_service.log_application_revisions( + project_id=UUID(request.state.project_id), + # + application_revisions_log=application_revisions_log_request.application_revisions, ) revisions_response = ApplicationRevisionsResponse( @@ -1774,6 +1754,8 @@ async def resolve_application_revision( application_variant_ref=application_revision_resolve_request.application_variant_ref, application_revision_ref=application_revision_resolve_request.application_revision_ref, # + application_revision=application_revision_resolve_request.application_revision, + # max_depth=application_revision_resolve_request.max_depth or 10, max_embeds=application_revision_resolve_request.max_embeds or 100, error_policy=application_revision_resolve_request.error_policy.value diff --git a/api/oss/src/apis/fastapi/environments/models.py b/api/oss/src/apis/fastapi/environments/models.py index ff4c2f36f6..1db75df973 100644 --- a/api/oss/src/apis/fastapi/environments/models.py +++ b/api/oss/src/apis/fastapi/environments/models.py @@ -12,6 +12,8 @@ EnvironmentCreate, EnvironmentEdit, EnvironmentQuery, + EnvironmentFork, + EnvironmentVariantFork, EnvironmentRevisionsLog, # EnvironmentVariant, @@ -46,6 +48,25 @@ class EnvironmentEditRequest(BaseModel): environment: EnvironmentEdit +class EnvironmentForkRequest(BaseModel): + environment: EnvironmentFork = Field( + description="Fork specification. Supply the source variant and optional revision to fork from, plus slug/name for the new variant.", + ) + + +class EnvironmentVariantForkRequest(BaseModel): + environment_variant: EnvironmentVariantFork = Field( + description="Config for the new variant (slug, name, description, flags).", + ) + environment_variant_ref: Reference = Field( + description="Source variant to fork from.", + ) + environment_revision_ref: Optional[Reference] = Field( + default=None, + description="Pin the fork to this revision; defaults to the source variant's head.", + ) + + class EnvironmentQueryRequest(BaseModel): environment: Optional[EnvironmentQuery] = None # @@ -116,16 +137,15 @@ class EnvironmentRevisionQueryRequest(BaseModel): environment_variant_refs: Optional[List[Reference]] = None environment_revision_refs: Optional[List[Reference]] = None # - application_refs: Optional[List[Reference]] = None + references: Optional[List[Reference]] = None # include_archived: Optional[bool] = None # windowing: Optional[Windowing] = None - resolve: Optional[bool] = None # Optionally resolve embeds on query class EnvironmentRevisionCommitRequest(BaseModel): - environment_revision_commit: EnvironmentRevisionCommit + environment_revision: EnvironmentRevisionCommit class EnvironmentRevisionRetrieveRequest(BaseModel): @@ -162,7 +182,7 @@ class EnvironmentRevisionRetrieveRequest(BaseModel): class EnvironmentRevisionsLogRequest(BaseModel): - environment: EnvironmentRevisionsLog + environment_revisions: EnvironmentRevisionsLog class EnvironmentRevisionResponse(BaseModel): @@ -216,6 +236,14 @@ class EnvironmentRevisionResolveRequest(BaseModel): environment_variant_ref: Optional[Reference] = None environment_revision_ref: Optional[Reference] = None # + environment_revision: Optional[EnvironmentRevision] = Field( + default=None, + description=( + "Resolve the references embedded in this revision payload directly, " + "without fetching it first. Only `data` is used; id and metadata are ignored." + ), + ) + # max_depth: Optional[int] = 10 max_embeds: Optional[int] = 100 error_policy: Optional[ErrorPolicy] = ErrorPolicy.EXCEPTION diff --git a/api/oss/src/apis/fastapi/environments/router.py b/api/oss/src/apis/fastapi/environments/router.py index 457dda4c8b..06e84c1c87 100644 --- a/api/oss/src/apis/fastapi/environments/router.py +++ b/api/oss/src/apis/fastapi/environments/router.py @@ -18,8 +18,6 @@ EnvironmentFlags, EnvironmentEdit, EnvironmentRevisionCommit, - EnvironmentRevisionData, - # SimpleEnvironment, ) from oss.src.core.embeds.dtos import ErrorPolicy @@ -31,6 +29,7 @@ from oss.src.apis.fastapi.environments.models import ( EnvironmentCreateRequest, EnvironmentEditRequest, + EnvironmentVariantForkRequest, EnvironmentQueryRequest, EnvironmentRevisionsLogRequest, EnvironmentResponse, @@ -224,6 +223,16 @@ def __init__( response_model_exclude_none=True, ) + self.router.add_api_route( + "/variants/fork", + self.fork_environment_variant, + methods=["POST"], + operation_id="fork_environment_variant", + status_code=status.HTTP_200_OK, + response_model=EnvironmentVariantResponse, + response_model_exclude_none=True, + ) + # ENVIRONMENT REVISIONS ------------------------------------------------ self.router.add_api_route( @@ -744,6 +753,42 @@ async def query_environment_variants( return environment_variants_response + @intercept_exceptions() + @handle_git_exceptions() + async def fork_environment_variant( + self, + request: Request, + *, + environment_fork_request: EnvironmentVariantForkRequest, + ) -> EnvironmentVariantResponse: + """Fork an existing environment variant into a new variant. + + The new variant starts from the source variant's head revision (or a + pinned revision if `environment_revision_ref` is provided). Provide + `slug` and `name` in the fork body to identify the new variant. + """ + if is_ee(): + if not await check_action_access( # type: ignore + user_uid=request.state.user_id, + project_id=request.state.project_id, + permission=Permission.EDIT_ENVIRONMENTS, # type: ignore + ): + raise FORBIDDEN_EXCEPTION # type: ignore + + environment_variant = await self.environments_service.fork_environment_variant( + project_id=UUID(request.state.project_id), + user_id=UUID(request.state.user_id), + # + environment_variant_fork=environment_fork_request.environment_variant, + environment_variant_ref=environment_fork_request.environment_variant_ref, + environment_revision_ref=environment_fork_request.environment_revision_ref, + ) + + return EnvironmentVariantResponse( + count=1 if environment_variant else 0, + environment_variant=environment_variant, + ) + # ENVIRONMENT REVISIONS ------------------------------------------------ @intercept_exceptions() @@ -824,6 +869,8 @@ async def resolve_environment_revision_endpoint( environment_variant_ref=environment_revision_resolve_request.environment_variant_ref, environment_revision_ref=environment_revision_resolve_request.environment_revision_ref, # + environment_revision=environment_revision_resolve_request.environment_revision, + # max_depth=environment_revision_resolve_request.max_depth or 10, max_embeds=environment_revision_resolve_request.max_embeds or 100, error_policy=environment_revision_resolve_request.error_policy @@ -1075,30 +1122,13 @@ async def query_environment_revisions( environment_variant_refs=environment_revision_query_request.environment_variant_refs, environment_revision_refs=environment_revision_query_request.environment_revision_refs, # - application_refs=environment_revision_query_request.application_refs, + references=environment_revision_query_request.references, # include_archived=environment_revision_query_request.include_archived, # windowing=environment_revision_query_request.windowing, ) - # Optionally resolve embeds for all revisions if requested - if environment_revisions and environment_revision_query_request.resolve: - embeds_service = self.environments_service.embeds_service - - for revision in environment_revisions: - if revision and revision.data: - try: - resolved_config, _ = await embeds_service.resolve_configuration( - project_id=UUID(request.state.project_id), - configuration=revision.data.model_dump(), - ) - revision.data = EnvironmentRevisionData(**resolved_config) - except Exception as e: - log.error( - f"Failed to resolve embeds for revision {revision.id}: {e}" - ) - response = EnvironmentRevisionsResponse( count=len(environment_revisions), environment_revisions=environment_revisions, @@ -1129,7 +1159,7 @@ async def commit_environment_revision( ): raise FORBIDDEN_EXCEPTION # type: ignore - commit = environment_revision_commit_request.environment_revision_commit + commit = environment_revision_commit_request.environment_revision has_data = commit.data is not None has_delta = commit.delta is not None @@ -1156,7 +1186,7 @@ async def commit_environment_revision( project_id=UUID(request.state.project_id), user_id=UUID(request.state.user_id), # - environment_revision_commit=environment_revision_commit_request.environment_revision_commit, + environment_revision_commit=environment_revision_commit_request.environment_revision, ) return EnvironmentRevisionResponse( @@ -1179,12 +1209,10 @@ async def log_environment_revisions( ): raise FORBIDDEN_EXCEPTION # type: ignore - environment_revisions = ( - await self.environments_service.log_environment_revisions( - project_id=UUID(request.state.project_id), - # - environment_revisions_log=environment_revisions_log_request.environment, - ) + environment_revisions = await self.environments_service.log_environment_revisions( + project_id=UUID(request.state.project_id), + # + environment_revisions_log=environment_revisions_log_request.environment_revisions, ) revisions_response = EnvironmentRevisionsResponse( diff --git a/api/oss/src/apis/fastapi/evaluators/models.py b/api/oss/src/apis/fastapi/evaluators/models.py index 660eb2bee1..e81c34244f 100644 --- a/api/oss/src/apis/fastapi/evaluators/models.py +++ b/api/oss/src/apis/fastapi/evaluators/models.py @@ -13,6 +13,7 @@ EvaluatorEdit, EvaluatorQuery, EvaluatorFork, + EvaluatorVariantFork, EvaluatorRevisionsLog, # EvaluatorVariant, @@ -170,28 +171,23 @@ class EvaluatorVariantQueryRequest(BaseModel): ) -class EvaluatorVariantForkRequest(BaseModel): # TODO: FIX ME - """Legacy fork payload. Use `EvaluatorForkRequest` for new code.""" - - source_evaluator_variant_ref: Reference = Field( - description="Variant to fork from.", - ) - target_evaluator_ref: Reference = Field( - description="Evaluator that will receive the new variant.", +class EvaluatorVariantForkRequest(BaseModel): + evaluator_variant: EvaluatorVariantFork = Field( + description="Config for the new variant (slug, name, description, flags).", ) - slug: Optional[str] = Field(default=None, description="Slug for the new variant.") - name: Optional[str] = Field( - default=None, description="Display name for the new variant." + evaluator_variant_ref: Reference = Field( + description="Source variant to fork from.", ) - description: Optional[str] = Field( - default=None, description="Optional description." + evaluator_revision_ref: Optional[Reference] = Field( + default=None, + description="Pin the fork to this revision; defaults to the source variant's head.", ) class EvaluatorRevisionsLogRequest(BaseModel): """Body for listing the revision log of an evaluator variant.""" - evaluator: EvaluatorRevisionsLog = Field( + evaluator_revisions: EvaluatorRevisionsLog = Field( description="Log request scoped to an evaluator / variant / revision by id, slug, or version.", ) @@ -269,16 +265,12 @@ class EvaluatorRevisionQueryRequest(BaseModel): default=None, description="Cursor-based pagination controls.", ) - resolve: Optional[bool] = Field( - default=None, - description="When true, resolve embedded references on each returned revision's `data`.", - ) class EvaluatorRevisionCommitRequest(BaseModel): """Body for committing a new revision on a variant.""" - evaluator_revision_commit: EvaluatorRevisionCommit = Field( + evaluator_revision: EvaluatorRevisionCommit = Field( description="Commit payload carrying the `evaluator_variant_id`, optional commit `message`, and the revision `data`.", ) @@ -501,6 +493,14 @@ class EvaluatorRevisionResolveRequest(BaseModel): description="Resolve this specific revision.", ) # + evaluator_revision: Optional[EvaluatorRevision] = Field( + default=None, + description=( + "Resolve the references embedded in this revision payload directly, " + "without fetching it first. Only `data` is used; id and metadata are ignored." + ), + ) + # max_depth: Optional[int] = Field( default=10, description="Maximum recursion depth when following embedded references. Defaults to 10.", diff --git a/api/oss/src/apis/fastapi/evaluators/router.py b/api/oss/src/apis/fastapi/evaluators/router.py index 21a18a5386..08a36f2964 100644 --- a/api/oss/src/apis/fastapi/evaluators/router.py +++ b/api/oss/src/apis/fastapi/evaluators/router.py @@ -17,8 +17,6 @@ ) from oss.src.core.evaluators.dtos import ( EvaluatorRevisionCommit, - EvaluatorRevisionData, - # SimpleEvaluatorQuery, SimpleEvaluatorQueryFlags, ) @@ -38,7 +36,7 @@ EvaluatorCreateRequest, EvaluatorEditRequest, EvaluatorQueryRequest, - EvaluatorForkRequest, + EvaluatorVariantForkRequest, EvaluatorRevisionsLogRequest, EvaluatorResponse, EvaluatorsResponse, @@ -1070,7 +1068,7 @@ async def fork_evaluator_variant( *, evaluator_variant_id: Optional[UUID] = None, # - evaluator_variant_fork_request: EvaluatorForkRequest, + evaluator_variant_fork_request: EvaluatorVariantForkRequest, ): """Fork an evaluator variant into a new variant. @@ -1087,23 +1085,23 @@ async def fork_evaluator_variant( ): raise FORBIDDEN_EXCEPTION # type: ignore - fork_request = evaluator_variant_fork_request.evaluator - + evaluator_variant_ref = evaluator_variant_fork_request.evaluator_variant_ref if evaluator_variant_id: if ( - fork_request.evaluator_variant_id - and fork_request.evaluator_variant_id != evaluator_variant_id + evaluator_variant_ref.id + and evaluator_variant_ref.id != evaluator_variant_id ): return EvaluatorVariantResponse() - - if not fork_request.evaluator_variant_id: - fork_request.evaluator_variant_id = evaluator_variant_id + if not evaluator_variant_ref.id: + evaluator_variant_ref = Reference(id=evaluator_variant_id) evaluator_variant = await self.evaluators_service.fork_evaluator_variant( project_id=UUID(request.state.project_id), user_id=UUID(request.state.user_id), # - evaluator_fork=fork_request, + evaluator_variant_fork=evaluator_variant_fork_request.evaluator_variant, + evaluator_variant_ref=evaluator_variant_ref, + evaluator_revision_ref=evaluator_variant_fork_request.evaluator_revision_ref, ) evaluator_variant_response = EvaluatorVariantResponse( @@ -1611,8 +1609,7 @@ async def query_evaluator_revisions( Returns revision payloads. Use `evaluator_refs`, `evaluator_variant_refs`, or `evaluator_revision_refs` to scope - the query. Pass `resolve=true` to expand embedded references on - each revision's `data`. + the query. """ if is_ee(): if not await check_action_access( # type: ignore @@ -1636,23 +1633,6 @@ async def query_evaluator_revisions( windowing=evaluator_revision_query_request.windowing, ) - # Optionally resolve embeds for all revisions if requested - if evaluator_revisions and evaluator_revision_query_request.resolve: - embeds_service = self.evaluators_service.embeds_service - - for revision in evaluator_revisions: - if revision and revision.data: - try: - resolved_config, _ = await embeds_service.resolve_configuration( - project_id=UUID(request.state.project_id), - configuration=revision.data.model_dump(), - ) - revision.data = EvaluatorRevisionData(**resolved_config) - except Exception as e: - log.error( - f"Failed to resolve embeds for revision {revision.id}: {e}" - ) - response = EvaluatorRevisionsResponse( count=len(evaluator_revisions), evaluator_revisions=evaluator_revisions, @@ -1693,7 +1673,7 @@ async def commit_evaluator_revision( project_id=UUID(request.state.project_id), user_id=UUID(request.state.user_id), # - evaluator_revision_commit=evaluator_revision_commit_request.evaluator_revision_commit, + evaluator_revision_commit=evaluator_revision_commit_request.evaluator_revision, ) response = EvaluatorRevisionResponse( @@ -1729,7 +1709,7 @@ async def log_evaluator_revisions( evaluator_revisions = await self.evaluators_service.log_evaluator_revisions( project_id=UUID(request.state.project_id), # - evaluator_revisions_log=evaluator_revisions_log_request.evaluator, + evaluator_revisions_log=evaluator_revisions_log_request.evaluator_revisions, ) revisions_response = EvaluatorRevisionsResponse( @@ -1777,6 +1757,8 @@ async def resolve_evaluator_revision( evaluator_variant_ref=evaluator_revision_resolve_request.evaluator_variant_ref, evaluator_revision_ref=evaluator_revision_resolve_request.evaluator_revision_ref, # + evaluator_revision=evaluator_revision_resolve_request.evaluator_revision, + # max_depth=evaluator_revision_resolve_request.max_depth or 10, max_embeds=evaluator_revision_resolve_request.max_embeds or 100, error_policy=evaluator_revision_resolve_request.error_policy.value diff --git a/api/oss/src/apis/fastapi/queries/models.py b/api/oss/src/apis/fastapi/queries/models.py index 4204aadf04..61f051d554 100644 --- a/api/oss/src/apis/fastapi/queries/models.py +++ b/api/oss/src/apis/fastapi/queries/models.py @@ -12,6 +12,8 @@ QueryCreate, QueryEdit, QueryQuery, + QueryFork, + QueryVariantFork, # QueryVariant, QueryVariantCreate, @@ -42,6 +44,12 @@ class QueryEditRequest(BaseModel): query: QueryEdit +class QueryForkRequest(BaseModel): + query: QueryFork = Field( + description="Fork specification. Supply the source variant and optional revision to fork from, plus slug/name for the new variant.", + ) + + class QueryQueryRequest(BaseModel): query: Optional[QueryQuery] = None # @@ -85,13 +93,16 @@ class QueryVariantQueryRequest(BaseModel): class QueryVariantForkRequest(BaseModel): - source_query_variant_ref: Reference - target_query_ref: Reference - # - slug: Optional[str] = None - # - name: Optional[str] = None - description: Optional[str] = None + query_variant: QueryVariantFork = Field( + description="Config for the new variant (slug, name, description, flags).", + ) + query_variant_ref: Reference = Field( + description="Source variant to fork from.", + ) + query_revision_ref: Optional[Reference] = Field( + default=None, + description="Pin the fork to this revision; defaults to the source variant's head.", + ) class QueryVariantResponse(BaseModel): @@ -128,7 +139,7 @@ class QueryRevisionQueryRequest(BaseModel): class QueryRevisionCommitRequest(BaseModel): - query_revision_commit: QueryRevisionCommit + query_revision: QueryRevisionCommit class QueryRevisionsLogRequest(BaseModel): diff --git a/api/oss/src/apis/fastapi/queries/router.py b/api/oss/src/apis/fastapi/queries/router.py index c05ba310c0..c5eb1ac150 100644 --- a/api/oss/src/apis/fastapi/queries/router.py +++ b/api/oss/src/apis/fastapi/queries/router.py @@ -24,6 +24,7 @@ from oss.src.apis.fastapi.queries.models import ( QueryCreateRequest, QueryEditRequest, + QueryVariantForkRequest, QueryQueryRequest, QueryResponse, QueriesResponse, @@ -217,6 +218,16 @@ def __init__( response_model_exclude_none=True, ) + self.router.add_api_route( + "/variants/fork", + self.fork_query_variant, + methods=["POST"], + operation_id="fork_query_variant", + status_code=status.HTTP_200_OK, + response_model=QueryVariantResponse, + response_model_exclude_none=True, + ) + # QUERY REVISIONS ------------------------------------------------------ self.router.add_api_route( @@ -692,6 +703,42 @@ async def query_query_variants( query_variants=query_variants, ) + @intercept_exceptions() + @handle_git_exceptions() + async def fork_query_variant( + self, + request: Request, + *, + query_fork_request: QueryVariantForkRequest, + ) -> QueryVariantResponse: + """Fork an existing query variant into a new variant. + + The new variant starts from the source variant's head revision (or a + pinned revision if `query_revision_ref` is provided). Provide `slug` + and `name` in the fork body to identify the new variant. + """ + if is_ee(): + if not await check_action_access( # type: ignore + user_uid=request.state.user_id, + project_id=request.state.project_id, + permission=Permission.EDIT_QUERIES, # type: ignore + ): + raise FORBIDDEN_EXCEPTION # type: ignore + + query_variant = await self.queries_service.fork_query_variant( + project_id=UUID(request.state.project_id), + user_id=UUID(request.state.user_id), + # + query_variant_fork=query_fork_request.query_variant, + query_variant_ref=query_fork_request.query_variant_ref, + query_revision_ref=query_fork_request.query_revision_ref, + ) + + return QueryVariantResponse( + count=1 if query_variant else 0, + query_variant=query_variant, + ) + # QUERY REVISIONS ---------------------------------------------------------- @intercept_exceptions() @@ -925,7 +972,7 @@ async def commit_query_revision( project_id=UUID(request.state.project_id), user_id=UUID(request.state.user_id), # - query_revision_commit=query_revision_commit_request.query_revision_commit, + query_revision_commit=query_revision_commit_request.query_revision, ) query_revision_response = QueryRevisionResponse( diff --git a/api/oss/src/apis/fastapi/testsets/models.py b/api/oss/src/apis/fastapi/testsets/models.py index 23cb2607f6..2aec94aed9 100644 --- a/api/oss/src/apis/fastapi/testsets/models.py +++ b/api/oss/src/apis/fastapi/testsets/models.py @@ -12,6 +12,8 @@ TestsetCreate, TestsetEdit, TestsetQuery, + TestsetFork, + TestsetVariantFork, TestsetRevisionsLog, # TestsetVariant, @@ -46,6 +48,25 @@ class TestsetEditRequest(BaseModel): ) +class TestsetForkRequest(BaseModel): + testset: TestsetFork = Field( + description="Fork specification. Supply the source variant and optional revision to fork from, plus slug/name for the new variant.", + ) + + +class TestsetVariantForkRequest(BaseModel): + testset_variant: TestsetVariantFork = Field( + description="Config for the new variant (slug, name, description, flags).", + ) + testset_variant_ref: Reference = Field( + description="Source variant to fork from.", + ) + testset_revision_ref: Optional[Reference] = Field( + default=None, + description="Pin the fork to this revision; defaults to the source variant's head.", + ) + + class TestsetQueryRequest(BaseModel): testset: Optional[TestsetQuery] = Field( default=None, @@ -215,7 +236,7 @@ class TestsetRevisionQueryRequest(BaseModel): class TestsetRevisionCommitRequest(BaseModel): - testset_revision_commit: TestsetRevisionCommit = Field( + testset_revision: TestsetRevisionCommit = Field( description="New revision to commit. Pass either `data` (full replacement of the testcase list) or `delta` (add/remove/replace operations against the base revision) — not both.", ) include_testcases: Optional[bool] = Field( @@ -271,7 +292,7 @@ class TestsetRevisionRetrieveRequest(BaseModel): class TestsetRevisionsLogRequest(BaseModel): - testset_revision: TestsetRevisionsLog = Field( + testset_revisions: TestsetRevisionsLog = Field( description="Scope for the log: one of `testset_id`, `testset_variant_id`, or `testset_revision_id`. Optional `depth` limits how far back to walk.", ) include_testcases: Optional[bool] = Field( diff --git a/api/oss/src/apis/fastapi/testsets/router.py b/api/oss/src/apis/fastapi/testsets/router.py index 8659a0fa0e..efb2ba9abe 100644 --- a/api/oss/src/apis/fastapi/testsets/router.py +++ b/api/oss/src/apis/fastapi/testsets/router.py @@ -59,6 +59,7 @@ from oss.src.apis.fastapi.testsets.models import ( TestsetCreateRequest, TestsetEditRequest, + TestsetVariantForkRequest, TestsetQueryRequest, TestsetResponse, TestsetsResponse, @@ -402,6 +403,16 @@ def __init__( response_model_exclude_none=True, ) + self.router.add_api_route( + "/variants/fork", + self.fork_testset_variant, + methods=["POST"], + operation_id="fork_testset_variant", + status_code=status.HTTP_200_OK, + response_model=TestsetVariantResponse, + response_model_exclude_none=True, + ) + # TESTSET REVISIONS ---------------------------------------------------- self.router.add_api_route( @@ -974,6 +985,42 @@ async def query_testset_variants( return testset_variant_response + @intercept_exceptions() + @handle_git_exceptions() + async def fork_testset_variant( + self, + request: Request, + *, + testset_fork_request: TestsetVariantForkRequest, + ) -> TestsetVariantResponse: + """Fork an existing testset variant into a new variant. + + The new variant starts from the source variant's head revision (or a + pinned revision if `testset_revision_ref` is provided). Provide `slug` + and `name` in the fork body to identify the new variant. + """ + if is_ee(): + if not await check_action_access( # type: ignore + user_uid=request.state.user_id, + project_id=request.state.project_id, + permission=Permission.EDIT_TESTSETS, # type: ignore + ): + raise FORBIDDEN_EXCEPTION # type: ignore + + testset_variant = await self.testsets_service.fork_testset_variant( + project_id=UUID(request.state.project_id), + user_id=UUID(request.state.user_id), + # + testset_variant_fork=testset_fork_request.testset_variant, + testset_variant_ref=testset_fork_request.testset_variant_ref, + testset_revision_ref=testset_fork_request.testset_revision_ref, + ) + + return TestsetVariantResponse( + count=1 if testset_variant else 0, + testset_variant=testset_variant, + ) + # TESTSET REVISIONS -------------------------------------------------------- @intercept_exceptions() @@ -1431,7 +1478,7 @@ async def commit_testset_revision( ): raise FORBIDDEN_EXCEPTION # type: ignore - commit = testset_revision_commit_request.testset_revision_commit + commit = testset_revision_commit_request.testset_revision if commit.data and commit.delta: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, @@ -1582,7 +1629,7 @@ async def log_testset_revisions( testset_revisions = await self.testsets_service.log_testset_revisions( project_id=UUID(request.state.project_id), # - testset_revisions_log=testset_revisions_log_request.testset_revision, + testset_revisions_log=testset_revisions_log_request.testset_revisions, include_testcases=testset_revisions_log_request.include_testcases, ) diff --git a/api/oss/src/apis/fastapi/workflows/models.py b/api/oss/src/apis/fastapi/workflows/models.py index 0f40f14ec4..bd3be50b3c 100644 --- a/api/oss/src/apis/fastapi/workflows/models.py +++ b/api/oss/src/apis/fastapi/workflows/models.py @@ -18,6 +18,7 @@ WorkflowEdit, WorkflowQuery, WorkflowFork, + WorkflowVariantFork, WorkflowRevisionsLog, # WorkflowVariant, @@ -93,6 +94,19 @@ class WorkflowForkRequest(BaseModel): ) +class WorkflowVariantForkRequest(BaseModel): + workflow_variant: WorkflowVariantFork = Field( + description="Config for the new variant (slug, name, description, flags).", + ) + workflow_variant_ref: Reference = Field( + description="Source variant to fork from.", + ) + workflow_revision_ref: Optional[Reference] = Field( + default=None, + description="Pin the fork to this revision; defaults to the source variant's head.", + ) + + class WorkflowResponse(BaseModel): count: int = Field( default=0, @@ -361,7 +375,7 @@ class WorkflowRevisionDeployRequest(BaseModel): class WorkflowRevisionsLogRequest(BaseModel): - workflow: WorkflowRevisionsLog = Field( + workflow_revisions: WorkflowRevisionsLog = Field( description=( "Log query. Supply `workflow_id`, `workflow_variant_id`, or " "`workflow_revision_id` to scope the log, and an optional `depth`." diff --git a/api/oss/src/apis/fastapi/workflows/router.py b/api/oss/src/apis/fastapi/workflows/router.py index 85828cef1d..ca5445fed6 100644 --- a/api/oss/src/apis/fastapi/workflows/router.py +++ b/api/oss/src/apis/fastapi/workflows/router.py @@ -32,7 +32,7 @@ WorkflowCreateRequest, WorkflowEditRequest, WorkflowQueryRequest, - WorkflowForkRequest, + WorkflowVariantForkRequest, WorkflowResponse, WorkflowsResponse, # @@ -1156,7 +1156,7 @@ async def fork_workflow_variant( self, request: Request, *, - workflow_fork_request: WorkflowForkRequest, + workflow_fork_request: WorkflowVariantForkRequest, ) -> WorkflowVariantResponse: if is_ee(): if not await check_action_access( # type: ignore @@ -1170,7 +1170,9 @@ async def fork_workflow_variant( project_id=UUID(request.state.project_id), user_id=UUID(request.state.user_id), # - workflow_fork=workflow_fork_request.workflow, + workflow_variant_fork=workflow_fork_request.workflow_variant, + workflow_variant_ref=workflow_fork_request.workflow_variant_ref, + workflow_revision_ref=workflow_fork_request.workflow_revision_ref, ) # Invalidate legacy caches so the registry page reflects the forked variant @@ -1493,7 +1495,7 @@ async def log_workflow_revisions( workflow_revisions = await self.workflows_service.log_workflow_revisions( project_id=UUID(request.state.project_id), # - workflow_revisions_log=workflow_revisions_log_request.workflow, + workflow_revisions_log=workflow_revisions_log_request.workflow_revisions, ) await publish_revision_event( diff --git a/api/oss/src/core/applications/service.py b/api/oss/src/core/applications/service.py index 45d98d3aba..e8583232e1 100644 --- a/api/oss/src/core/applications/service.py +++ b/api/oss/src/core/applications/service.py @@ -7,7 +7,7 @@ WorkflowCreate, WorkflowEdit, WorkflowQuery, - WorkflowFork, + WorkflowVariantFork, # WorkflowVariantCreate, WorkflowVariantEdit, @@ -47,7 +47,7 @@ ApplicationRevisionsLog, ApplicationCreate, ApplicationEdit, - ApplicationFork, + ApplicationVariantFork, # ApplicationVariant, ApplicationVariantCreate, @@ -485,19 +485,21 @@ async def fork_application_variant( project_id: UUID, user_id: UUID, # - application_fork: ApplicationFork, + application_variant_fork: ApplicationVariantFork, + application_variant_ref: Reference, + application_revision_ref: Optional[Reference] = None, ) -> Optional[ApplicationVariant]: - workflow_fork = WorkflowFork( - **application_fork.model_dump( - mode="json", - ) + workflow_variant_fork = WorkflowVariantFork( + **application_variant_fork.model_dump(mode="json"), ) workflow_variant = await self.workflows_service.fork_workflow_variant( project_id=project_id, user_id=user_id, # - workflow_fork=workflow_fork, + workflow_variant_fork=workflow_variant_fork, + workflow_variant_ref=application_variant_ref, + workflow_revision_ref=application_revision_ref, ) if not workflow_variant: @@ -938,6 +940,8 @@ async def resolve_application_revision( application_variant_ref: Optional[Reference] = None, application_revision_ref: Optional[Reference] = None, # + application_revision: Optional["ApplicationRevision"] = None, + # max_depth: int = 10, max_embeds: int = 100, error_policy: str = "exception", @@ -947,24 +951,29 @@ async def resolve_application_revision( """ Fetch and resolve an application revision with embedded references. - Applications are workflows with is_application=True. This method - delegates to WorkflowsService.resolve_workflow_revision and converts - the result to Application types for backward compatibility. - - Args: - project_id: Project scope - user_id: User performing resolution - application_ref: Application reference - application_variant_ref: Variant reference - application_revision_ref: Revision reference - max_depth: Maximum nesting depth for embeds - max_embeds: Maximum total embeds allowed - error_policy: How to handle errors (exception, placeholder, keep) - include_archived: Include archived entities - - Returns: - Tuple of (ApplicationRevision with resolved configuration, ResolutionInfo metadata) + When `application_revision` is provided, resolves its data inline without + fetching from DB. Only `data` is used; id and metadata are ignored. """ + if not self.embeds_service: + raise RuntimeError("EmbedsService not initialized") + + if application_revision is not None: + if not application_revision.data: + return None + ( + resolved_data, + resolution_info, + ) = await self.embeds_service.resolve_configuration( + project_id=project_id, + configuration=application_revision.data.model_dump(mode="json"), + max_depth=max_depth, + max_embeds=max_embeds, + error_policy=ErrorPolicy(error_policy), + include_archived=include_archived, + ) + application_revision.data = ApplicationRevisionData(**resolved_data) + return (application_revision, resolution_info) + # Fetch the application revision revision = await self.fetch_application_revision( project_id=project_id, @@ -980,9 +989,6 @@ async def resolve_application_revision( return None # Use embeds service for resolution - if not self.embeds_service: - raise RuntimeError("EmbedsService not initialized") - ( revision_data, resolution_info, @@ -995,7 +1001,6 @@ async def resolve_application_revision( include_archived=include_archived, ) - # Update revision with resolved configuration revision.data = ApplicationRevisionData(**revision_data) return (revision, resolution_info) diff --git a/api/oss/src/core/environments/dtos.py b/api/oss/src/core/environments/dtos.py index 5c92065181..041c78eb77 100644 --- a/api/oss/src/core/environments/dtos.py +++ b/api/oss/src/core/environments/dtos.py @@ -20,11 +20,13 @@ ArtifactCreate, ArtifactEdit, ArtifactQuery, + ArtifactFork, # Variant, VariantCreate, VariantEdit, VariantQuery, + VariantFork, # Revision, RevisionsLog, @@ -32,6 +34,7 @@ RevisionEdit, RevisionQuery, RevisionCommit, + RevisionFork, ) @@ -232,6 +235,48 @@ class EnvironmentRevisionQuery(RevisionQuery): pass +class EnvironmentRevisionFork(RevisionFork): + data: Optional[EnvironmentRevisionData] = None + + +class EnvironmentVariantFork(VariantFork): + pass + + +class EnvironmentVariantForkAlias(AliasConfig): + environment_variant: Optional[EnvironmentVariantFork] = None + + variant: Optional[VariantFork] = Field( + default=None, + exclude=True, + alias="environment_variant", + ) + + +class EnvironmentRevisionForkAlias(AliasConfig): + environment_revision: Optional[EnvironmentRevisionFork] = None + + revision: Optional[RevisionFork] = Field( + default=None, + exclude=True, + alias="environment_revision", + ) + + +class EnvironmentFork( + ArtifactFork, + EnvironmentVariantIdAlias, + EnvironmentRevisionIdAlias, + EnvironmentVariantForkAlias, + EnvironmentRevisionForkAlias, +): + def model_post_init(self, __context) -> None: + sync_alias("environment_variant", "variant", self) + sync_alias("environment_revision", "revision", self) + sync_alias("environment_variant_id", "variant_id", self) + sync_alias("environment_revision_id", "revision_id", self) + + class EnvironmentRevisionCommit( RevisionCommit, EnvironmentIdAlias, diff --git a/api/oss/src/core/environments/service.py b/api/oss/src/core/environments/service.py index fb65c4b354..26fc57446a 100644 --- a/api/oss/src/core/environments/service.py +++ b/api/oss/src/core/environments/service.py @@ -9,6 +9,7 @@ EnvironmentEdit, EnvironmentFlags, EnvironmentQuery, + EnvironmentVariantFork, # EnvironmentRevision, EnvironmentRevisionCommit, @@ -39,6 +40,7 @@ ArtifactCreate, ArtifactEdit, ArtifactQuery, + ArtifactFork, RetrievalInfo, RevisionCommit, # @@ -649,6 +651,38 @@ async def query_environment_variants( return environment_variants + async def fork_environment_variant( + self, + *, + project_id: UUID, + user_id: UUID, + # + environment_variant_fork: EnvironmentVariantFork, + environment_variant_ref: Reference, + environment_revision_ref: Optional[Reference] = None, + ) -> Optional[EnvironmentVariant]: + _artifact_fork = ArtifactFork( + variant_id=environment_variant_ref.id, + revision_id=environment_revision_ref.id + if environment_revision_ref + else None, + variant=environment_variant_fork, + ) + + variant = await self.environments_dao.fork_variant( + project_id=project_id, + user_id=user_id, + # + artifact_fork=_artifact_fork, + ) + + if not variant: + return None + + return EnvironmentVariant( + **variant.model_dump(mode="json"), + ) + # environment revisions ------------------------------------------------ async def create_environment_revision( @@ -952,7 +986,7 @@ async def query_environment_revisions( environment_variant_refs: Optional[List[Reference]] = None, environment_revision_refs: Optional[List[Reference]] = None, # - application_refs: Optional[List[Reference]] = None, + references: Optional[List[Reference]] = None, # include_archived: Optional[bool] = None, # @@ -977,7 +1011,7 @@ async def query_environment_revisions( variant_refs=environment_variant_refs, revision_refs=environment_revision_refs, # - application_refs=application_refs, + references=references, # include_archived=include_archived, # @@ -1223,6 +1257,8 @@ async def resolve_environment_revision( environment_variant_ref: Optional[Reference] = None, environment_revision_ref: Optional[Reference] = None, # + environment_revision: Optional["EnvironmentRevision"] = None, + # max_depth: int = 10, max_embeds: int = 100, error_policy: str = "exception", @@ -1232,27 +1268,29 @@ async def resolve_environment_revision( """ Fetch and resolve an environment revision with embedded references. - Resolves embedded workflow and environment references within the - environment revision's configuration data. - - Args: - project_id: Project scope - user_id: User performing resolution - environment_ref: Environment reference - environment_variant_ref: Variant reference - environment_revision_ref: Revision reference - max_depth: Maximum nesting depth for embeds - max_embeds: Maximum total embeds allowed - error_policy: How to handle errors (exception, placeholder, keep) - include_archived: Include archived entities - - Returns: - Tuple of (EnvironmentRevision with resolved configuration, ResolutionInfo metadata) - - Raises: - Various embed resolution errors based on error_policy + When `environment_revision` is provided, resolves its data inline without + fetching from DB. Only `data` is used; id and metadata are ignored. """ - # Fetch the environment revision + if not self.embeds_service: + raise RuntimeError("EmbedsService not initialized") + + if environment_revision is not None: + if not environment_revision.data: + return None + ( + resolved_data, + resolution_info, + ) = await self.embeds_service.resolve_configuration( + project_id=project_id, + configuration=environment_revision.data.model_dump(mode="json"), + max_depth=max_depth, + max_embeds=max_embeds, + error_policy=ErrorPolicy(error_policy), + include_archived=include_archived, + ) + environment_revision.data = EnvironmentRevisionData(**resolved_data) + return (environment_revision, resolution_info) + revision = await self.fetch_environment_revision( project_id=project_id, # @@ -1266,10 +1304,6 @@ async def resolve_environment_revision( if not revision or not revision.data: return None - # Use embeds service for resolution - if not self.embeds_service: - raise RuntimeError("EmbedsService not initialized") - ( revision_data, resolution_info, @@ -1282,7 +1316,6 @@ async def resolve_environment_revision( include_archived=include_archived, ) - # Update revision with resolved configuration revision.data = EnvironmentRevisionData(**revision_data) return (revision, resolution_info) diff --git a/api/oss/src/core/evaluators/service.py b/api/oss/src/core/evaluators/service.py index b58eb446a2..e7b84cffc5 100644 --- a/api/oss/src/core/evaluators/service.py +++ b/api/oss/src/core/evaluators/service.py @@ -6,7 +6,7 @@ WorkflowCreate, WorkflowEdit, WorkflowQuery, - WorkflowFork, + WorkflowVariantFork, # WorkflowVariantCreate, WorkflowVariantEdit, @@ -46,7 +46,7 @@ EvaluatorRevisionsLog, EvaluatorCreate, EvaluatorEdit, - EvaluatorFork, + EvaluatorVariantFork, # EvaluatorVariant, EvaluatorVariantCreate, @@ -488,19 +488,21 @@ async def fork_evaluator_variant( project_id: UUID, user_id: UUID, # - evaluator_fork: EvaluatorFork, + evaluator_variant_fork: EvaluatorVariantFork, + evaluator_variant_ref: Reference, + evaluator_revision_ref: Optional[Reference] = None, ) -> Optional[EvaluatorVariant]: - workflow_fork = WorkflowFork( - **evaluator_fork.model_dump( - mode="json", - ) + workflow_variant_fork = WorkflowVariantFork( + **evaluator_variant_fork.model_dump(mode="json"), ) workflow_variant = await self.workflows_service.fork_workflow_variant( project_id=project_id, user_id=user_id, # - workflow_fork=workflow_fork, + workflow_variant_fork=workflow_variant_fork, + workflow_variant_ref=evaluator_variant_ref, + workflow_revision_ref=evaluator_revision_ref, ) if not workflow_variant: @@ -937,6 +939,8 @@ async def resolve_evaluator_revision( evaluator_variant_ref: Optional[Reference] = None, evaluator_revision_ref: Optional[Reference] = None, # + evaluator_revision: Optional["EvaluatorRevision"] = None, + # max_depth: int = 10, max_embeds: int = 100, error_policy: str = "exception", @@ -946,25 +950,29 @@ async def resolve_evaluator_revision( """ Fetch and resolve an evaluator revision with embedded references. - Evaluators are workflows with is_evaluator=True. This method - delegates to WorkflowsService.resolve_workflow_revision and converts - the result to Evaluator types for backward compatibility. - - Args: - project_id: Project scope - user_id: User performing resolution - evaluator_ref: Evaluator reference - evaluator_variant_ref: Variant reference - evaluator_revision_ref: Revision reference - max_depth: Maximum nesting depth for embeds - max_embeds: Maximum total embeds allowed - error_policy: How to handle errors (exception, placeholder, keep) - include_archived: Include archived entities - - Returns: - Tuple of (EvaluatorRevision with resolved configuration, ResolutionInfo metadata) + When `evaluator_revision` is provided, resolves its data inline without + fetching from DB. Only `data` is used; id and metadata are ignored. """ - # Fetch the evaluator revision + if not self.embeds_service: + raise RuntimeError("EmbedsService not initialized") + + if evaluator_revision is not None: + if not evaluator_revision.data: + return None + ( + resolved_data, + resolution_info, + ) = await self.embeds_service.resolve_configuration( + project_id=project_id, + configuration=evaluator_revision.data.model_dump(mode="json"), + max_depth=max_depth, + max_embeds=max_embeds, + error_policy=ErrorPolicy(error_policy), + include_archived=include_archived, + ) + evaluator_revision.data = EvaluatorRevisionData(**resolved_data) + return (evaluator_revision, resolution_info) + revision = await self.fetch_evaluator_revision( project_id=project_id, # @@ -978,10 +986,6 @@ async def resolve_evaluator_revision( if not revision or not revision.data: return None - # Use embeds service for resolution - if not self.embeds_service: - raise RuntimeError("EmbedsService not initialized") - ( revision_data, resolution_info, @@ -994,7 +998,6 @@ async def resolve_evaluator_revision( include_archived=include_archived, ) - # Update revision with resolved configuration revision.data = EvaluatorRevisionData(**revision_data) return (revision, resolution_info) diff --git a/api/oss/src/core/queries/service.py b/api/oss/src/core/queries/service.py index c9cd373ff7..ead219978b 100644 --- a/api/oss/src/core/queries/service.py +++ b/api/oss/src/core/queries/service.py @@ -51,7 +51,7 @@ QueryCreate, QueryEdit, QueryQuery, - QueryFork, + QueryVariantFork, # QueryVariant, QueryVariantCreate, @@ -583,10 +583,14 @@ async def fork_query_variant( project_id: UUID, user_id: UUID, # - query_fork: QueryFork, + query_variant_fork: QueryVariantFork, + query_variant_ref: Reference, + query_revision_ref: Optional[Reference] = None, ) -> Optional[QueryVariant]: _artifact_fork = ArtifactFork( - **query_fork.model_dump(mode="json"), + variant_id=query_variant_ref.id, + revision_id=query_revision_ref.id if query_revision_ref else None, + variant=query_variant_fork, ) variant = await self.queries_dao.fork_variant( diff --git a/api/oss/src/core/testsets/dtos.py b/api/oss/src/core/testsets/dtos.py index 744fe93af9..521c93254c 100644 --- a/api/oss/src/core/testsets/dtos.py +++ b/api/oss/src/core/testsets/dtos.py @@ -19,11 +19,13 @@ ArtifactCreate, ArtifactEdit, ArtifactQuery, + ArtifactFork, # Variant, VariantCreate, VariantEdit, VariantQuery, + VariantFork, # Revision, RevisionsLog, @@ -31,6 +33,7 @@ RevisionEdit, RevisionQuery, RevisionCommit, + RevisionFork, ) from oss.src.core.testcases.dtos import ( Testcase, @@ -229,6 +232,48 @@ class TestsetRevisionDelta(BaseModel): columns: Optional[TestsetRevisionDeltaColumns] = None +class TestsetRevisionFork(RevisionFork): + data: Optional[TestsetRevisionData] = None + + +class TestsetVariantFork(VariantFork): + pass + + +class TestsetVariantForkAlias(AliasConfig): + testset_variant: Optional[TestsetVariantFork] = None + + variant: Optional[VariantFork] = Field( + default=None, + exclude=True, + alias="testset_variant", + ) + + +class TestsetRevisionForkAlias(AliasConfig): + testset_revision: Optional[TestsetRevisionFork] = None + + revision: Optional[RevisionFork] = Field( + default=None, + exclude=True, + alias="testset_revision", + ) + + +class TestsetFork( + ArtifactFork, + TestsetVariantIdAlias, + TestsetRevisionIdAlias, + TestsetVariantForkAlias, + TestsetRevisionForkAlias, +): + def model_post_init(self, __context) -> None: + sync_alias("testset_variant", "variant", self) + sync_alias("testset_revision", "revision", self) + sync_alias("testset_variant_id", "variant_id", self) + sync_alias("testset_revision_id", "revision_id", self) + + class TestsetRevisionCommit( RevisionCommit, TestsetIdAlias, diff --git a/api/oss/src/core/testsets/service.py b/api/oss/src/core/testsets/service.py index 2f2fa6a19c..2dc478b49b 100644 --- a/api/oss/src/core/testsets/service.py +++ b/api/oss/src/core/testsets/service.py @@ -17,6 +17,7 @@ ArtifactCreate, ArtifactEdit, ArtifactQuery, + ArtifactFork, RetrievalInfo, # VariantCreate, @@ -33,6 +34,7 @@ TestsetEdit, TestsetQuery, TestsetRevisionsLog, + TestsetVariantFork, # TestsetVariant, TestsetVariantCreate, @@ -485,6 +487,36 @@ async def edit_testset_variant( return testset_variant + async def fork_testset_variant( + self, + *, + project_id: UUID, + user_id: UUID, + # + testset_variant_fork: TestsetVariantFork, + testset_variant_ref: Reference, + testset_revision_ref: Optional[Reference] = None, + ) -> Optional[TestsetVariant]: + _artifact_fork = ArtifactFork( + variant_id=testset_variant_ref.id, + revision_id=testset_revision_ref.id if testset_revision_ref else None, + variant=testset_variant_fork, + ) + + variant = await self.testsets_dao.fork_variant( + project_id=project_id, + user_id=user_id, + # + artifact_fork=_artifact_fork, + ) + + if not variant: + return None + + return TestsetVariant( + **variant.model_dump(mode="json"), + ) + async def archive_testset_variant( self, *, diff --git a/api/oss/src/core/workflows/service.py b/api/oss/src/core/workflows/service.py index 1a754ffef2..9217cfdf2a 100644 --- a/api/oss/src/core/workflows/service.py +++ b/api/oss/src/core/workflows/service.py @@ -55,7 +55,7 @@ WorkflowCreate, WorkflowEdit, WorkflowQuery, - WorkflowFork, + WorkflowVariantFork, WorkflowRevisionsLog, # WorkflowVariant, @@ -962,10 +962,14 @@ async def fork_workflow_variant( project_id: UUID, user_id: UUID, # - workflow_fork: WorkflowFork, + workflow_variant_fork: WorkflowVariantFork, + workflow_variant_ref: Reference, + workflow_revision_ref: Optional[Reference] = None, ) -> Optional[WorkflowVariant]: _artifact_fork = ArtifactFork( - **workflow_fork.model_dump(mode="json"), + variant_id=workflow_variant_ref.id, + revision_id=workflow_revision_ref.id if workflow_revision_ref else None, + variant=workflow_variant_fork, ) variant = await self.workflows_dao.fork_variant( diff --git a/api/oss/src/dbs/postgres/git/dao.py b/api/oss/src/dbs/postgres/git/dao.py index c601db6ebd..69943ad2d1 100644 --- a/api/oss/src/dbs/postgres/git/dao.py +++ b/api/oss/src/dbs/postgres/git/dao.py @@ -1308,7 +1308,7 @@ async def query_revisions( variant_refs: Optional[List[Reference]] = None, revision_refs: Optional[List[Reference]] = None, # - application_refs: Optional[List[Reference]] = None, + references: Optional[List[Reference]] = None, # include_archived: Optional[bool] = None, # @@ -1479,7 +1479,7 @@ async def query_revisions( revision_dbes = result.scalars().all() - # TEMPORARY ADAPTER: this `application_refs` filter exists only for the + # TEMPORARY ADAPTER: this `references` filter exists only for the # current web UX/UI, which wants to view environment history by # application and only surface revisions where that application's # deployment changed. This is not canonical DAO behavior. @@ -1489,8 +1489,8 @@ async def query_revisions( # this logic should not live in the DAO, but we are not moving it # elsewhere because the intended transition is from "temporary # adapter here" to "removed entirely". - if application_refs: - app_ids = {str(ref.id) for ref in application_refs if ref.id} + if references: + app_ids = {str(ref.id) for ref in references if ref.id} if app_ids: filtered_dbes = [] prev_app_revision_ids: dict[str, str | None] = { diff --git a/api/oss/tests/pytest/acceptance/applications/test_application_retrieval_info.py b/api/oss/tests/pytest/acceptance/applications/test_application_retrieval_info.py index 321008a7d7..b9ea82048e 100644 --- a/api/oss/tests/pytest/acceptance/applications/test_application_retrieval_info.py +++ b/api/oss/tests/pytest/acceptance/applications/test_application_retrieval_info.py @@ -28,7 +28,7 @@ def application_fixture(authed_api): "POST", "/applications/revisions/commit", json={ - "application_revision_commit": { + "application_revision": { "slug": f"{slug}-v0", "application_id": app_id, "application_variant_id": variant_id, @@ -41,7 +41,7 @@ def application_fixture(authed_api): "POST", "/applications/revisions/commit", json={ - "application_revision_commit": { + "application_revision": { "slug": f"{slug}-v1", "application_id": app_id, "application_variant_id": variant_id, @@ -132,7 +132,7 @@ def env_backed_application_fixture(authed_api): "POST", "/applications/revisions/commit", json={ - "application_revision_commit": { + "application_revision": { "slug": f"{slug}-v0", "application_id": app_id, "application_variant_id": variant_id, @@ -145,7 +145,7 @@ def env_backed_application_fixture(authed_api): "POST", "/applications/revisions/commit", json={ - "application_revision_commit": { + "application_revision": { "slug": f"{slug}-v1", "application_id": app_id, "application_variant_id": variant_id, @@ -177,7 +177,7 @@ def env_backed_application_fixture(authed_api): "POST", "/environments/revisions/commit", json={ - "environment_revision_commit": { + "environment_revision": { "slug": f"{env_slug}-r0", "environment_id": env_id, "environment_variant_id": env_variant_id, @@ -192,7 +192,7 @@ def env_backed_application_fixture(authed_api): "POST", "/environments/revisions/commit", json={ - "environment_revision_commit": { + "environment_revision": { "slug": f"{env_slug}-r1", "environment_id": env_id, "environment_variant_id": env_variant_id, diff --git a/api/oss/tests/pytest/acceptance/applications/test_application_variants_and_revisions.py b/api/oss/tests/pytest/acceptance/applications/test_application_variants_and_revisions.py index b3737b05fb..51489fd149 100644 --- a/api/oss/tests/pytest/acceptance/applications/test_application_variants_and_revisions.py +++ b/api/oss/tests/pytest/acceptance/applications/test_application_variants_and_revisions.py @@ -144,7 +144,7 @@ def test_commit_application_revision_generates_slug_when_missing(self, authed_ap "POST", "/applications/revisions/commit", json={ - "application_revision_commit": { + "application_revision": { "application_id": application["id"], "application_variant_id": variant["id"], "data": {"parameters": {"model": "test-model"}}, diff --git a/api/oss/tests/pytest/acceptance/environments/test_environment_retrieval_info.py b/api/oss/tests/pytest/acceptance/environments/test_environment_retrieval_info.py index 3f321f1c18..9bfda165cc 100644 --- a/api/oss/tests/pytest/acceptance/environments/test_environment_retrieval_info.py +++ b/api/oss/tests/pytest/acceptance/environments/test_environment_retrieval_info.py @@ -28,7 +28,7 @@ def environment_fixture(authed_api): "POST", "/environments/revisions/commit", json={ - "environment_revision_commit": { + "environment_revision": { "slug": f"{slug}-r0", "environment_id": env_id, "environment_variant_id": variant_id, @@ -43,7 +43,7 @@ def environment_fixture(authed_api): "POST", "/environments/revisions/commit", json={ - "environment_revision_commit": { + "environment_revision": { "slug": f"{slug}-r1", "environment_id": env_id, "environment_variant_id": variant_id, diff --git a/api/oss/tests/pytest/acceptance/evaluators/test_evaluator_retrieval_info.py b/api/oss/tests/pytest/acceptance/evaluators/test_evaluator_retrieval_info.py index c2df56ec69..2c2e62cf53 100644 --- a/api/oss/tests/pytest/acceptance/evaluators/test_evaluator_retrieval_info.py +++ b/api/oss/tests/pytest/acceptance/evaluators/test_evaluator_retrieval_info.py @@ -22,7 +22,7 @@ def evaluator_fixture(authed_api): "POST", "/evaluators/revisions/commit", json={ - "evaluator_revision_commit": { + "evaluator_revision": { "slug": f"{slug}-v0", "evaluator_id": eid, "evaluator_variant_id": vid, @@ -35,7 +35,7 @@ def evaluator_fixture(authed_api): "POST", "/evaluators/revisions/commit", json={ - "evaluator_revision_commit": { + "evaluator_revision": { "slug": f"{slug}-v1", "evaluator_id": eid, "evaluator_variant_id": vid, @@ -108,7 +108,7 @@ def env_backed_evaluator_fixture(authed_api): "POST", "/evaluators/revisions/commit", json={ - "evaluator_revision_commit": { + "evaluator_revision": { "slug": f"{slug}-v0", "evaluator_id": eid, "evaluator_variant_id": vid, @@ -121,7 +121,7 @@ def env_backed_evaluator_fixture(authed_api): "POST", "/evaluators/revisions/commit", json={ - "evaluator_revision_commit": { + "evaluator_revision": { "slug": f"{slug}-v1", "evaluator_id": eid, "evaluator_variant_id": vid, @@ -152,7 +152,7 @@ def env_backed_evaluator_fixture(authed_api): "POST", "/environments/revisions/commit", json={ - "environment_revision_commit": { + "environment_revision": { "slug": f"{env_slug}-r0", "environment_id": env_id, "environment_variant_id": env_variant_id, @@ -167,7 +167,7 @@ def env_backed_evaluator_fixture(authed_api): "POST", "/environments/revisions/commit", json={ - "environment_revision_commit": { + "environment_revision": { "slug": f"{env_slug}-r1", "environment_id": env_id, "environment_variant_id": env_variant_id, diff --git a/api/oss/tests/pytest/acceptance/legacy_variants/test_legacy_configs_fetch_acceptance.py b/api/oss/tests/pytest/acceptance/legacy_variants/test_legacy_configs_fetch_acceptance.py index 7f7d5cc8a2..8da36e387b 100644 --- a/api/oss/tests/pytest/acceptance/legacy_variants/test_legacy_configs_fetch_acceptance.py +++ b/api/oss/tests/pytest/acceptance/legacy_variants/test_legacy_configs_fetch_acceptance.py @@ -44,7 +44,7 @@ def _create_application_config(authed_api): "POST", "/applications/revisions/commit", json={ - "application_revision_commit": { + "application_revision": { "slug": f"{app_slug}-v0", "application_id": app_id, "application_variant_id": variant_id, @@ -59,7 +59,7 @@ def _create_application_config(authed_api): "POST", "/applications/revisions/commit", json={ - "application_revision_commit": { + "application_revision": { "slug": f"{app_slug}-v1", "application_id": app_id, "application_variant_id": variant_id, @@ -124,7 +124,7 @@ def _deploy_config_to_environment(authed_api, config): "POST", "/environments/revisions/commit", json={ - "environment_revision_commit": { + "environment_revision": { "slug": uuid4().hex[-12:], "environment_id": env_id, "environment_variant_id": env_variant_id, @@ -140,7 +140,7 @@ def _deploy_config_to_environment(authed_api, config): "POST", "/environments/revisions/commit", json={ - "environment_revision_commit": { + "environment_revision": { "slug": f"{env_slug}-v1", "environment_id": env_id, "environment_variant_id": env_variant_id, diff --git a/api/oss/tests/pytest/acceptance/loadables/test_loadable_strategies.py b/api/oss/tests/pytest/acceptance/loadables/test_loadable_strategies.py index 878a7fcc50..1d5b294bb0 100644 --- a/api/oss/tests/pytest/acceptance/loadables/test_loadable_strategies.py +++ b/api/oss/tests/pytest/acceptance/loadables/test_loadable_strategies.py @@ -180,7 +180,7 @@ def mock_data(authed_api): "POST", "/queries/revisions/commit", json={ - "query_revision_commit": { + "query_revision": { "slug": uuid4().hex[-12:], "query_id": query_id, "query_variant_id": query["variant_id"], @@ -703,7 +703,7 @@ def test_grumpy_a1_query_filter_matches_nothing(self, authed_api, mock_data): "POST", "/queries/revisions/commit", json={ - "query_revision_commit": { + "query_revision": { "slug": uuid4().hex[-12:], "query_id": query["id"], "query_variant_id": query["variant_id"], diff --git a/api/oss/tests/pytest/acceptance/queries/test_queries_basics.py b/api/oss/tests/pytest/acceptance/queries/test_queries_basics.py index 74db92b7cb..0eba11ca1f 100644 --- a/api/oss/tests/pytest/acceptance/queries/test_queries_basics.py +++ b/api/oss/tests/pytest/acceptance/queries/test_queries_basics.py @@ -66,7 +66,7 @@ def test_create_simple_query_returns_200(self, authed_api): "POST", "/queries/revisions/commit", json={ - "query_revision_commit": { + "query_revision": { "slug": uuid4().hex[-12:], "query_id": query["id"], "query_variant_id": query["variant_id"], @@ -107,7 +107,7 @@ def test_create_simple_query_stores_filtering(self, authed_api): "POST", "/queries/revisions/commit", json={ - "query_revision_commit": { + "query_revision": { "slug": uuid4().hex[-12:], "query_id": query["id"], "query_variant_id": query["variant_id"], diff --git a/api/oss/tests/pytest/acceptance/queries/test_query_retrieval_info.py b/api/oss/tests/pytest/acceptance/queries/test_query_retrieval_info.py index 64570c72eb..9b67a521d3 100644 --- a/api/oss/tests/pytest/acceptance/queries/test_query_retrieval_info.py +++ b/api/oss/tests/pytest/acceptance/queries/test_query_retrieval_info.py @@ -26,7 +26,7 @@ def query_fixture(authed_api): "POST", "/queries/revisions/commit", json={ - "query_revision_commit": { + "query_revision": { "slug": f"{slug}-v0", "query_id": qid, "query_variant_id": vid, @@ -39,7 +39,7 @@ def query_fixture(authed_api): "POST", "/queries/revisions/commit", json={ - "query_revision_commit": { + "query_revision": { "slug": f"{slug}-v1", "query_id": qid, "query_variant_id": vid, diff --git a/api/oss/tests/pytest/acceptance/test_revision_commit_extra_forbid.py b/api/oss/tests/pytest/acceptance/test_revision_commit_extra_forbid.py index 2abab8c69d..d3774a2e43 100644 --- a/api/oss/tests/pytest/acceptance/test_revision_commit_extra_forbid.py +++ b/api/oss/tests/pytest/acceptance/test_revision_commit_extra_forbid.py @@ -60,7 +60,7 @@ def test_commit_workflow_revision_rejects_unknown_data_field(authed_api): authed_api, "/workflows/revisions/commit", { - "workflow_revision_commit": { + "workflow_revision": { "slug": uuid4().hex[-12:], "workflow_id": workflow["id"], "workflow_variant_id": variant["id"], @@ -121,7 +121,7 @@ def test_commit_application_revision_rejects_unknown_data_field(authed_api): authed_api, "/applications/revisions/commit", { - "application_revision_commit": { + "application_revision": { "application_id": application["id"], "application_variant_id": variant["id"], "data": {"ag_config": {"prompt": {}}}, @@ -181,7 +181,7 @@ def test_commit_evaluator_revision_rejects_unknown_data_field(authed_api): authed_api, "/evaluators/revisions/commit", { - "evaluator_revision_commit": { + "evaluator_revision": { "evaluator_id": evaluator["id"], "evaluator_variant_id": variant["id"], "data": {"ag_config": {"prompt": {}}}, @@ -226,7 +226,7 @@ def test_commit_testset_revision_rejects_unknown_data_field(authed_api): authed_api, "/testsets/revisions/commit", { - "testset_revision_commit": { + "testset_revision": { "slug": uuid4().hex[-12:], "testset_id": testset["id"], "testset_variant_id": variant["id"], @@ -260,7 +260,7 @@ def test_commit_query_revision_rejects_unknown_data_field(authed_api): authed_api, "/queries/revisions/commit", { - "query_revision_commit": { + "query_revision": { "slug": uuid4().hex[-12:], "query_id": query["id"], "query_variant_id": query["variant_id"], @@ -306,7 +306,7 @@ def test_commit_environment_revision_rejects_unknown_data_field(authed_api): authed_api, "/environments/revisions/commit", { - "environment_revision_commit": { + "environment_revision": { "slug": uuid4().hex[-12:], "environment_id": environment["id"], "environment_variant_id": variant["id"], diff --git a/api/oss/tests/pytest/acceptance/test_revision_logs.py b/api/oss/tests/pytest/acceptance/test_revision_logs.py new file mode 100644 index 0000000000..0feb60a5de --- /dev/null +++ b/api/oss/tests/pytest/acceptance/test_revision_logs.py @@ -0,0 +1,299 @@ +from uuid import uuid4 + + +def _create_application_variant(authed_api): + slug = f"app-log-{uuid4().hex[:8]}" + response = authed_api( + "POST", + "/applications/", + json={ + "application": { + "slug": slug, + "name": slug, + "flags": { + "is_application": True, + "is_evaluator": False, + "is_snippet": False, + }, + } + }, + ) + assert response.status_code == 200, response.text + application = response.json()["application"] + + response = authed_api( + "POST", + "/applications/variants/", + json={ + "application_variant": { + "slug": f"{slug}-v", + "name": f"{slug}-v", + "flags": { + "is_application": True, + "is_evaluator": False, + "is_snippet": False, + }, + "application_id": application["id"], + } + }, + ) + assert response.status_code == 200, response.text + variant = response.json()["application_variant"] + return application, variant + + +def _create_environment_variant(authed_api): + slug = f"env-log-{uuid4().hex[:8]}" + response = authed_api( + "POST", + "/environments/", + json={"environment": {"slug": slug}}, + ) + assert response.status_code == 200, response.text + environment = response.json()["environment"] + + response = authed_api( + "POST", + "/environments/variants/", + json={ + "environment_variant": { + "slug": f"{slug}-v", + "environment_id": environment["id"], + } + }, + ) + assert response.status_code == 200, response.text + variant = response.json()["environment_variant"] + return environment, variant + + +def _create_evaluator_variant(authed_api): + slug = f"eval-log-{uuid4().hex[:8]}" + response = authed_api( + "POST", + "/evaluators/", + json={"evaluator": {"slug": slug}}, + ) + assert response.status_code == 200, response.text + evaluator = response.json()["evaluator"] + + response = authed_api( + "POST", + "/evaluators/variants/", + json={ + "evaluator_variant": { + "slug": f"{slug}-v", + "evaluator_id": evaluator["id"], + } + }, + ) + assert response.status_code == 200, response.text + variant = response.json()["evaluator_variant"] + return evaluator, variant + + +def _create_query_variant(authed_api): + slug = f"query-log-{uuid4().hex[:8]}" + response = authed_api( + "POST", + "/simple/queries/", + json={ + "query": { + "slug": slug, + "name": slug, + "data": { + "filtering": { + "operator": "and", + "conditions": [ + { + "field": "attributes", + "key": f"k-{slug}", + "value": "v", + "operator": "is", + } + ], + }, + "windowing": {"limit": 10}, + }, + } + }, + ) + assert response.status_code == 200, response.text + query = response.json()["query"] + variant = { + "id": query["variant_id"], + } + return query, variant + + +def _create_testset_variant(authed_api): + slug = f"testset-log-{uuid4().hex[:8]}" + response = authed_api( + "POST", + "/testsets/", + json={"testset": {"slug": slug}}, + ) + assert response.status_code == 200, response.text + testset = response.json()["testset"] + + response = authed_api( + "POST", + "/testsets/variants/", + json={ + "testset_variant": { + "slug": f"{slug}-v", + "testset_id": testset["id"], + } + }, + ) + assert response.status_code == 200, response.text + variant = response.json()["testset_variant"] + return testset, variant + + +class TestRevisionLogs: + def test_applications_revision_log_accepts_current_wrapper(self, authed_api): + application, variant = _create_application_variant(authed_api) + + for version in (1, 2): + response = authed_api( + "POST", + "/applications/revisions/commit", + json={ + "application_revision": { + "application_id": application["id"], + "application_variant_id": variant["id"], + "slug": f"app-log-r{version}-{uuid4().hex[:6]}", + "data": {"parameters": {"version": version}}, + } + }, + ) + assert response.status_code == 200, response.text + + response = authed_api( + "POST", + "/applications/revisions/log", + json={"application_revisions": {"application_variant_id": variant["id"]}}, + ) + + assert response.status_code == 200, response.text + assert response.json()["count"] == 2 + + def test_environments_revision_log_accepts_current_wrapper(self, authed_api): + environment, variant = _create_environment_variant(authed_api) + + for version in (1, 2): + response = authed_api( + "POST", + "/environments/revisions/commit", + json={ + "environment_revision": { + "environment_id": environment["id"], + "environment_variant_id": variant["id"], + "slug": f"env-log-r{version}-{uuid4().hex[:6]}", + "data": {"references": {}, "version": version}, + } + }, + ) + assert response.status_code == 200, response.text + + response = authed_api( + "POST", + "/environments/revisions/log", + json={"environment_revisions": {"environment_variant_id": variant["id"]}}, + ) + + assert response.status_code == 200, response.text + assert response.json()["count"] == 2 + + def test_evaluators_revision_log_accepts_current_wrapper(self, authed_api): + evaluator, variant = _create_evaluator_variant(authed_api) + + for version in (1, 2): + response = authed_api( + "POST", + "/evaluators/revisions/commit", + json={ + "evaluator_revision": { + "evaluator_id": evaluator["id"], + "evaluator_variant_id": variant["id"], + "slug": f"eval-log-r{version}-{uuid4().hex[:6]}", + "data": {"parameters": {"version": version}}, + } + }, + ) + assert response.status_code == 200, response.text + + response = authed_api( + "POST", + "/evaluators/revisions/log", + json={"evaluator_revisions": {"evaluator_variant_id": variant["id"]}}, + ) + + assert response.status_code == 200, response.text + assert response.json()["count"] == 2 + + def test_queries_revision_log_accepts_current_wrapper(self, authed_api): + query, variant = _create_query_variant(authed_api) + + response = authed_api( + "POST", + "/queries/revisions/commit", + json={ + "query_revision": { + "query_id": query["id"], + "query_variant_id": variant["id"], + "slug": f"query-log-r1-{uuid4().hex[:6]}", + "data": { + "filtering": { + "operator": "and", + "conditions": [ + { + "field": "attributes", + "key": f"k2-{uuid4().hex[:6]}", + "value": "v2", + "operator": "is", + } + ], + } + }, + } + }, + ) + assert response.status_code == 200, response.text + + response = authed_api( + "POST", + "/queries/revisions/log", + json={"query_revisions": {"query_variant_id": variant["id"]}}, + ) + + assert response.status_code == 200, response.text + assert response.json()["count"] == 2 + + def test_testsets_revision_log_accepts_current_wrapper(self, authed_api): + testset, variant = _create_testset_variant(authed_api) + + for version in (1, 2): + response = authed_api( + "POST", + "/testsets/revisions/commit", + json={ + "testset_revision": { + "testset_id": testset["id"], + "testset_variant_id": variant["id"], + "slug": f"testset-log-r{version}-{uuid4().hex[:6]}", + "data": {"testcases": []}, + } + }, + ) + assert response.status_code == 200, response.text + + response = authed_api( + "POST", + "/testsets/revisions/log", + json={"testset_revisions": {"testset_variant_id": variant["id"]}}, + ) + + assert response.status_code == 200, response.text + assert response.json()["count"] == 2 diff --git a/api/oss/tests/pytest/acceptance/test_revision_retrieve_ambiguous_400.py b/api/oss/tests/pytest/acceptance/test_revision_retrieve_ambiguous_400.py index 6c931d0586..528e1e62f9 100644 --- a/api/oss/tests/pytest/acceptance/test_revision_retrieve_ambiguous_400.py +++ b/api/oss/tests/pytest/acceptance/test_revision_retrieve_ambiguous_400.py @@ -137,7 +137,7 @@ def _create_application_stack(authed_api): "POST", "/applications/revisions/commit", json={ - "application_revision_commit": { + "application_revision": { "application_id": app["id"], "application_variant_id": variant["id"], "data": {"parameters": {"model": "test-model"}}, @@ -204,7 +204,7 @@ def _create_evaluator_stack(authed_api): "POST", "/evaluators/revisions/commit", json={ - "evaluator_revision_commit": { + "evaluator_revision": { "evaluator_id": evaluator["id"], "evaluator_variant_id": variant["id"], "data": {"parameters": {"model": "test-model"}}, @@ -269,7 +269,7 @@ def _create_testset_stack(authed_api): "POST", "/testsets/revisions/commit", json={ - "testset_revision_commit": { + "testset_revision": { "testset_id": testset["id"], "testset_variant_id": variant["id"], "data": {"testcases": []}, @@ -327,7 +327,7 @@ def _create_query_stack(authed_api): "POST", "/queries/revisions/commit", json={ - "query_revision_commit": { + "query_revision": { "query_id": query["id"], "query_variant_id": query["variant_id"], "data": {"windowing": {"limit": 50}}, @@ -393,7 +393,7 @@ def _create_environment_stack(authed_api): "POST", "/environments/revisions/commit", json={ - "environment_revision_commit": { + "environment_revision": { "environment_id": environment["id"], "environment_variant_id": variant["id"], "data": {"references": {}}, diff --git a/api/oss/tests/pytest/acceptance/test_revision_retrieve_env_path.py b/api/oss/tests/pytest/acceptance/test_revision_retrieve_env_path.py index 790d022024..13edd76021 100644 --- a/api/oss/tests/pytest/acceptance/test_revision_retrieve_env_path.py +++ b/api/oss/tests/pytest/acceptance/test_revision_retrieve_env_path.py @@ -57,7 +57,7 @@ def _create_environment_with_deployment(authed_api, *, key, payload): "POST", "/environments/revisions/commit", json={ - "environment_revision_commit": { + "environment_revision": { "slug": f"envr-{slug}-init", "environment_id": env["id"], "environment_variant_id": env_variant["id"], @@ -72,7 +72,7 @@ def _create_environment_with_deployment(authed_api, *, key, payload): "POST", "/environments/revisions/commit", json={ - "environment_revision_commit": { + "environment_revision": { "slug": f"envr-{slug}", "environment_id": env["id"], "environment_variant_id": env_variant["id"], @@ -241,7 +241,7 @@ def _create_application_stack(authed_api): "POST", "/applications/revisions/commit", json={ - "application_revision_commit": { + "application_revision": { "application_id": app["id"], "application_variant_id": variant["id"], "data": {"parameters": {"model": "test-model"}}, @@ -367,7 +367,7 @@ def _create_evaluator_stack(authed_api): "POST", "/evaluators/revisions/commit", json={ - "evaluator_revision_commit": { + "evaluator_revision": { "evaluator_id": evaluator["id"], "evaluator_variant_id": variant["id"], "data": {"parameters": {"threshold": 0.5}}, diff --git a/api/oss/tests/pytest/acceptance/test_revision_retrieve_inconsistent_400.py b/api/oss/tests/pytest/acceptance/test_revision_retrieve_inconsistent_400.py index fad550d736..0000ea870b 100644 --- a/api/oss/tests/pytest/acceptance/test_revision_retrieve_inconsistent_400.py +++ b/api/oss/tests/pytest/acceptance/test_revision_retrieve_inconsistent_400.py @@ -110,7 +110,7 @@ def _create_application_stack(authed_api): "POST", "/applications/revisions/commit", json={ - "application_revision_commit": { + "application_revision": { "application_id": app["id"], "application_variant_id": variant["id"], "data": {"parameters": {"model": "test-model"}}, @@ -166,7 +166,7 @@ def _create_evaluator_stack(authed_api): "POST", "/evaluators/revisions/commit", json={ - "evaluator_revision_commit": { + "evaluator_revision": { "evaluator_id": evaluator["id"], "evaluator_variant_id": variant["id"], "data": {"parameters": {"model": "test-model"}}, @@ -220,7 +220,7 @@ def _create_testset_stack(authed_api): "POST", "/testsets/revisions/commit", json={ - "testset_revision_commit": { + "testset_revision": { "testset_id": testset["id"], "testset_variant_id": variant["id"], "data": {"testcases": []}, @@ -267,7 +267,7 @@ def _create_query_stack(authed_api): "POST", "/queries/revisions/commit", json={ - "query_revision_commit": { + "query_revision": { "query_id": query["id"], "query_variant_id": query["variant_id"], "data": {"windowing": {"limit": 50}}, @@ -321,7 +321,7 @@ def _create_environment_stack(authed_api): "POST", "/environments/revisions/commit", json={ - "environment_revision_commit": { + "environment_revision": { "environment_id": environment["id"], "environment_variant_id": variant["id"], "data": {"references": {}}, diff --git a/api/oss/tests/pytest/acceptance/test_revision_retrieve_positive.py b/api/oss/tests/pytest/acceptance/test_revision_retrieve_positive.py index 70e1166ce8..736d074c37 100644 --- a/api/oss/tests/pytest/acceptance/test_revision_retrieve_positive.py +++ b/api/oss/tests/pytest/acceptance/test_revision_retrieve_positive.py @@ -254,7 +254,7 @@ def _create_application_stack(authed_api): "POST", "/applications/revisions/commit", json={ - "application_revision_commit": { + "application_revision": { "application_id": app["id"], "application_variant_id": variant["id"], "data": {"parameters": {"model": "test-model"}}, @@ -268,7 +268,7 @@ def _create_application_stack(authed_api): "POST", "/applications/revisions/commit", json={ - "application_revision_commit": { + "application_revision": { "application_id": app["id"], "application_variant_id": variant["id"], "data": {"parameters": {"model": "test-model-v2"}}, @@ -298,7 +298,7 @@ def _create_application_stack(authed_api): "POST", "/applications/revisions/commit", json={ - "application_revision_commit": { + "application_revision": { "application_id": app["id"], "application_variant_id": second_variant["id"], "data": {"parameters": {"model": "test-model-alt"}}, @@ -446,7 +446,7 @@ def _create_evaluator_stack(authed_api): "POST", "/evaluators/revisions/commit", json={ - "evaluator_revision_commit": { + "evaluator_revision": { "evaluator_id": evaluator["id"], "evaluator_variant_id": variant["id"], "data": {"parameters": {"model": "test-model"}}, @@ -460,7 +460,7 @@ def _create_evaluator_stack(authed_api): "POST", "/evaluators/revisions/commit", json={ - "evaluator_revision_commit": { + "evaluator_revision": { "evaluator_id": evaluator["id"], "evaluator_variant_id": variant["id"], "data": {"parameters": {"model": "test-model-v2"}}, @@ -490,7 +490,7 @@ def _create_evaluator_stack(authed_api): "POST", "/evaluators/revisions/commit", json={ - "evaluator_revision_commit": { + "evaluator_revision": { "evaluator_id": evaluator["id"], "evaluator_variant_id": second_variant["id"], "data": {"parameters": {"model": "test-model-alt"}}, @@ -642,7 +642,7 @@ def _create_testset_stack(authed_api): "POST", "/testsets/revisions/commit", json={ - "testset_revision_commit": { + "testset_revision": { "testset_id": testset["id"], "testset_variant_id": variant["id"], "data": {"testcases": []}, @@ -656,7 +656,7 @@ def _create_testset_stack(authed_api): "POST", "/testsets/revisions/commit", json={ - "testset_revision_commit": { + "testset_revision": { "testset_id": testset["id"], "testset_variant_id": variant["id"], "data": {"testcases": [{"inputs": {"q": "v2"}}]}, @@ -685,7 +685,7 @@ def _create_testset_stack(authed_api): "POST", "/testsets/revisions/commit", json={ - "testset_revision_commit": { + "testset_revision": { "testset_id": testset["id"], "testset_variant_id": second_variant["id"], "data": {"testcases": [{"inputs": {"q": "alt"}}]}, @@ -822,7 +822,7 @@ def _create_query_stack(authed_api): "POST", "/queries/revisions/commit", json={ - "query_revision_commit": { + "query_revision": { "query_id": query["id"], "query_variant_id": query["variant_id"], "data": {"windowing": {"limit": 50}}, @@ -836,7 +836,7 @@ def _create_query_stack(authed_api): "POST", "/queries/revisions/commit", json={ - "query_revision_commit": { + "query_revision": { "query_id": query["id"], "query_variant_id": query["variant_id"], "data": {"windowing": {"limit": 100}}, @@ -865,7 +865,7 @@ def _create_query_stack(authed_api): "POST", "/queries/revisions/commit", json={ - "query_revision_commit": { + "query_revision": { "query_id": query["id"], "query_variant_id": second_variant["id"], "data": {"windowing": {"limit": 25}}, @@ -1005,7 +1005,7 @@ def _create_environment_stack(authed_api): "POST", "/environments/revisions/commit", json={ - "environment_revision_commit": { + "environment_revision": { "environment_id": environment["id"], "environment_variant_id": variant["id"], "data": {"references": {}}, @@ -1019,7 +1019,7 @@ def _create_environment_stack(authed_api): "POST", "/environments/revisions/commit", json={ - "environment_revision_commit": { + "environment_revision": { "environment_id": environment["id"], "environment_variant_id": variant["id"], "data": {"references": {"note": {}}}, @@ -1048,7 +1048,7 @@ def _create_environment_stack(authed_api): "POST", "/environments/revisions/commit", json={ - "environment_revision_commit": { + "environment_revision": { "environment_id": environment["id"], "environment_variant_id": second_variant["id"], "data": {"references": {"alt": {}}}, diff --git a/api/oss/tests/pytest/acceptance/testsets/test_testset_retrieval_info.py b/api/oss/tests/pytest/acceptance/testsets/test_testset_retrieval_info.py index 823b21a37e..40fd108d1b 100644 --- a/api/oss/tests/pytest/acceptance/testsets/test_testset_retrieval_info.py +++ b/api/oss/tests/pytest/acceptance/testsets/test_testset_retrieval_info.py @@ -27,7 +27,7 @@ def testset_fixture(authed_api): "POST", "/testsets/revisions/commit", json={ - "testset_revision_commit": { + "testset_revision": { "slug": f"{slug}-v0", "testset_id": tid, "testset_variant_id": vid, @@ -42,7 +42,7 @@ def testset_fixture(authed_api): "POST", "/testsets/revisions/commit", json={ - "testset_revision_commit": { + "testset_revision": { "slug": f"{slug}-v1", "testset_id": tid, "testset_variant_id": vid, diff --git a/api/oss/tests/pytest/acceptance/tracing/test_traces_preview.py b/api/oss/tests/pytest/acceptance/tracing/test_traces_preview.py index 90dc969c8d..1c6a41c002 100644 --- a/api/oss/tests/pytest/acceptance/tracing/test_traces_preview.py +++ b/api/oss/tests/pytest/acceptance/tracing/test_traces_preview.py @@ -529,7 +529,7 @@ def test_query_traces_by_query_revision_ref(self, authed_api): "POST", "/queries/revisions/commit", json={ - "query_revision_commit": { + "query_revision": { "slug": uuid4().hex[-12:], "query_id": query["id"], "query_variant_id": query["variant_id"], @@ -589,7 +589,7 @@ def test_query_traces_span_focus_conflict_returns_409(self, authed_api): "POST", "/queries/revisions/commit", json={ - "query_revision_commit": { + "query_revision": { "slug": uuid4().hex[-12:], "query_id": query["id"], "query_variant_id": query["variant_id"], diff --git a/api/oss/tests/pytest/acceptance/workflows/test_workflow_embeds_cross_entity.py b/api/oss/tests/pytest/acceptance/workflows/test_workflow_embeds_cross_entity.py index 161d07c055..baae0936bf 100644 --- a/api/oss/tests/pytest/acceptance/workflows/test_workflow_embeds_cross_entity.py +++ b/api/oss/tests/pytest/acceptance/workflows/test_workflow_embeds_cross_entity.py @@ -102,7 +102,7 @@ def test_workflow_embeds_environment(self, authed_api): "POST", "/environments/revisions/commit", json={ - "environment_revision_commit": { + "environment_revision": { "slug": uuid4().hex[-12:], "environment_id": env_id, "environment_variant_id": env_variant_id, @@ -117,7 +117,7 @@ def test_workflow_embeds_environment(self, authed_api): "POST", "/environments/revisions/commit", json={ - "environment_revision_commit": { + "environment_revision": { "slug": f"{env_slug}-v1", "environment_id": env_id, "environment_variant_id": env_variant_id, @@ -266,7 +266,7 @@ def test_workflow_embeds_environment_header(self, authed_api): "POST", "/environments/revisions/commit", json={ - "environment_revision_commit": { + "environment_revision": { "slug": uuid4().hex[-12:], "environment_id": env_id, "environment_variant_id": env_variant_id, @@ -281,7 +281,7 @@ def test_workflow_embeds_environment_header(self, authed_api): "POST", "/environments/revisions/commit", json={ - "environment_revision_commit": { + "environment_revision": { "slug": f"{env_slug}-v1", "environment_id": env_id, "environment_variant_id": env_variant_id, @@ -485,7 +485,7 @@ def test_environment_embeds_workflow(self, authed_api): "POST", "/environments/revisions/commit", json={ - "environment_revision_commit": { + "environment_revision": { "slug": uuid4().hex[-12:], "environment_id": env_id, "environment_variant_id": env_variant_id, @@ -500,7 +500,7 @@ def test_environment_embeds_workflow(self, authed_api): "POST", "/environments/revisions/commit", json={ - "environment_revision_commit": { + "environment_revision": { "slug": f"{env_slug}-v1", "environment_id": env_id, "environment_variant_id": env_variant_id, @@ -647,7 +647,7 @@ def test_workflow_environment_workflow_chain(self, authed_api): "POST", "/environments/revisions/commit", json={ - "environment_revision_commit": { + "environment_revision": { "slug": uuid4().hex[-12:], "environment_id": env_id, "environment_variant_id": env_variant_id, @@ -662,7 +662,7 @@ def test_workflow_environment_workflow_chain(self, authed_api): "POST", "/environments/revisions/commit", json={ - "environment_revision_commit": { + "environment_revision": { "slug": f"{env_slug}-v1", "environment_id": env_id, "environment_variant_id": env_variant_id, diff --git a/api/oss/tests/pytest/acceptance/workflows/test_workflow_lineage.py b/api/oss/tests/pytest/acceptance/workflows/test_workflow_lineage.py index a2ff722d48..3c378c870b 100644 --- a/api/oss/tests/pytest/acceptance/workflows/test_workflow_lineage.py +++ b/api/oss/tests/pytest/acceptance/workflows/test_workflow_lineage.py @@ -213,7 +213,7 @@ def test_log_all_workflow_revisions_by_variant(self, authed_api, mock_data): "POST", "/workflows/revisions/log", json={ - "workflow": { + "workflow_revisions": { "workflow_variant_id": workflow_variant["id"], }, }, @@ -234,7 +234,7 @@ def test_log_last_workflow_revisions_by_variant(self, authed_api, mock_data): "POST", "/workflows/revisions/log", json={ - "workflow": { + "workflow_revisions": { "workflow_variant_id": workflow_variant["id"], "depth": 2, }, @@ -258,7 +258,7 @@ def test_log_all_workflow_revisions(self, authed_api, mock_data): "POST", "/workflows/revisions/log", json={ - "workflow": { + "workflow_revisions": { "workflow_revision_id": workflow_revision["id"], }, }, @@ -280,7 +280,7 @@ def test_log_last_workflow_revisions(self, authed_api, mock_data): "POST", "/workflows/revisions/log", json={ - "workflow": { + "workflow_revisions": { "workflow_revision_id": workflow_revision["id"], "depth": 2, }, @@ -364,7 +364,7 @@ def test_full_fork_workflow_variant(self, authed_api, mock_data): "POST", "/workflows/revisions/log", json={ - "workflow": { + "workflow_revisions": { "workflow_variant_id": workflow_variant["id"], }, }, @@ -445,7 +445,7 @@ def test_shallow_fork_workflow_variant(self, authed_api, mock_data): "POST", "/workflows/revisions/log", json={ - "workflow": { + "workflow_revisions": { "workflow_variant_id": workflow_variant["id"], }, }, @@ -490,7 +490,7 @@ def test_fork_workflow_variant_without_revision(self, authed_api, mock_data): "POST", "/workflows/revisions/log", json={ - "workflow": { + "workflow_revisions": { "workflow_variant_id": workflow_variant["id"], }, }, diff --git a/api/oss/tests/pytest/acceptance/workflows/test_workflow_retrieval_info.py b/api/oss/tests/pytest/acceptance/workflows/test_workflow_retrieval_info.py index a0ec0f7707..1f114bff6f 100644 --- a/api/oss/tests/pytest/acceptance/workflows/test_workflow_retrieval_info.py +++ b/api/oss/tests/pytest/acceptance/workflows/test_workflow_retrieval_info.py @@ -196,7 +196,7 @@ def env_backed_fixture(authed_api): "POST", "/environments/revisions/commit", json={ - "environment_revision_commit": { + "environment_revision": { "slug": f"{env_slug}-r0", "environment_id": env_id, "environment_variant_id": env_variant_id, @@ -212,7 +212,7 @@ def env_backed_fixture(authed_api): "POST", "/environments/revisions/commit", json={ - "environment_revision_commit": { + "environment_revision": { "slug": f"{env_slug}-r1", "environment_id": env_id, "environment_variant_id": env_variant_id, diff --git a/api/oss/tests/pytest/unit/environments/test_commit_validation.py b/api/oss/tests/pytest/unit/environments/test_commit_validation.py index b648bf1bed..ff81270250 100644 --- a/api/oss/tests/pytest/unit/environments/test_commit_validation.py +++ b/api/oss/tests/pytest/unit/environments/test_commit_validation.py @@ -48,7 +48,7 @@ async def test_commit_environment_revision_rejects_missing_data_and_delta(monkey await router.commit_environment_revision( _DummyRequest(), environment_revision_commit_request=EnvironmentRevisionCommitRequest( - environment_revision_commit=EnvironmentRevisionCommit( + environment_revision=EnvironmentRevisionCommit( slug="env-slug", environment_id=uuid4(), ) @@ -78,7 +78,7 @@ async def test_commit_environment_revision_rejects_data_and_delta_together(monke await router.commit_environment_revision( _DummyRequest(), environment_revision_commit_request=EnvironmentRevisionCommitRequest( - environment_revision_commit=EnvironmentRevisionCommit( + environment_revision=EnvironmentRevisionCommit( slug="env-slug", environment_id=uuid4(), data=EnvironmentRevisionData(), @@ -111,7 +111,7 @@ async def test_commit_environment_revision_accepts_delta_only(monkeypatch): response = await router.commit_environment_revision( _DummyRequest(), environment_revision_commit_request=EnvironmentRevisionCommitRequest( - environment_revision_commit=EnvironmentRevisionCommit( + environment_revision=EnvironmentRevisionCommit( slug="env-slug", environment_id=uuid4(), delta=EnvironmentRevisionDelta(), diff --git a/docs/designs/git-entities/request-body-fields.md b/docs/designs/git-entities/request-body-fields.md new file mode 100644 index 0000000000..af2224523a --- /dev/null +++ b/docs/designs/git-entities/request-body-fields.md @@ -0,0 +1,517 @@ +# Request-body fields — git-backed entities + +Inventory of **every non-meta field** on every request body, for all +git-backed entities (`workflows`, `applications`, `evaluators`, `queries`, +`testsets`, `environments`), all levels (artifact / variant / revision), +and both routers (`//*` and `/simple//*`). + +Extracted programmatically from +`api/oss/src/apis/fastapi//models.py` on branch +`refactor/unify-revision-request-fields` (PR #4470 stacked on PR #4469). +Meta fields (`project_id`, `user_id`, `windowing`, `include_archived`, +`include_*`) are filtered out. + +--- + +## Generic shape + +`` ∈ `{workflow, application, evaluator, query, testset, environment}`. +`` is the PascalCase form. + +### Simple/ router + +| Endpoint | Fields | +|---|---| +| `POST /simple//` | `: SimpleCreate` | +| `PUT /simple//{id}` | `: SimpleEdit` | +| `POST /simple//query` | `: SimpleQuery` + `_refs: List[Reference]` | + +### Artifact level + +| Endpoint | Fields | +|---|---| +| `POST //` | `: Create` | +| `PUT //{id}` | `: Edit` | +| `POST //query` | `: Query` + `_refs: List[Reference]` | +| `POST //{id}/fork` *(all six entities)* | `: Fork` | + +### Variant level + +| Endpoint | Fields | +|---|---| +| `POST //variants/` | `_variant: VariantCreate` | +| `PUT //variants/{id}` | `_variant: VariantEdit` | +| `POST //variants/query` | `_variant: VariantQuery` + `_refs: List[Reference]` + `_variant_refs: List[Reference]` | +| `POST //variants/fork` *(all six entities)* | `_variant: VariantFork` + `_variant_ref: Reference` + `_revision_ref: Reference` | + +### Revision level + +| Endpoint | Fields | +|---|---| +| `POST //revisions/` | `_revision: RevisionCreate` | +| `PUT //revisions/{id}` | `_revision: RevisionEdit` | +| `POST //revisions/query` | `_revision: RevisionQuery` + `_refs: List[Reference]` + `_variant_refs: List[Reference]` + `_revision_refs: List[Reference]` | +| `POST //revisions/commit` | `_revision: RevisionCommit` | +| `POST //revisions/log` | `_revisions: RevisionsLog` | +| `POST //revisions/retrieve` | `_ref: Reference` + `_variant_ref: Reference` + `_revision_ref: Reference` + `environment_ref: Reference` + `environment_variant_ref: Reference` + `environment_revision_ref: Reference` + `key: str` + `resolve: bool` *(queries, testsets, environments drop the `environment_*` triple and `key`)* | +| `POST //revisions/resolve` *(workflows, applications, evaluators, environments)* | `_ref: Reference` + `_variant_ref: Reference` + `_revision_ref: Reference` + `_revision: Revision` + `max_depth: int` + `max_embeds: int` + `error_policy: ErrorPolicy` | +| `POST //revisions/deploy` *(workflows, applications, evaluators)* | `_ref: Reference` + `_variant_ref: Reference` + `_revision_ref: Reference` + `environment_ref: Reference` + `environment_variant_ref: Reference` + `environment_revision_ref: Reference` + `key: str` + `message: str` | + +--- + +## Naming rules + +- **Create / Edit / Fork / Query (artifact, variant, revision)**: wrapper is + the singular noun (`workflow`, `workflow_variant`, `workflow_revision`). + The verb suffix is dropped on the field but kept in the type name. +- **Commit**: `_revision: RevisionCommit` +- **Log**: `_revisions: RevisionsLog` (plural `revisions`) +- **Retrieve / Resolve / Deploy**: no typed wrapper — flat `_ref` / + `_variant_ref` / `_revision_ref` + extras. +- **Query bodies**: always carry `_refs: List[Reference]`; deeper + levels also carry `_variant_refs` and `_revision_refs`. +- **Variant Fork**: `_variant: VariantFork` (new variant + config: slug, name, description, flags — same noun-only convention as + Create/Edit/Query), `_variant_ref: Reference` (source variant to + copy from), `_revision_ref: Optional[Reference]` (pin to a + specific revision; defaults to source head). + +--- + +## Inline-resolve mode + +`/revisions/resolve` supports two modes for all four entities that have it +(`workflows`, `applications`, `evaluators`, `environments`): + +| Mode | How to trigger | What happens | +| --- | --- | --- | +| **Reference mode** | Pass `_ref` / `_variant_ref` / `_revision_ref` | Server retrieves the revision from DB, then resolves its `@ag.references` embeds. | +| **Inline mode** | Pass `_revision: Revision` (a full revision object) | Server resolves the embed tokens in the supplied revision without touching the DB. The caller must already hold the revision data. | + +**Who uses inline mode?** The SDK resolver +(`sdks/python/agenta/sdk/middlewares/running/resolver.py`) calls inline +mode on `/workflows/revisions/resolve` when it has already fetched the +revision and needs to expand `@ag.references` tokens without a second +round-trip. The same field is available on the other three entities +(`applications`, `evaluators`, `environments`) but is not yet called from +the SDK. + +**Why not queries/testsets?** Those entities do not support `@ag.references` +embed tokens, so they have no Resolve endpoint at all. + +--- + +## Cross-entity filter on EnvironmentRevisionQuery + +`EnvironmentRevisionQueryRequest` carries `references: List[Reference]` +in addition to the standard `environment_refs` / `environment_variant_refs` / +`environment_revision_refs`. This lets callers scope environment revisions by +the entities they reference — useful when querying which environment +revisions point at a given application variant. + +**Why only environments?** Environment revisions carry a `data.references` +map that links environments to application (and workflow/evaluator) variants. +Filtering environment revisions by the application they point at is a natural +join; the inverse (filtering application revisions by which environment +deploys them) is not currently implemented but could follow the same pattern. + +--- + +## Per-entity full class listings + +### workflows + +``` +SimpleWorkflowCreateRequest: + workflow: SimpleWorkflowCreate +SimpleWorkflowEditRequest: + workflow: SimpleWorkflowEdit +SimpleWorkflowQueryRequest: + workflow: SimpleWorkflowQuery + workflow_refs: List[Reference] + +WorkflowCreateRequest: + workflow: WorkflowCreate +WorkflowEditRequest: + workflow: WorkflowEdit +WorkflowForkRequest: + workflow: WorkflowFork +WorkflowQueryRequest: + workflow: WorkflowQuery + workflow_refs: List[Reference] + +WorkflowVariantCreateRequest: + workflow_variant: WorkflowVariantCreate +WorkflowVariantEditRequest: + workflow_variant: WorkflowVariantEdit +WorkflowVariantQueryRequest: + workflow_variant: WorkflowVariantQuery + workflow_refs: List[Reference] + workflow_variant_refs: List[Reference] +WorkflowVariantForkRequest: + workflow_variant: WorkflowVariantFork + workflow_variant_ref: Reference + workflow_revision_ref: Optional[Reference] + +WorkflowRevisionCreateRequest: + workflow_revision: WorkflowRevisionCreate +WorkflowRevisionEditRequest: + workflow_revision: WorkflowRevisionEdit +WorkflowRevisionQueryRequest: + workflow_revision: WorkflowRevisionQuery + workflow_refs: List[Reference] + workflow_variant_refs: List[Reference] + workflow_revision_refs: List[Reference] +WorkflowRevisionCommitRequest: + workflow_revision: WorkflowRevisionCommit +WorkflowRevisionsLogRequest: + workflow_revisions: WorkflowRevisionsLog +WorkflowRevisionRetrieveRequest: + workflow_ref: Reference + workflow_variant_ref: Reference + workflow_revision_ref: Reference + environment_ref: Reference + environment_variant_ref: Reference + environment_revision_ref: Reference + key: str + resolve: bool +WorkflowRevisionResolveRequest: + workflow_ref: Reference + workflow_variant_ref: Reference + workflow_revision_ref: Reference + workflow_revision: WorkflowRevision # inline-resolve mode + max_depth: int + max_embeds: int + error_policy: ErrorPolicy +WorkflowRevisionDeployRequest: + workflow_ref: Reference + workflow_variant_ref: Reference + workflow_revision_ref: Reference + environment_ref: Reference + environment_variant_ref: Reference + environment_revision_ref: Reference + key: str + message: str +``` + +### applications + +``` +SimpleApplicationCreateRequest: + application: SimpleApplicationCreate +SimpleApplicationEditRequest: + application: SimpleApplicationEdit +SimpleApplicationQueryRequest: + application: SimpleApplicationQuery + application_refs: List[Reference] + +ApplicationCreateRequest: + application: ApplicationCreate +ApplicationEditRequest: + application: ApplicationEdit +ApplicationForkRequest: + application: ApplicationFork +ApplicationQueryRequest: + application: ApplicationQuery + application_refs: List[Reference] + +ApplicationVariantCreateRequest: + application_variant: ApplicationVariantCreate +ApplicationVariantEditRequest: + application_variant: ApplicationVariantEdit +ApplicationVariantQueryRequest: + application_variant: ApplicationVariantQuery + application_refs: List[Reference] + application_variant_refs: List[Reference] +ApplicationVariantForkRequest: + application_variant: ApplicationVariantFork + application_variant_ref: Reference + application_revision_ref: Optional[Reference] + +ApplicationRevisionCreateRequest: + application_revision: ApplicationRevisionCreate +ApplicationRevisionEditRequest: + application_revision: ApplicationRevisionEdit +ApplicationRevisionQueryRequest: + application_revision: ApplicationRevisionQuery + application_refs: List[Reference] + application_variant_refs: List[Reference] + application_revision_refs: List[Reference] +ApplicationRevisionCommitRequest: + application_revision: ApplicationRevisionCommit +ApplicationRevisionsLogRequest: + application_revisions: ApplicationRevisionsLog +ApplicationRevisionRetrieveRequest: + application_ref: Reference + application_variant_ref: Reference + application_revision_ref: Reference + environment_ref: Reference + environment_variant_ref: Reference + environment_revision_ref: Reference + key: str + resolve: bool +ApplicationRevisionResolveRequest: + application_ref: Reference + application_variant_ref: Reference + application_revision_ref: Reference + application_revision: ApplicationRevision # inline-resolve mode + max_depth: int + max_embeds: int + error_policy: ErrorPolicy +ApplicationRevisionDeployRequest: + application_ref: Reference + application_variant_ref: Reference + application_revision_ref: Reference + environment_ref: Reference + environment_variant_ref: Reference + environment_revision_ref: Reference + key: str + message: str +``` + +### evaluators + +``` +SimpleEvaluatorCreateRequest: + evaluator: SimpleEvaluatorCreate +SimpleEvaluatorEditRequest: + evaluator: SimpleEvaluatorEdit +SimpleEvaluatorQueryRequest: + evaluator: SimpleEvaluatorQuery + evaluator_refs: List[Reference] + +EvaluatorCreateRequest: + evaluator: EvaluatorCreate +EvaluatorEditRequest: + evaluator: EvaluatorEdit +EvaluatorForkRequest: + evaluator: EvaluatorFork +EvaluatorQueryRequest: + evaluator: EvaluatorQuery + evaluator_refs: List[Reference] + +EvaluatorVariantCreateRequest: + evaluator_variant: EvaluatorVariantCreate +EvaluatorVariantEditRequest: + evaluator_variant: EvaluatorVariantEdit +EvaluatorVariantQueryRequest: + evaluator_variant: EvaluatorVariantQuery + evaluator_refs: List[Reference] + evaluator_variant_refs: List[Reference] +EvaluatorVariantForkRequest: + evaluator_variant: EvaluatorVariantFork + evaluator_variant_ref: Reference + evaluator_revision_ref: Optional[Reference] + +EvaluatorRevisionCreateRequest: + evaluator_revision: EvaluatorRevisionCreate +EvaluatorRevisionEditRequest: + evaluator_revision: EvaluatorRevisionEdit +EvaluatorRevisionQueryRequest: + evaluator_revision: EvaluatorRevisionQuery + evaluator_refs: List[Reference] + evaluator_variant_refs: List[Reference] + evaluator_revision_refs: List[Reference] +EvaluatorRevisionCommitRequest: + evaluator_revision: EvaluatorRevisionCommit +EvaluatorRevisionsLogRequest: + evaluator_revisions: EvaluatorRevisionsLog +EvaluatorRevisionRetrieveRequest: + evaluator_ref: Reference + evaluator_variant_ref: Reference + evaluator_revision_ref: Reference + environment_ref: Reference + environment_variant_ref: Reference + environment_revision_ref: Reference + key: str + resolve: bool +EvaluatorRevisionResolveRequest: + evaluator_ref: Reference + evaluator_variant_ref: Reference + evaluator_revision_ref: Reference + evaluator_revision: EvaluatorRevision # inline-resolve mode + max_depth: int + max_embeds: int + error_policy: ErrorPolicy +EvaluatorRevisionDeployRequest: + evaluator_ref: Reference + evaluator_variant_ref: Reference + evaluator_revision_ref: Reference + environment_ref: Reference + environment_variant_ref: Reference + environment_revision_ref: Reference + key: str + message: str +``` + +### queries + +``` +SimpleQueryCreateRequest: + query: SimpleQueryCreate +SimpleQueryEditRequest: + query: SimpleQueryEdit +SimpleQueryQueryRequest: + query: SimpleQueryQuery + query_refs: List[Reference] + +QueryCreateRequest: + query: QueryCreate +QueryEditRequest: + query: QueryEdit +QueryForkRequest: + query: QueryFork +QueryQueryRequest: + query: QueryQuery + query_refs: List[Reference] + +QueryVariantCreateRequest: + query_variant: QueryVariantCreate +QueryVariantEditRequest: + query_variant: QueryVariantEdit +QueryVariantQueryRequest: + query_variant: QueryVariantQuery + query_refs: List[Reference] + query_variant_refs: List[Reference] +QueryVariantForkRequest: + query_variant: QueryVariantFork + query_variant_ref: Reference + query_revision_ref: Optional[Reference] + +QueryRevisionCreateRequest: + query_revision: QueryRevisionCreate +QueryRevisionEditRequest: + query_revision: QueryRevisionEdit +QueryRevisionQueryRequest: + query_revision: QueryRevisionQuery + query_refs: List[Reference] + query_variant_refs: List[Reference] + query_revision_refs: List[Reference] +QueryRevisionCommitRequest: + query_revision: QueryRevisionCommit +QueryRevisionsLogRequest: + query_revisions: QueryRevisionsLog +QueryRevisionRetrieveRequest: + query_ref: Reference + query_variant_ref: Reference + query_revision_ref: Reference + # no environment_* triple, no key +``` + +### testsets + +``` +SimpleTestsetCreateRequest: + testset: SimpleTestsetCreate +SimpleTestsetEditRequest: + testset: SimpleTestsetEdit +SimpleTestsetQueryRequest: + testset: SimpleTestsetQuery + testset_refs: List[Reference] + +TestsetCreateRequest: + testset: TestsetCreate +TestsetEditRequest: + testset: TestsetEdit +TestsetForkRequest: + testset: TestsetFork +TestsetQueryRequest: + testset: TestsetQuery + testset_refs: List[Reference] + +TestsetVariantCreateRequest: + testset_variant: TestsetVariantCreate +TestsetVariantEditRequest: + testset_variant: TestsetVariantEdit +TestsetVariantQueryRequest: + testset_variant: TestsetVariantQuery + testset_refs: List[Reference] + testset_variant_refs: List[Reference] +TestsetVariantForkRequest: + testset_variant: TestsetVariantFork + testset_variant_ref: Reference + testset_revision_ref: Optional[Reference] + +TestsetRevisionCreateRequest: + testset_revision: TestsetRevisionCreate +TestsetRevisionEditRequest: + testset_revision: TestsetRevisionEdit +TestsetRevisionQueryRequest: + testset_revision: TestsetRevisionQuery + testset_refs: List[Reference] + testset_variant_refs: List[Reference] + testset_revision_refs: List[Reference] +TestsetRevisionCommitRequest: + testset_revision: TestsetRevisionCommit +TestsetRevisionsLogRequest: + testset_revisions: TestsetRevisionsLog +TestsetRevisionRetrieveRequest: + testset_ref: Reference + testset_variant_ref: Reference + testset_revision_ref: Reference + # no environment_* triple, no key +``` + +### environments + +``` +SimpleEnvironmentCreateRequest: + environment: SimpleEnvironmentCreate +SimpleEnvironmentEditRequest: + environment: SimpleEnvironmentEdit +SimpleEnvironmentQueryRequest: + environment: SimpleEnvironmentQuery + environment_refs: List[Reference] + +EnvironmentCreateRequest: + environment: EnvironmentCreate +EnvironmentEditRequest: + environment: EnvironmentEdit +EnvironmentForkRequest: + environment: EnvironmentFork +EnvironmentQueryRequest: + environment: EnvironmentQuery + environment_refs: List[Reference] + +EnvironmentVariantCreateRequest: + environment_variant: EnvironmentVariantCreate +EnvironmentVariantEditRequest: + environment_variant: EnvironmentVariantEdit +EnvironmentVariantQueryRequest: + environment_variant: EnvironmentVariantQuery + environment_refs: List[Reference] + environment_variant_refs: List[Reference] +EnvironmentVariantForkRequest: + environment_variant: EnvironmentVariantFork + environment_variant_ref: Reference + environment_revision_ref: Optional[Reference] + +EnvironmentRevisionCreateRequest: + environment_revision: EnvironmentRevisionCreate +EnvironmentRevisionEditRequest: + environment_revision: EnvironmentRevisionEdit +EnvironmentRevisionQueryRequest: + environment_revision: EnvironmentRevisionQuery + environment_refs: List[Reference] + environment_variant_refs: List[Reference] + environment_revision_refs: List[Reference] + references: List[Reference] # cross-entity filter: scope env revisions by any referenced entity +EnvironmentRevisionCommitRequest: + environment_revision: EnvironmentRevisionCommit +EnvironmentRevisionsLogRequest: + environment_revisions: EnvironmentRevisionsLog +EnvironmentRevisionRetrieveRequest: + environment_ref: Reference + environment_variant_ref: Reference + environment_revision_ref: Reference + resolve: bool + # no key (environments are the pointer, not the target of a key lookup) +EnvironmentRevisionResolveRequest: + environment_ref: Reference + environment_variant_ref: Reference + environment_revision_ref: Reference + environment_revision: EnvironmentRevision # inline-resolve mode + max_depth: int + max_embeds: int + error_policy: ErrorPolicy +``` diff --git a/docs/designs/git-entities/tasks.md b/docs/designs/git-entities/tasks.md new file mode 100644 index 0000000000..5006e4435e --- /dev/null +++ b/docs/designs/git-entities/tasks.md @@ -0,0 +1,58 @@ +# Tasks — git-backed entities + +## Open + +- [x] **Unified variant fork request shape**: all six entities now have `VariantForkRequest(ForkRequest)` — a named subclass of the artifact-level request with the same single `: Fork` field. Old flat `EvaluatorVariantForkRequest` (source_/target_ shape) and `QueryVariantForkRequest` replaced. All six `/variants/fork` handlers updated to use `*VariantForkRequest`. + +- [ ] **Fork parity — artifact level**: add `POST //{id}/fork` to `queries`, `testsets`, and `environments` so all six git-backed entities have artifact-level fork. Requires `QueryFork` / `TestsetFork` / `EnvironmentFork` DTOs + `QueryForkRequest` / `TestsetForkRequest` / `EnvironmentForkRequest` models + router handlers + service methods. + +- [ ] **Fork parity — variant level**: add `POST //variants/fork` to all six git-backed entities. Currently only evaluators and queries have it; workflows, applications, testsets, and environments are missing it. + + **What exists already** (can be used as-is or as the pattern): + - `WorkflowVariantFork(VariantFork)` + `WorkflowVariantForkAlias` in `core/workflows/dtos.py` + - `ApplicationVariantFork(WorkflowVariantFork)` + `ApplicationVariantForkAlias` in `core/applications/dtos.py` + - `QueryVariantFork(VariantFork)` + `QueryVariantForkAlias` in `core/queries/dtos.py` + + **What needs to be created**: + - `EvaluatorVariantFork(VariantFork)` + `EvaluatorVariantForkAlias` in `core/evaluators/dtos.py` — the existing `EvaluatorVariantForkRequest` is marked `# TODO: FIX ME` and uses a flat `source_` / `target_` shape instead of a typed DTO. + - `TestsetVariantFork(VariantFork)` + `TestsetVariantForkAlias` in `core/testsets/dtos.py` + - `EnvironmentVariantFork(VariantFork)` + `EnvironmentVariantForkAlias` in `core/environments/dtos.py` + + **Request model shape** — replace the current ad-hoc `source__variant_ref` + `target__ref` flat fields with a typed wrapper, matching the artifact-level Fork pattern: + ``` + VariantForkRequest: + _variant_fork: VariantFork # typed DTO carrying slug, name, description, flags + _variant_ref: Reference # variant to fork from + _ref: Reference # artifact that will receive the new variant + ``` + + **Remaining work per entity**: + - `workflows`: DTO exists; add `WorkflowVariantForkRequest` model + router handler + service method. + - `applications`: DTO exists; add `ApplicationVariantForkRequest` model + router handler + service method. + - `evaluators`: replace `EvaluatorVariantForkRequest` (remove `# TODO: FIX ME`); add `EvaluatorVariantFork` DTO + router handler + service method. + - `queries`: DTO exists; replace `QueryVariantForkRequest` flat shape with typed wrapper; add router handler + service method. + - `testsets`: create `TestsetVariantFork` DTO + `TestsetVariantForkRequest` model + router handler + service method. + - `environments`: create `EnvironmentVariantFork` DTO + `EnvironmentVariantForkRequest` model + router handler + service method. + +- [x] **Dropped `resolve: bool` from all Revision-level Query endpoints** (applications, evaluators, environments models + routers). Partial-resolution failures on a list of revisions are undetectable and silently corrupt results — `resolution_info.errors` only works on single-revision `/retrieve`. Use `/revisions/retrieve` with `resolve: true` for per-revision observable resolution. + +- [x] **Added inline-resolve mode to all entities with `/revisions/resolve`**: `application_revision`, `evaluator_revision`, `environment_revision` fields added to the respective `*RevisionResolveRequest` models, services, and routers — matching the existing `workflow_revision` pattern. Services check `embeds_service` once at the top (covers both branches). Only current SDK caller is via `/workflows/revisions/resolve`; other entities' inline mode is now available. + +- [x] **Renamed `application_refs` → `references` on `EnvironmentRevisionQueryRequest`** (model + router + web). Service/DAO internal name stays `application_refs`; only the request body field is renamed. Web caller in `web/packages/agenta-entities/src/environment/api/api.ts` updated. + +- [ ] **Create / Edit / Query wrappers drop the verb**: `WorkflowCreateRequest` → `workflow` (not `workflow_create`). Large blast radius to fix uniformly — all clients and docs would need updating. Document as deliberate exception or schedule unification? + +## Done + +- [x] **Fork parity — artifact level**: `QueryForkRequest`, `TestsetForkRequest`, `EnvironmentForkRequest` + DTOs (`TestsetFork`, `EnvironmentFork`) + service methods + router endpoints added. All six entities now have `POST /variants/fork`. +- [x] **Fork parity — variant level (queries)**: `QueryForkRequest` wired to existing `fork_query_variant` service + router endpoint added. +- [x] **Fork parity — variant level (testsets, environments)**: `TestsetFork`/`EnvironmentFork` DTOs + service methods + `TestsetForkRequest`/`EnvironmentForkRequest` models + router endpoints added. + +--- + +## Previously done + +- [x] Commit wrapper field unified to `_revision: RevisionCommit` across all 6 entities (models, routers, tests, SDK, web) +- [x] Log wrapper field unified to `_revisions: RevisionsLog` across all 6 entities (models, routers, tests, SDK, web) +- [x] `RetrievalInfo` moved from router layer to `core/git/` (PR #4469) +- [x] Full environment references (env triple + target triple) surfaced in retrieve responses and forwarded into traces (PR #4469) diff --git a/sdks/python/agenta/sdk/managers/shared.py b/sdks/python/agenta/sdk/managers/shared.py index 89f4d4a064..2df67098ca 100644 --- a/sdks/python/agenta/sdk/managers/shared.py +++ b/sdks/python/agenta/sdk/managers/shared.py @@ -937,7 +937,7 @@ def history( method="POST", endpoint="/applications/revisions/log", json={ - "application": { + "application_revisions_log": { "application_id": variant.get("application_id") or variant.get("artifact_id"), "application_variant_id": variant.get("application_variant_id") @@ -980,7 +980,7 @@ async def ahistory( method="POST", endpoint="/applications/revisions/log", json={ - "application": { + "application_revisions_log": { "application_id": variant.get("application_id") or variant.get("artifact_id"), "application_variant_id": variant.get("application_variant_id") diff --git a/web/oss/src/components/DeploymentsDashboard/store/deploymentStore.ts b/web/oss/src/components/DeploymentsDashboard/store/deploymentStore.ts index c57ec5d6eb..3e2189f84e 100644 --- a/web/oss/src/components/DeploymentsDashboard/store/deploymentStore.ts +++ b/web/oss/src/components/DeploymentsDashboard/store/deploymentStore.ts @@ -213,7 +213,7 @@ export const deploymentPaginatedStore = createPaginatedEntityStore< // Filter out v0 revisions (auto-created initial revisions) and revisions // that don't contain a reference for the current app (client-side safety net - // in case the backend doesn't filter by application_refs) + // in case the backend doesn't filter by `references`) const withAppRef = response.environment_revisions .filter((r) => (r.version ?? 0) > 0) .filter((r) => { diff --git a/web/packages/agenta-entities/src/environment/api/api.ts b/web/packages/agenta-entities/src/environment/api/api.ts index fe8662770e..c990a1a06e 100644 --- a/web/packages/agenta-entities/src/environment/api/api.ts +++ b/web/packages/agenta-entities/src/environment/api/api.ts @@ -100,7 +100,7 @@ export async function fetchEnvironmentRevisionsList({ `${getAgentaApiUrl()}/environments/revisions/query`, { environment_refs: [{id: environmentId}], - ...(applicationId ? {application_refs: [{id: applicationId}]} : {}), + ...(applicationId ? {references: [{id: applicationId}]} : {}), ...(hasRevisionQuery ? {environment_revision: {message}} : {}), windowing: {limit: 100, order: "descending"}, },