feat: add project file system with CRUD, upload, inline editing, and video attachment support

- Add new `/projects/{slug}/files` endpoint group for per-project filesystem operations including directory and file CRUD, upload, and inline editing with public read and owner write access
- Extend attachment system to support video formats (webm, ogv, mov, m4v) with proper file icons and MIME types
- Implement configurable allowed file types via `allowed_file_types` site setting, replacing hardcoded `ALLOWED_UPLOAD_TYPES` with dynamic `allowed_extensions()` and `is_extension_allowed()` functions
- Add `delete_all_project_files()` call in `delete_content_item()` to clean up project files when a project is deleted
- Create database indexes on `project_files` table for `(project_uid, path)` and `(project_uid, parent_path)` to optimize file lookups
- Introduce `docs_prose.py` module with `render_prose()` function that renders Markdown content inside `data-render` divs using mistune, enabling dynamic prose rendering in documentation pages
- Enhance docs search with Markdown-aware text stripping (`_demarkdown()`) and improved HTML/script/style sanitization for better search indexing
- Update documentation API samples to reflect new attachment response fields (`is_image`, `is_video`, `mime_type`) and note video format support
- Update README to document the new project files endpoint and clarify AI gateway attribution for guest Devii sessions
This commit is contained in:
2026-06-08 20:51:09 +00:00
parent e0535bb7c5
commit 97eb58fc19
110 changed files with 5534 additions and 603 deletions
+34 -10
View File
@@ -12,17 +12,25 @@ from devplacepy.config import INTERNAL_GATEWAY_URL, INTERNAL_MODEL
DEFAULT_AI_URL = INTERNAL_GATEWAY_URL
DEFAULT_AI_MODEL = INTERNAL_MODEL
DEFAULT_BASE_URL = "http://127.0.0.1:10500"
DEFAULT_TIMEOUT_SECONDS = 45.0
DEFAULT_MAX_RESPONSE_CHARS = 12000
CONTEXT_WINDOW_TOKENS = 1_048_576
MAX_OUTPUT_TOKENS = 384_000
SYSTEM_RESERVE_TOKENS = 64_000
CHARS_PER_TOKEN = 3
CONTEXT_INPUT_BUDGET_TOKENS = CONTEXT_WINDOW_TOKENS - MAX_OUTPUT_TOKENS - SYSTEM_RESERVE_TOKENS
MIN_TIMEOUT_SECONDS = 300.0
DEFAULT_TIMEOUT_SECONDS = 300.0
DEFAULT_MAX_RESPONSE_CHARS = 200_000
DEFAULT_MAX_TOOL_ITERATIONS = 40
DEFAULT_DELEGATE_MAX_ITERATIONS = 25
DEFAULT_CONTEXT_COMPACT_THRESHOLD = 220_000
DEFAULT_CONTEXT_COMPACT_THRESHOLD = CONTEXT_INPUT_BUDGET_TOKENS * CHARS_PER_TOKEN
DEFAULT_CONTEXT_KEEP_TAIL = 12
DEFAULT_RECALL_TOP_K = 3
DEFAULT_FETCH_MAX_CHARS = 24_000
DEFAULT_FETCH_TIMEOUT_SECONDS = 30.0
DEFAULT_FETCH_MAX_BYTES = 5_000_000
DEFAULT_FETCH_MAX_CHARS = 200_000
DEFAULT_FETCH_TIMEOUT_SECONDS = 300.0
DEFAULT_FETCH_MAX_BYTES = 8_000_000
DEFAULT_RSEARCH_URL = "https://rsearch.app.molodetz.nl"
DEFAULT_RSEARCH_TIMEOUT_SECONDS = 300.0
@dataclass(frozen=True)
@@ -55,6 +63,8 @@ class Settings:
allow_eval: bool
rsearch_enabled: bool
rsearch_url: str
rsearch_timeout_seconds: float
daily_limit_usd: float
def load_settings() -> Settings:
@@ -94,6 +104,10 @@ def load_settings() -> Settings:
allow_eval=os.environ.get("DEVII_ALLOW_EVAL", "1").lower() in ("1", "true", "yes", "on"),
rsearch_enabled=os.environ.get("DEVII_RSEARCH_ENABLED", "1").lower() in ("1", "true", "yes", "on"),
rsearch_url=os.environ.get("DEVII_RSEARCH_URL", DEFAULT_RSEARCH_URL).rstrip("/"),
rsearch_timeout_seconds=float(
os.environ.get("DEVII_RSEARCH_TIMEOUT", DEFAULT_RSEARCH_TIMEOUT_SECONDS)
),
daily_limit_usd=0.0,
)
@@ -110,19 +124,24 @@ FIELD_PRICE_CACHE_HIT = "devii_price_cache_hit"
FIELD_PRICE_CACHE_MISS = "devii_price_cache_miss"
FIELD_PRICE_OUTPUT = "devii_price_output"
FIELD_ALLOW_EVAL = "devii_allow_eval"
FIELD_TIMEOUT = "devii_timeout"
FIELD_FETCH_TIMEOUT = "devii_fetch_timeout"
FIELD_RSEARCH_ENABLED = "devii_rsearch_enabled"
FIELD_RSEARCH_URL = "devii_rsearch_url"
FIELD_RSEARCH_TIMEOUT = "devii_rsearch_timeout"
LESSONS_DB_PATH = os.environ.get("DEVII_LESSONS_DB", "devii_lessons.db")
def build_settings(config: dict, base_url: str, api_key: str) -> Settings:
def build_settings(config: dict, base_url: str, api_key: str, owner_kind: str = "guest") -> Settings:
configured_key = config[FIELD_AI_KEY] or str(uuid.uuid4())
ai_key = api_key if (owner_kind == "user" and api_key) else configured_key
return Settings(
ai_url=config[FIELD_AI_URL] or DEFAULT_AI_URL,
ai_key=config[FIELD_AI_KEY] or str(uuid.uuid4()),
ai_key=ai_key,
ai_model=config[FIELD_AI_MODEL] or DEFAULT_AI_MODEL,
base_url=(base_url or DEFAULT_BASE_URL).rstrip("/"),
timeout_seconds=DEFAULT_TIMEOUT_SECONDS,
timeout_seconds=float(config.get(FIELD_TIMEOUT) or DEFAULT_TIMEOUT_SECONDS),
max_response_chars=DEFAULT_MAX_RESPONSE_CHARS,
max_tool_iterations=int(config[FIELD_MAX_ITERATIONS]),
log_level="INFO",
@@ -140,10 +159,15 @@ def build_settings(config: dict, base_url: str, api_key: str) -> Settings:
login_email="",
login_password="",
fetch_max_chars=DEFAULT_FETCH_MAX_CHARS,
fetch_timeout_seconds=DEFAULT_FETCH_TIMEOUT_SECONDS,
fetch_timeout_seconds=float(config.get(FIELD_FETCH_TIMEOUT) or DEFAULT_FETCH_TIMEOUT_SECONDS),
fetch_max_bytes=DEFAULT_FETCH_MAX_BYTES,
fetch_allow_private=False,
allow_eval=bool(config.get(FIELD_ALLOW_EVAL, True)),
rsearch_enabled=bool(config.get(FIELD_RSEARCH_ENABLED, True)),
rsearch_url=(config.get(FIELD_RSEARCH_URL) or DEFAULT_RSEARCH_URL).rstrip("/"),
rsearch_timeout_seconds=float(config.get(FIELD_RSEARCH_TIMEOUT) or DEFAULT_RSEARCH_TIMEOUT_SECONDS),
daily_limit_usd=float(
(config.get(FIELD_GUEST_DAILY_USD, 0.05) if owner_kind == "guest"
else config.get(FIELD_USER_DAILY_USD, 1.0)) or 0.0
),
)