Files
DocsGPT/docsgpt/agents/tools/artifact_ref.py
T
Alex 574f96341e refactor: rename the application package to docsgpt
The backend import package is now docsgpt, the name it will carry on PyPI;
application was far too generic to install into anyone's site-packages.
git mv plus a mechanical rewrite of every import, dotted string and path
reference: 734 Python files, the compose files, Dockerfile, workflows, docs,
setup scripts, devcontainer, k8s manifests, vscode config, pytest and coverage
config, .gitignore. Behaviour is unchanged.

Kept for one release:
- A top-level application package whose meta-path finder resolves
  application.x.y to the already-imported docsgpt.x.y object, so old imports
  and entry points (celery -A application.app.celery,
  uvicorn application.asgi:asgi_app) keep working with a FutureWarning.
- Celery registers every application.* task name as an alias of its
  docsgpt.* task on start-up, so messages queued by the previous release still
  run. The redbeat key prefix moves to redbeat:docsgpt:v2: so schedule entries
  the previous release wrote are left unread instead of firing twice.

The backend image builds from the repository root (docker build -f
docsgpt/Dockerfile .) so it can ship the alias package; a root .dockerignore
allow-lists docsgpt/ and application/ and keeps caches, local data, .env
files, the sample index files and the Dockerfile out. Compose and the image
workflows point at the new context.
2026-09-07 10:20:43 +01:00

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 docsgpt.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