fed8b2eed7
Backend release / release (push) Waiting to run
Bandit Security Scan / bandit_scan (push) Waiting to run
Build and push multi-arch DocsGPT Docker image / build (linux/amd64, ubuntu-latest, amd64) (push) Waiting to run
Build and push multi-arch DocsGPT Docker image / build (linux/arm64, ubuntu-24.04-arm, arm64) (push) Waiting to run
Build and push multi-arch DocsGPT Docker image / manifest (push) Blocked by required conditions
Build and push DocsGPT FE Docker image for development / build (linux/amd64, ubuntu-latest, amd64) (push) Waiting to run
Build and push DocsGPT FE Docker image for development / build (linux/arm64, ubuntu-24.04-arm, arm64) (push) Waiting to run
Build and push DocsGPT FE Docker image for development / manifest (push) Blocked by required conditions
Python linting / ruff (push) Waiting to run
Run python tests with pytest / Run tests and count coverage (3.12) (push) Waiting to run
React Widget Build / build (push) Waiting to run
68 lines
2.5 KiB
Python
68 lines
2.5 KiB
Python
"""Virtual short artifact handles (``A1``, ``A2``, ...) the model can type to reference an artifact.
|
|
|
|
A ref is NOT a stored column: ``A{n}`` is the artifact's STABLE per-parent ``ref_seq``, assigned at
|
|
creation and kept in the artifact's ``metadata``, so deleting an earlier artifact no longer
|
|
re-points a later ref the model already holds. Artifacts created before ``ref_seq`` existed have
|
|
none, so resolution falls back to the legacy positional (n-th by created_at) lookup. Refs resolve
|
|
only inside the caller's parent (``conversation_id`` or ``workflow_run_id``), never cross-tenant;
|
|
resolution still goes through the parent-scoped authz gate.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from typing import Any, Optional
|
|
|
|
from application.storage.db.base_repository import looks_like_uuid
|
|
|
|
_REF_RE = re.compile(r"^[Aa](\d+)$")
|
|
|
|
|
|
def make_ref(position: int) -> str:
|
|
"""Build the short ref string for a 1-based position (``1`` -> ``"A1"``)."""
|
|
return f"A{position}"
|
|
|
|
|
|
def parse_ref(value: Any) -> Optional[int]:
|
|
"""Parse a short ref like ``A1``/``a2`` into its 1-based position, or None when it is not a ref."""
|
|
if not isinstance(value, str):
|
|
return None
|
|
match = _REF_RE.match(value.strip())
|
|
if match is None:
|
|
return None
|
|
position = int(match.group(1))
|
|
return position if position >= 1 else None
|
|
|
|
|
|
def resolve_artifact_id(
|
|
repo: Any,
|
|
raw: Any,
|
|
*,
|
|
conversation_id: Optional[str] = None,
|
|
workflow_run_id: Optional[str] = None,
|
|
) -> Optional[str]:
|
|
"""Resolve a short ref or a uuid to an artifact id, scoped to the caller's parent; None otherwise."""
|
|
position = parse_ref(raw)
|
|
if position is not None:
|
|
# A ref is the artifact's stable per-parent ``ref_seq``: resolve by it first so a
|
|
# deletion of an earlier artifact never re-points this ref. Legacy rows (created
|
|
# before ref_seq) and repos without the newer method fall back to the positional
|
|
# (n-th by created_at) lookup.
|
|
by_seq = getattr(repo, "resolve_id_by_ref_seq", None)
|
|
if callable(by_seq):
|
|
resolved = by_seq(
|
|
position,
|
|
conversation_id=conversation_id,
|
|
workflow_run_id=workflow_run_id,
|
|
)
|
|
if resolved is not None:
|
|
return resolved
|
|
return repo.artifact_id_at_position(
|
|
position,
|
|
conversation_id=conversation_id,
|
|
workflow_run_id=workflow_run_id,
|
|
)
|
|
if looks_like_uuid(raw):
|
|
return str(raw).strip()
|
|
return None
|