Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions src/backend/workflows/durable/talk_representation_workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,20 @@ def _handle(request: WorkflowActionRequest) -> WorkflowActionResult:
request.inputs.get("presentation_concept_id"),
request.data.get("presentation_concept_id"),
)
# This action materialises exactly one presentation. A programme-level
# list cannot identify that record, even when a workshop title is present.
if not presentation_concept_id and _resolve_context_list(
request, "existing_presentation_concept_ids"
):
return WorkflowActionResult(
status="failed",
error=(
"talk_representation_requires_single_presentation:"
"provide presentation_concept_id and the fields for that talk; "
"existing_presentation_concept_ids is a programme-level list, "
"not a single presentation input"
),
)
if not title and not presentation_concept_id:
return WorkflowActionResult(
status="failed",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"family_id": "talk_representation_workflow_seed_bundle",
"managed_by": "talk_representation_workflow_vontology_service",
"schema_version": "repo_seed_workflow_bundle.v1",
"seed_version": "1",
"seed_version": "2",
"source_tag": "JVNAUTOSCI-1459",
"supported_action_ids": [
"talk.normalise_inputs",
Expand All @@ -12,8 +12,8 @@
],
"workflows": [
{
"content": "Represent a talk by normalising presentation inputs, materialising or reusing the presentation concept, linking the presenter, and verifying the requested talk representation profile.",
"description": "Canonical durable workflow for representing talks and presentations in Vontology from structured meeting and speaker inputs.",
"content": "Normalise one presentation, materialise or reuse its explicit concept ID, link its presenter when supplied, and verify the requested fields. Optional meeting and speaker fields may be omitted. A programme list of existing_presentation_concept_ids must first be resolved into separate per-presentation inputs by the caller; it is not the singular presentation_concept_id.",
"description": "Represent one talk or presentation from structured inputs: provide presentation_concept_id to reuse an existing concept, or title to create one. This is not a programme/batch enrichment or source-text recovery workflow.",
"display_name": "Talk Representation Workflow",
"publication_spec": {
"initial_state": "normalise_inputs",
Expand All @@ -27,67 +27,67 @@
{
"concept_id": "#V#workflow_mapping_talk_representation_workflow_normalise_inputs_presentation_concept_id_to_presentation_concept_id_parameter",
"context_key": "presentation_concept_id",
"required": true,
"required": false,
"tool_param": "presentation_concept_id"
},
{
"concept_id": "#V#workflow_mapping_talk_representation_workflow_normalise_inputs_title_to_title_parameter",
"context_key": "title",
"required": true,
"required": false,
"tool_param": "title"
},
{
"concept_id": "#V#workflow_mapping_talk_representation_workflow_normalise_inputs_speaker_name_to_speaker_name_parameter",
"context_key": "speaker_name",
"required": true,
"required": false,
"tool_param": "speaker_name"
},
{
"concept_id": "#V#workflow_mapping_talk_representation_workflow_normalise_inputs_summary_to_summary_parameter",
"context_key": "summary",
"required": true,
"required": false,
"tool_param": "summary"
},
{
"concept_id": "#V#workflow_mapping_talk_representation_workflow_normalise_inputs_start_time_to_start_time_parameter",
"context_key": "start_time",
"required": true,
"required": false,
"tool_param": "start_time"
},
{
"concept_id": "#V#workflow_mapping_talk_representation_workflow_normalise_inputs_meeting_link_to_meeting_link_parameter",
"context_key": "meeting_link",
"required": true,
"required": false,
"tool_param": "meeting_link"
},
{
"concept_id": "#V#workflow_mapping_talk_representation_workflow_normalise_inputs_meeting_id_to_meeting_id_parameter",
"context_key": "meeting_id",
"required": true,
"required": false,
"tool_param": "meeting_id"
},
{
"concept_id": "#V#workflow_mapping_talk_representation_workflow_normalise_inputs_meeting_passcode_to_meeting_passcode_parameter",
"context_key": "meeting_passcode",
"required": true,
"required": false,
"tool_param": "meeting_passcode"
},
{
"concept_id": "#V#workflow_mapping_talk_representation_workflow_normalise_inputs_presentation_status_to_presentation_status_parameter",
"context_key": "presentation_status",
"required": true,
"required": false,
"tool_param": "presentation_status"
},
{
"concept_id": "#V#workflow_mapping_talk_representation_workflow_normalise_inputs_presentation_type_ids_to_presentation_type_ids_parameter",
"context_key": "presentation_type_ids",
"required": true,
"required": false,
"tool_param": "presentation_type_ids"
},
{
"concept_id": "#V#workflow_mapping_talk_representation_workflow_normalise_inputs_verification_profile_to_verification_profile_parameter",
"context_key": "verification_profile",
"required": true,
"required": false,
"tool_param": "verification_profile"
}
],
Expand Down Expand Up @@ -1168,5 +1168,22 @@
"Capability-gap note: this workflow depends on Python durable actions because current VWL/runtime primitives cannot yet express a reusable, deterministic representation-materialisation primitive for concept creation/update plus postcondition verification. Workflow #V#academic_presentation_instance_workflow should migrate fully into VWL once the runtime can declaratively express stable concept reuse, singleton text upserts, relationship writes, and verification against required representation predicates."
]
}
]
],
"known_legacy_authority_payload_sha256_by_seed_version": {
"#V#talk_representation_workflow": {
"1": [
"10121992e1bfe7d848eabc4dbf325dd5bd73caabfd40c7b1a15365ebb2ed9f5b"
]
},
"#V#technical_scientific_talk_representation_workflow": {
"1": [
"37b003e5b07ced18137c37e4a7a45e0a5cd07afcdd94d2b100ecf1a50bb7d961"
]
},
"#V#academic_presentation_instance_workflow": {
"1": [
"7ca0537839d2f03b85ee7250e1d38297f1f26d073a1f2f99ce9dd56df2a318ee"
]
}
}
}
130 changes: 130 additions & 0 deletions tests/backend/test_talk_representation_workflow_vontology_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,3 +203,133 @@ def test_technical_scientific_talk_workflow_materialises_and_verifies_representa
for row in status_rows
if isinstance(row, dict)
)

