mirror of
https://github.com/tiennm99/DocsGPT.git
synced 2026-10-13 04:14:42 +00:00
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.
100 lines
3.3 KiB
Python
100 lines
3.3 KiB
Python
"""Compose the chat presets from shared fragments at load time.
|
|
|
|
The six presets (3 tones x 2 retrieval modes) used to be six near-identical
|
|
files: only the ``## Answering`` section differed, so every edit to the shared
|
|
prose had to land six times and silently drifted when it didn't. Each preset is
|
|
now assembled from single-source fragments under ``prompts/fragments/``.
|
|
|
|
Composition happens **here, at load time**, not through Jinja ``{% include %}``:
|
|
|
|
* ``TemplateEngine`` runs a ``SandboxedEnvironment`` with no loader, so adding
|
|
one would open a file-read surface reachable from user-authored templates.
|
|
* ``StreamProcessor._get_required_tool_actions`` parses the *raw* prompt text to
|
|
decide which tools to pre-execute. With template inheritance the child file
|
|
would not contain ``{{ tools.memory.memory_view }}``, so memory prefetch would
|
|
silently stop working. Composing first keeps that parser seeing the whole
|
|
prompt.
|
|
|
|
The composed output is byte-identical to the presets this replaced; the
|
|
``tests/test_prompt_composer.py`` goldens pin that.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
from typing import Dict, List, Tuple
|
|
|
|
FRAGMENTS_DIR = Path(__file__).resolve().parent / "fragments"
|
|
|
|
# Section order of every composed preset. ``answering`` is the only slot that
|
|
# varies; the rest are shared verbatim. There is deliberately no tool-usage
|
|
# section: per-tool policy lives in each tool's action ``description``, so it
|
|
# is sent only when the tool is actually attached and cannot drift from the
|
|
# schema. Cross-tool arbitration stays in ``answering``.
|
|
_SECTION_ORDER: Tuple[str, ...] = (
|
|
"identity.txt",
|
|
"persona.txt",
|
|
None, # answering — resolved per (mode, tone)
|
|
"formatting.txt",
|
|
"boundaries.txt",
|
|
"platform.txt",
|
|
"memory.txt",
|
|
"attachments.txt",
|
|
)
|
|
|
|
# preset id -> (retrieval mode, tone)
|
|
PRESET_VARIANTS: Dict[str, Tuple[str, str]] = {
|
|
"default": ("classic", "default"),
|
|
"creative": ("classic", "creative"),
|
|
"strict": ("classic", "strict"),
|
|
"agentic_default": ("agentic", "default"),
|
|
"agentic_creative": ("agentic", "creative"),
|
|
"agentic_strict": ("agentic", "strict"),
|
|
}
|
|
|
|
_cache: Dict[str, str] = {}
|
|
|
|
|
|
def _read(relative: str) -> str:
|
|
"""Read one fragment.
|
|
|
|
Args:
|
|
relative: Path relative to the fragments directory.
|
|
|
|
Returns:
|
|
str: The fragment's raw text.
|
|
"""
|
|
return (FRAGMENTS_DIR / relative).read_text(encoding="utf-8")
|
|
|
|
|
|
def is_composed_preset(preset_id: str) -> bool:
|
|
"""Return True when ``preset_id`` is one this module composes."""
|
|
return preset_id in PRESET_VARIANTS
|
|
|
|
|
|
def compose_preset(preset_id: str) -> str:
|
|
"""Assemble a preset from its fragments.
|
|
|
|
Args:
|
|
preset_id: One of the keys in :data:`PRESET_VARIANTS`.
|
|
|
|
Returns:
|
|
str: The complete prompt template, ready for the Jinja renderer.
|
|
|
|
Raises:
|
|
KeyError: If ``preset_id`` is not a composed preset.
|
|
"""
|
|
cached = _cache.get(preset_id)
|
|
if cached is not None:
|
|
return cached
|
|
|
|
mode, tone = PRESET_VARIANTS[preset_id]
|
|
parts: List[str] = []
|
|
for section in _SECTION_ORDER:
|
|
parts.append(
|
|
_read(f"answering/{mode}_{tone}.txt") if section is None else _read(section)
|
|
)
|
|
composed = "".join(parts)
|
|
_cache[preset_id] = composed
|
|
return composed
|