@pytest.mark.parametrize(
"workflow_id",
[
TALK_REPRESENTATION_WORKFLOW_ID,
TECHNICAL_SCIENTIFIC_TALK_REPRESENTATION_WORKFLOW_ID,
ACADEMIC_PRESENTATION_INSTANCE_WORKFLOW_ID,
],
)
def test_sparse_existing_presentation_reuses_identity_idempotently(
_reset_mock_db: Any,
workflow_id: str,
) -> None:
bootstrap_canonical_talk_representation_workflows()
existing_id = "#V#existing_programme_presentation"
concept_service.create_concept(
concept_id=existing_id,
name="Existing programme presentation",
parent_concept_ids=["#V#presentation"],
create_as_instance=True,
)
definition = load_workflow_definition_from_vontology(workflow_id)
executor = WorkflowExecutor(
registry=registry_factory.build_durable_action_registry(),
max_transitions=20,
)
from src.backend.db.mongo_client import get_db

before = set(get_db().concepts.distinct("concept_id"))
for _ in range(2):
result = executor.run(
definition,
environment=WorkflowEnvironment(
llm_client=None,
user_namespace="#V#test_user",
),
data={"presentation_concept_id": existing_id},
)
assert result.completed, (result.error, result.data.get("last_action_error"))
assert result.data["presentation_concept_id"] == existing_id
assert result.data["talk_representation_verified"] is True
assert (
result.data["materialisation_report"]["created_presentation_concept"]
is False
)
assert set(get_db().concepts.distinct("concept_id")) == before


def test_direct_talk_accepts_title_without_optional_meeting_fields(_reset_mock_db: Any):
bootstrap_canonical_talk_representation_workflows()
definition = load_workflow_definition_from_vontology(
TALK_REPRESENTATION_WORKFLOW_ID
)
result = WorkflowExecutor(
registry=registry_factory.build_durable_action_registry(),
max_transitions=20,
).run(
definition,
environment=WorkflowEnvironment(llm_client=None, user_namespace="#V#test_user"),
data={"title": "Sparse new presentation"},
)
assert result.completed, (result.error, result.data.get("last_action_error"))
assert result.data["talk_representation_verified"] is True


@pytest.mark.parametrize("title", [None, "Workshop programme"])
def test_batch_ids_are_not_silently_converted_to_a_new_presentation(
_reset_mock_db: Any,
title: str | None,
):
bootstrap_canonical_talk_representation_workflows()
from src.backend.db.mongo_client import get_db

before = set(get_db().concepts.distinct("concept_id"))
definition = load_workflow_definition_from_vontology(
TALK_REPRESENTATION_WORKFLOW_ID
)
data = {"existing_presentation_concept_ids": ["#V#talk_one", "#V#talk_two"]}
if title:
data["title"] = title
result = WorkflowExecutor(
registry=registry_factory.build_durable_action_registry(),
max_transitions=20,
).run(
definition,
environment=WorkflowEnvironment(llm_client=None, user_namespace="#V#test_user"),
data=data,
)
assert not result.data.get("talk_representation_materialised")
assert "talk_representation_requires_single_presentation" in str(
result.data.get("last_action_error")
)
assert set(get_db().concepts.distinct("concept_id")) == before


def test_reviewed_v1_migrates_optional_inputs_without_forced_publication(
_reset_mock_db: Any,
tmp_path,
monkeypatch,
):
import json
from src.backend.services import (
talk_representation_workflow_vontology_service as service,
)

path = service._REPO_SEED_ASSET_PATH
legacy = json.loads(path.read_text())
legacy["seed_version"] = "1"
legacy.pop("known_legacy_authority_payload_sha256_by_seed_version")
for mapping in legacy["workflows"][0]["publication_spec"]["steps"][0][
"context_input_mapping_specs"
]:
mapping["required"] = True
old_path = tmp_path / "legacy.json"
old_path.write_text(json.dumps(legacy))
monkeypatch.setattr(service, "_REPO_SEED_ASSET_PATH", old_path)
service.bootstrap_canonical_talk_representation_workflows()
old = load_workflow_definition_from_vontology(TALK_REPRESENTATION_WORKFLOW_ID)
assert (
"presentation_concept_id"
in old.states[old.initial_state].metadata["reads_context_keys"]
)
monkeypatch.setattr(service, "_REPO_SEED_ASSET_PATH", path)
report = service.bootstrap_canonical_talk_representation_workflows()
assert report["publication"]["counts"]["errors"] == 0
assert report["publication"]["counts"]["workflows_published"] == 1
current = load_workflow_definition_from_vontology(TALK_REPRESENTATION_WORKFLOW_ID)
assert not current.states[current.initial_state].metadata.get("reads_context_keys")
repeat = service.bootstrap_canonical_talk_representation_workflows()
assert repeat["publication"]["skipped"] is True
Loading