forked from retoor/devplacepy
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e5cb4772e0 | ||
|
|
ea943ac502 | ||
|
|
7a93c959df |
File diff suppressed because one or more lines are too long
@@ -39,10 +39,6 @@ NGINX_MAX_BODY_SIZE=50m
|
|||||||
NGINX_CACHE_ENABLED=false
|
NGINX_CACHE_ENABLED=false
|
||||||
NGINX_CACHE_MAX_SIZE=1g
|
NGINX_CACHE_MAX_SIZE=1g
|
||||||
|
|
||||||
# Max length of comment text (applied server-side on write and client-side on input).
|
|
||||||
# Change at any time via the env var; the DB has no length constraint.
|
|
||||||
DEVPLACE_COMMENT_MAX_LENGTH=1000
|
|
||||||
|
|
||||||
# Run the app container as this host user so shared files keep dev ownership.
|
# Run the app container as this host user so shared files keep dev ownership.
|
||||||
DEVPLACE_UID=1000
|
DEVPLACE_UID=1000
|
||||||
DEVPLACE_GID=1000
|
DEVPLACE_GID=1000
|
||||||
|
|||||||
Symlink
+1
@@ -0,0 +1 @@
|
|||||||
|
python3
|
||||||
Symlink
+1
@@ -0,0 +1 @@
|
|||||||
|
/usr/bin/python3
|
||||||
Symlink
+1
@@ -0,0 +1 @@
|
|||||||
|
python3
|
||||||
Symlink
+1
@@ -0,0 +1 @@
|
|||||||
|
lib
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
home = /usr/bin
|
||||||
|
include-system-site-packages = false
|
||||||
|
version = 3.11.2
|
||||||
|
executable = /usr/bin/python3.11
|
||||||
|
command = /usr/bin/python3 -m venv /workspace/repo/.venv
|
||||||
@@ -169,7 +169,6 @@ The log is **administrator-only**. `/admin/audit-log` is a paginated, filterable
|
|||||||
| `DEVPLACE_PRESENCE_TIMEOUT_SECONDS` | `60` | Online-presence window: a user counts as online for this many seconds after their last activity. `last_seen` is refreshed by a throttled in-place update at most once per half this interval per worker (no per-load inserts, no data growth) |
|
| `DEVPLACE_PRESENCE_TIMEOUT_SECONDS` | `60` | Online-presence window: a user counts as online for this many seconds after their last activity. `last_seen` is refreshed by a throttled in-place update at most once per half this interval per worker (no per-load inserts, no data growth) |
|
||||||
| `DEVPLACE_PRESENCE_ONLINE_LIMIT` | `30` | Maximum avatars shown in the feed's live "Online now" panel (ordered alphabetically by username) |
|
| `DEVPLACE_PRESENCE_ONLINE_LIMIT` | `30` | Maximum avatars shown in the feed's live "Online now" panel (ordered alphabetically by username) |
|
||||||
| `DEVPLACE_PRESENCE_ONLINE_MARGIN_SECONDS` | `20` | Grace margin before an online user drops to offline (hysteresis): online at the timeout, offline only after timeout + this. Prevents online/offline flicker for users hovering at the boundary |
|
| `DEVPLACE_PRESENCE_ONLINE_MARGIN_SECONDS` | `20` | Grace margin before an online user drops to offline (hysteresis): online at the timeout, offline only after timeout + this. Prevents online/offline flicker for users hovering at the boundary |
|
||||||
| `DEVPLACE_COMMENT_MAX_LENGTH` | `1000` | Maximum character length for comment body text. Applied server-side via Pydantic validation and client-side via `maxLength` on textareas. The database has no column-length constraint, so this can be changed at any time via the environment variable (requires restart) |
|
|
||||||
|
|
||||||
### Runtime settings
|
### Runtime settings
|
||||||
|
|
||||||
|
|||||||
@@ -68,7 +68,6 @@ INTERNAL_GATEWAY_URL = f"{INTERNAL_BASE_URL}/openai/v1/chat/completions"
|
|||||||
INTERNAL_EMBED_URL = f"{INTERNAL_BASE_URL}/openai/v1/embeddings"
|
INTERNAL_EMBED_URL = f"{INTERNAL_BASE_URL}/openai/v1/embeddings"
|
||||||
INTERNAL_MODEL = "molodetz"
|
INTERNAL_MODEL = "molodetz"
|
||||||
INTERNAL_EMBED_MODEL = "molodetz~embed"
|
INTERNAL_EMBED_MODEL = "molodetz~embed"
|
||||||
COMMENT_MAX_LENGTH = int(environ.get("DEVPLACE_COMMENT_MAX_LENGTH", "1000"))
|
|
||||||
DEFAULT_CORRECTION_PROMPT = "Leave literary as is, only do punctuation and casing"
|
DEFAULT_CORRECTION_PROMPT = "Leave literary as is, only do punctuation and casing"
|
||||||
DEFAULT_MODIFIER_PROMPT = (
|
DEFAULT_MODIFIER_PROMPT = (
|
||||||
"Execute what is behind `@ai` (the prompt) and replace that part including `@ai`"
|
"Execute what is behind `@ai` (the prompt) and replace that part including `@ai`"
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ from .email import EMAIL_ACCOUNT_DEFAULTS, list_email_accounts, get_email_accoun
|
|||||||
from .notifications import NOTIFICATION_TYPES, NOTIFICATION_CHANNELS, _NOTIFICATION_CHANNEL_COLUMNS, _NOTIFICATION_CHANNEL_DEFAULTS, _NOTIFICATION_TYPE_KEYS, _notification_prefs_cache, _notification_default, get_notification_default, set_notification_default, _notification_overrides, notification_enabled, get_notification_prefs, set_notification_pref, reset_notification_prefs, mark_notifications_read_by_target
|
from .notifications import NOTIFICATION_TYPES, NOTIFICATION_CHANNELS, _NOTIFICATION_CHANNEL_COLUMNS, _NOTIFICATION_CHANNEL_DEFAULTS, _NOTIFICATION_TYPE_KEYS, _notification_prefs_cache, _notification_default, get_notification_default, set_notification_default, _notification_overrides, notification_enabled, get_notification_prefs, set_notification_pref, reset_notification_prefs, mark_notifications_read_by_target
|
||||||
from .forks import record_fork, get_fork_parent, count_forks, soft_delete_fork_relations, delete_fork_relations
|
from .forks import record_fork, get_fork_parent, count_forks, soft_delete_fork_relations, delete_fork_relations
|
||||||
from .follows import get_follow_counts, get_follow_list, get_following_among
|
from .follows import get_follow_counts, get_follow_list, get_following_among
|
||||||
from .deepsearch import _ds_now, create_deepsearch_session, update_deepsearch_session, get_deepsearch_session, add_deepsearch_message, get_deepsearch_messages, get_cached_deepsearch_url, upsert_deepsearch_url_cache
|
from .deepsearch import _ds_now, create_deepsearch_session, update_deepsearch_session, get_deepsearch_session, add_deepsearch_message, get_deepsearch_messages, get_cached_deepsearch_url, upsert_deepsearch_url_cache, list_deepsearch_sessions
|
||||||
from .ranking import VOTABLE_TARGETS, STAR_TARGETS, _authors_cache, _ranked_authors, _rank_map, get_top_authors, get_leaderboard, get_user_rank, get_user_stars, clear_user_stars, update_target_stars, soft_delete_engagement, delete_engagement, get_target_owner_uid
|
from .ranking import VOTABLE_TARGETS, STAR_TARGETS, _authors_cache, _ranked_authors, _rank_map, get_top_authors, get_leaderboard, get_user_rank, get_user_stars, clear_user_stars, update_target_stars, soft_delete_engagement, delete_engagement, get_target_owner_uid
|
||||||
from .comments import _drop_blocked, _build_comment_items, load_comments, get_recent_comments_by_target_uids, get_recent_comments_by_post_uids, load_comments_by_target_uids
|
from .comments import _drop_blocked, _build_comment_items, load_comments, get_recent_comments_by_target_uids, get_recent_comments_by_post_uids, load_comments_by_target_uids
|
||||||
from .content import resolve_by_slug, resolve_object_url, get_uids_by_username_match, text_search_clause, get_daily_topic, get_featured_news
|
from .content import resolve_by_slug, resolve_object_url, get_uids_by_username_match, text_search_clause, get_daily_topic, get_featured_news
|
||||||
@@ -182,6 +182,7 @@ __all__ = [
|
|||||||
"get_deepsearch_messages",
|
"get_deepsearch_messages",
|
||||||
"get_cached_deepsearch_url",
|
"get_cached_deepsearch_url",
|
||||||
"upsert_deepsearch_url_cache",
|
"upsert_deepsearch_url_cache",
|
||||||
|
"list_deepsearch_sessions",
|
||||||
"VOTABLE_TARGETS",
|
"VOTABLE_TARGETS",
|
||||||
"STAR_TARGETS",
|
"STAR_TARGETS",
|
||||||
"_authors_cache",
|
"_authors_cache",
|
||||||
|
|||||||
@@ -84,6 +84,23 @@ def get_deepsearch_messages(session_uid: str, limit: int = 50) -> list[dict]:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def list_deepsearch_sessions(
|
||||||
|
owner_kind: str, owner_id: str, limit: int = 50
|
||||||
|
) -> list[dict]:
|
||||||
|
if "deepsearch_sessions" not in db.tables:
|
||||||
|
return []
|
||||||
|
rows = list(
|
||||||
|
get_table("deepsearch_sessions").find(
|
||||||
|
owner_kind=owner_kind,
|
||||||
|
owner_id=owner_id,
|
||||||
|
deleted_at=None,
|
||||||
|
order_by=["created_at desc"],
|
||||||
|
_limit=limit,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return sorted(rows, key=lambda r: (r.get("created_at", ""), r.get("uid", "")), reverse=True)
|
||||||
|
|
||||||
|
|
||||||
def get_cached_deepsearch_url(url_hash: str) -> dict | None:
|
def get_cached_deepsearch_url(url_hash: str) -> dict | None:
|
||||||
if "deepsearch_url_cache" not in db.tables:
|
if "deepsearch_url_cache" not in db.tables:
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
# retoor <retoor@molodetz.nl>
|
# retoor <retoor@molodetz.nl>
|
||||||
|
|
||||||
from devplacepy.config import COMMENT_MAX_LENGTH
|
|
||||||
from .._shared import COMMENT_TARGETS, GIST_LANGUAGES, PROJECT_TYPES, endpoint, field
|
from .._shared import COMMENT_TARGETS, GIST_LANGUAGES, PROJECT_TYPES, endpoint, field
|
||||||
from devplacepy.constants import TOPICS
|
from devplacepy.constants import TOPICS
|
||||||
|
|
||||||
@@ -230,7 +229,7 @@ four ways to sign requests.
|
|||||||
"textarea",
|
"textarea",
|
||||||
True,
|
True,
|
||||||
"Nice work.",
|
"Nice work.",
|
||||||
f"Body, 3-{COMMENT_MAX_LENGTH} characters.",
|
"Body, 3-1000 characters.",
|
||||||
),
|
),
|
||||||
field(
|
field(
|
||||||
"target_uid",
|
"target_uid",
|
||||||
@@ -291,7 +290,7 @@ four ways to sign requests.
|
|||||||
"textarea",
|
"textarea",
|
||||||
True,
|
True,
|
||||||
"Edited body.",
|
"Edited body.",
|
||||||
f"New body, 3-{COMMENT_MAX_LENGTH} characters.",
|
"New body, 3-1000 characters.",
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
sample_response={
|
sample_response={
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
# retoor <retoor@molodetz.nl>
|
# retoor <retoor@molodetz.nl>
|
||||||
|
|
||||||
from devplacepy.config import COMMENT_MAX_LENGTH
|
|
||||||
|
|
||||||
ROLE_LABELS = {"public": "Public", "user": "Member"}
|
ROLE_LABELS = {"public": "Public", "user": "Member"}
|
||||||
|
|
||||||
|
|
||||||
@@ -239,7 +237,7 @@ DEVRANT_GROUPS = {
|
|||||||
encoding="form",
|
encoding="form",
|
||||||
params=[
|
params=[
|
||||||
field("rant_id", "path", type="int", required=True, example="1", description="Rant id."),
|
field("rant_id", "path", type="int", required=True, example="1", description="Rant id."),
|
||||||
field("comment", "body", type="textarea", required=True, example="Great rant!", description=f"Comment text, 1-{COMMENT_MAX_LENGTH} chars."),
|
field("comment", "body", type="textarea", required=True, example="Great rant!", description="Comment text, 1-1000 chars."),
|
||||||
],
|
],
|
||||||
sample_response={"success": True},
|
sample_response={"success": True},
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from datetime import datetime
|
|||||||
from typing import Literal, Optional
|
from typing import Literal, Optional
|
||||||
from pydantic import BaseModel, Field, field_validator, model_validator
|
from pydantic import BaseModel, Field, field_validator, model_validator
|
||||||
from devplacepy.constants import TOPICS, REACTION_EMOJI
|
from devplacepy.constants import TOPICS, REACTION_EMOJI
|
||||||
from devplacepy.config import COMMENT_MAX_LENGTH, DEFAULT_CORRECTION_PROMPT, DEFAULT_MODIFIER_PROMPT
|
from devplacepy.config import DEFAULT_CORRECTION_PROMPT, DEFAULT_MODIFIER_PROMPT
|
||||||
|
|
||||||
|
|
||||||
def normalize_european_date(value):
|
def normalize_european_date(value):
|
||||||
@@ -157,7 +157,7 @@ class PostEditForm(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class CommentForm(BaseModel):
|
class CommentForm(BaseModel):
|
||||||
content: str = Field(min_length=3, max_length=COMMENT_MAX_LENGTH)
|
content: str = Field(min_length=3, max_length=1000)
|
||||||
target_uid: str = Field(default="", max_length=36)
|
target_uid: str = Field(default="", max_length=36)
|
||||||
post_uid: str = Field(default="", max_length=36)
|
post_uid: str = Field(default="", max_length=36)
|
||||||
target_type: Literal["post", "project", "news", "issue", "gist"] = "post"
|
target_type: Literal["post", "project", "news", "issue", "gist"] = "post"
|
||||||
@@ -172,7 +172,7 @@ class CommentForm(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class CommentEditForm(BaseModel):
|
class CommentEditForm(BaseModel):
|
||||||
content: str = Field(min_length=3, max_length=COMMENT_MAX_LENGTH)
|
content: str = Field(min_length=3, max_length=1000)
|
||||||
|
|
||||||
|
|
||||||
class ProjectForm(BaseModel):
|
class ProjectForm(BaseModel):
|
||||||
|
|||||||
@@ -69,8 +69,7 @@ async def edit_comment(request: Request, comment_id: str):
|
|||||||
if not is_owner(comment, user):
|
if not is_owner(comment, user):
|
||||||
return dr_error("You can only edit your own comments.", fail_reason="not_owner")
|
return dr_error("You can only edit your own comments.", fail_reason="not_owner")
|
||||||
text = (params.get("comment") or "").strip()
|
text = (params.get("comment") or "").strip()
|
||||||
from devplacepy.config import COMMENT_MAX_LENGTH
|
if len(text) < 1 or len(text) > 1000:
|
||||||
if len(text) < 1 or len(text) > COMMENT_MAX_LENGTH:
|
|
||||||
return dr_error("Invalid comment length.", fail_reason="length")
|
return dr_error("Invalid comment length.", fail_reason="length")
|
||||||
get_table("comments").update(
|
get_table("comments").update(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -227,8 +227,7 @@ async def comment_rant(request: Request, rant_id: str):
|
|||||||
text = (params.get("comment") or "").strip()
|
text = (params.get("comment") or "").strip()
|
||||||
if len(text) < 1:
|
if len(text) < 1:
|
||||||
return dr_error("Your comment is too short.")
|
return dr_error("Your comment is too short.")
|
||||||
from devplacepy.config import COMMENT_MAX_LENGTH
|
if len(text) > 1000:
|
||||||
if len(text) > COMMENT_MAX_LENGTH:
|
|
||||||
return dr_error("Your comment is too long.")
|
return dr_error("Your comment is too long.")
|
||||||
create_comment_record(request, user, "post", post["uid"], text)
|
create_comment_record(request, user, "post", post["uid"], text)
|
||||||
return dr_ok()
|
return dr_ok()
|
||||||
|
|||||||
@@ -11,7 +11,11 @@ from devplacepy import database
|
|||||||
from devplacepy.config import DEEPSEARCH_DIR
|
from devplacepy.config import DEEPSEARCH_DIR
|
||||||
from devplacepy.models import DeepsearchChatForm, DeepsearchRunForm
|
from devplacepy.models import DeepsearchChatForm, DeepsearchRunForm
|
||||||
from devplacepy.responses import respond
|
from devplacepy.responses import respond
|
||||||
from devplacepy.schemas import DeepsearchJobOut, DeepsearchSessionOut
|
from devplacepy.schemas import (
|
||||||
|
DeepsearchJobOut,
|
||||||
|
DeepsearchListOut,
|
||||||
|
DeepsearchSessionOut,
|
||||||
|
)
|
||||||
from devplacepy.seo import base_seo_context, site_url, web_application_schema, website_schema
|
from devplacepy.seo import base_seo_context, site_url, web_application_schema, website_schema
|
||||||
from devplacepy.services.deepsearch.chat import DeepsearchChat
|
from devplacepy.services.deepsearch.chat import DeepsearchChat
|
||||||
from devplacepy.services.deepsearch.export import to_json, to_markdown, to_pdf
|
from devplacepy.services.deepsearch.export import to_json, to_markdown, to_pdf
|
||||||
@@ -100,10 +104,13 @@ async def deepsearch_page(request: Request):
|
|||||||
web_application_schema("DeepSearch", description, "/tools/deepsearch", base),
|
web_application_schema("DeepSearch", description, "/tools/deepsearch", base),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
sessions = []
|
||||||
|
if user:
|
||||||
|
sessions = database.list_deepsearch_sessions("user", user["uid"])
|
||||||
return templates.TemplateResponse(
|
return templates.TemplateResponse(
|
||||||
request,
|
request,
|
||||||
"tools/deepsearch.html",
|
"tools/deepsearch.html",
|
||||||
{**seo_ctx, "request": request, "user": user},
|
{**seo_ctx, "request": request, "user": user, "history_sessions": sessions},
|
||||||
)
|
)
|
||||||
|
|
||||||
@router.post("/run")
|
@router.post("/run")
|
||||||
@@ -197,6 +204,27 @@ def _enqueue(uid: str, payload: dict, owner_kind: str, owner_id: str, query: str
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/list")
|
||||||
|
async def deepsearch_list(request: Request):
|
||||||
|
owner_kind, owner_id = owner_for(request)
|
||||||
|
sessions = database.list_deepsearch_sessions(owner_kind, owner_id)
|
||||||
|
return JSONResponse(
|
||||||
|
DeepsearchListOut.model_validate(
|
||||||
|
{"sessions": [_session_summary(s) for s in sessions]}
|
||||||
|
).model_dump(mode="json")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _session_summary(session: dict) -> dict:
|
||||||
|
return {
|
||||||
|
"uid": session.get("uid", ""),
|
||||||
|
"query": session.get("query", ""),
|
||||||
|
"status": session.get("status", ""),
|
||||||
|
"created_at": session.get("created_at", ""),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{uid}")
|
@router.get("/{uid}")
|
||||||
async def deepsearch_status(request: Request, uid: str):
|
async def deepsearch_status(request: Request, uid: str):
|
||||||
job = queue.get_job(uid)
|
job = queue.get_job(uid)
|
||||||
@@ -322,6 +350,7 @@ async def deepsearch_export_pdf(request: Request, uid: str):
|
|||||||
raise not_found("Report not ready")
|
raise not_found("Report not ready")
|
||||||
return Response(content=to_pdf(report), media_type="application/pdf")
|
return Response(content=to_pdf(report), media_type="application/pdf")
|
||||||
|
|
||||||
|
|
||||||
@router.websocket("/{uid}/ws")
|
@router.websocket("/{uid}/ws")
|
||||||
async def deepsearch_ws(websocket: WebSocket, uid: str):
|
async def deepsearch_ws(websocket: WebSocket, uid: str):
|
||||||
await websocket.accept()
|
await websocket.accept()
|
||||||
|
|||||||
@@ -79,7 +79,9 @@ from devplacepy.schemas.containers import (
|
|||||||
from devplacepy.schemas.jobs import (
|
from devplacepy.schemas.jobs import (
|
||||||
DbQueryJobOut,
|
DbQueryJobOut,
|
||||||
DeepsearchJobOut,
|
DeepsearchJobOut,
|
||||||
|
DeepsearchListOut,
|
||||||
DeepsearchSessionOut,
|
DeepsearchSessionOut,
|
||||||
|
DeepsearchSessionSummary,
|
||||||
ForkJobOut,
|
ForkJobOut,
|
||||||
PlanningJobOut,
|
PlanningJobOut,
|
||||||
SeoJobOut,
|
SeoJobOut,
|
||||||
|
|||||||
@@ -142,6 +142,17 @@ class DeepsearchSessionOut(_Out):
|
|||||||
completed_at: Optional[str] = None
|
completed_at: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class DeepsearchSessionSummary(_Out):
|
||||||
|
uid: str = ""
|
||||||
|
query: str = ""
|
||||||
|
status: str = ""
|
||||||
|
created_at: str = ""
|
||||||
|
|
||||||
|
|
||||||
|
class DeepsearchListOut(_Out):
|
||||||
|
sessions: list = []
|
||||||
|
|
||||||
|
|
||||||
class DbQueryJobOut(_Out):
|
class DbQueryJobOut(_Out):
|
||||||
uid: str = ""
|
uid: str = ""
|
||||||
kind: str = ""
|
kind: str = ""
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ MENTION_POLL_SECONDS = int(os.environ.get("MENTION_POLL_SECONDS", "30"))
|
|||||||
DM_POLL_SECONDS = int(os.environ.get("DM_POLL_SECONDS", "10"))
|
DM_POLL_SECONDS = int(os.environ.get("DM_POLL_SECONDS", "10"))
|
||||||
BOT_USERNAME = os.environ.get("BOT_USERNAME", "")
|
BOT_USERNAME = os.environ.get("BOT_USERNAME", "")
|
||||||
|
|
||||||
COMMENT_CHAR_LIMIT = int(os.environ.get("DEVPLACE_COMMENT_MAX_LENGTH", "1000"))
|
COMMENT_CHAR_LIMIT = 1000
|
||||||
MESSAGE_CHAR_LIMIT = 2000
|
MESSAGE_CHAR_LIMIT = 2000
|
||||||
PART_SUFFIX_RESERVE = 12
|
PART_SUFFIX_RESERVE = 12
|
||||||
PART_DELIVERY_DELAY = 0.5
|
PART_DELIVERY_DELAY = 0.5
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from devplacepy.config import COMMENT_MAX_LENGTH
|
|
||||||
from ..spec import Action
|
from ..spec import Action
|
||||||
from ._shared import ATTACHMENTS, TARGET_TYPE, body, confirm, path
|
from ._shared import ATTACHMENTS, TARGET_TYPE, body, confirm, path
|
||||||
|
|
||||||
@@ -29,7 +28,7 @@ COMMENTS_ACTIONS: tuple[Action, ...] = (
|
|||||||
summary="Edit the body of one of your own comments",
|
summary="Edit the body of one of your own comments",
|
||||||
params=(
|
params=(
|
||||||
path("comment_uid", "Uid of the comment."),
|
path("comment_uid", "Uid of the comment."),
|
||||||
body("content", f"New comment body, 3-{COMMENT_MAX_LENGTH} characters.", required=True),
|
body("content", "New comment body, 3-1000 characters.", required=True),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Action(
|
Action(
|
||||||
|
|||||||
@@ -108,6 +108,18 @@ TOOLS_ACTIONS: tuple[Action, ...] = (
|
|||||||
params=(path("uid", "DeepSearch job uid returned by deepsearch."),),
|
params=(path("uid", "DeepSearch job uid returned by deepsearch."),),
|
||||||
requires_auth=False,
|
requires_auth=False,
|
||||||
),
|
),
|
||||||
|
Action(
|
||||||
|
name="deepsearch_list",
|
||||||
|
method="GET",
|
||||||
|
path="/tools/deepsearch/list",
|
||||||
|
summary="List the user's past DeepSearch sessions",
|
||||||
|
description=(
|
||||||
|
"Returns the signed-in user's deep research session history, newest first, each "
|
||||||
|
"with its uid, query, status and created_at."
|
||||||
|
),
|
||||||
|
params=(),
|
||||||
|
requires_auth=True,
|
||||||
|
),
|
||||||
Action(
|
Action(
|
||||||
name="isslop",
|
name="isslop",
|
||||||
method="POST",
|
method="POST",
|
||||||
|
|||||||
@@ -83,8 +83,6 @@ export class CommentManager {
|
|||||||
textarea.className = "emoji-picker-target";
|
textarea.className = "emoji-picker-target";
|
||||||
textarea.value = text.dataset.raw || text.textContent;
|
textarea.value = text.dataset.raw || text.textContent;
|
||||||
textarea.rows = 3;
|
textarea.rows = 3;
|
||||||
const ref = document.querySelector("[data-comment-maxlength]");
|
|
||||||
textarea.maxLength = ref ? parseInt(ref.dataset.commentMaxlength, 10) : 1000;
|
|
||||||
form.appendChild(textarea);
|
form.appendChild(textarea);
|
||||||
|
|
||||||
const actions = document.createElement("div");
|
const actions = document.createElement("div");
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
{% if user %}
|
{% if user %}
|
||||||
<form class="comment-form" method="POST" action="/comments/create" data-comment-maxlength="{{ comment_max_length }}">
|
<form class="comment-form" method="POST" action="/comments/create">
|
||||||
<input type="hidden" name="target_uid" value="{{ _comment_target_uid }}">
|
<input type="hidden" name="target_uid" value="{{ _comment_target_uid }}">
|
||||||
<input type="hidden" name="target_type" value="{{ _comment_target_type }}">
|
<input type="hidden" name="target_type" value="{{ _comment_target_type }}">
|
||||||
{% set _user = user %}{% set _size = 32 %}{% set _size_class = "sm" %}{% include "_avatar_link.html" %}
|
{% set _user = user %}{% set _size = 32 %}{% set _size_class = "sm" %}{% include "_avatar_link.html" %}
|
||||||
<textarea name="content" placeholder="Your opinion goes here..." required aria-required="true" aria-label="Comment" maxlength="{{ comment_max_length }}" class="emoji-picker-target" data-mention></textarea>
|
<textarea name="content" placeholder="Your opinion goes here..." required aria-required="true" aria-label="Comment" maxlength="1000" class="emoji-picker-target" data-mention></textarea>
|
||||||
<div class="comment-form-actions">
|
<div class="comment-form-actions">
|
||||||
<dp-upload multiple
|
<dp-upload multiple
|
||||||
max-size="{{ max_upload_size_mb() }}"
|
max-size="{{ max_upload_size_mb() }}"
|
||||||
|
|||||||
@@ -95,6 +95,22 @@
|
|||||||
<p>Research complete.</p>
|
<p>Research complete.</p>
|
||||||
<a class="btn btn-primary" data-deepsearch-open href="#">Open report and chat</a>
|
<a class="btn btn-primary" data-deepsearch-open href="#">Open report and chat</a>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{% if user and history_sessions %}
|
||||||
|
<section class="ds-history mt-6">
|
||||||
|
<h2 class="text-lg font-semibold mb-2">My Research History</h2>
|
||||||
|
<ul class="space-y-1">
|
||||||
|
{% for session in history_sessions %}
|
||||||
|
<li>
|
||||||
|
<a href="/tools/deepsearch/{{ session.uid }}/session"
|
||||||
|
class="text-blue-600 hover:underline text-sm">
|
||||||
|
{{ session.query }} — <span class="text-gray-500">{{ session.created_at[:16].replace('T', ' ') }}</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import jinja2
|
|||||||
from fastapi.templating import Jinja2Templates
|
from fastapi.templating import Jinja2Templates
|
||||||
from markupsafe import Markup, escape
|
from markupsafe import Markup, escape
|
||||||
from devplacepy.cache import TTLCache
|
from devplacepy.cache import TTLCache
|
||||||
from devplacepy.config import COMMENT_MAX_LENGTH, STATIC_VERSION, TEMPLATES_DIR, TEMPLATE_AUTO_RELOAD, PRESENCE_TIMEOUT_SECONDS
|
from devplacepy.config import STATIC_VERSION, TEMPLATES_DIR, TEMPLATE_AUTO_RELOAD, PRESENCE_TIMEOUT_SECONDS
|
||||||
from devplacepy.constants import TOPICS, REACTION_EMOJI
|
from devplacepy.constants import TOPICS, REACTION_EMOJI
|
||||||
from devplacepy.database import get_int_setting, get_setting, get_table
|
from devplacepy.database import get_int_setting, get_setting, get_table
|
||||||
from devplacepy.avatar import avatar_url, avatar_seed
|
from devplacepy.avatar import avatar_url, avatar_seed
|
||||||
@@ -67,7 +67,6 @@ templates.env.globals["is_self"] = is_self
|
|||||||
templates.env.globals["guest_disabled"] = guest_disabled
|
templates.env.globals["guest_disabled"] = guest_disabled
|
||||||
templates.env.globals["is_online"] = presence.is_online
|
templates.env.globals["is_online"] = presence.is_online
|
||||||
templates.env.globals["presence_timeout"] = PRESENCE_TIMEOUT_SECONDS
|
templates.env.globals["presence_timeout"] = PRESENCE_TIMEOUT_SECONDS
|
||||||
templates.env.globals["comment_max_length"] = COMMENT_MAX_LENGTH
|
|
||||||
|
|
||||||
from devplacepy.docs_devrant import devrant_endpoints
|
from devplacepy.docs_devrant import devrant_endpoints
|
||||||
|
|
||||||
|
|||||||
@@ -1,28 +1,16 @@
|
|||||||
2026-07-19T08:11:05 INFO logging initialised at /workspace/repo/dpc.log
|
2026-07-19T17:32:58 INFO logging initialised at /workspace/repo/dpc.log
|
||||||
2026-07-19T08:11:05 DEBUG model=molodetz-pro fps=30
|
2026-07-19T17:32:58 DEBUG model=molodetz-pro fps=30
|
||||||
2026-07-19T08:11:05 INFO read task from file: /workspace/prompts/research-1.txt
|
2026-07-19T17:32:58 INFO read task from file: /workspace/prompts/research-9.txt
|
||||||
2026-07-19T08:11:05 INFO settings merged: model=<default> allow=0 deny=0 ask=0
|
2026-07-19T17:32:58 INFO settings merged: model=<default> allow=0 deny=0 ask=0
|
||||||
2026-07-19T15:03:09 INFO logging initialised at /workspace/repo/dpc.log
|
2026-07-19T17:38:24 INFO logging initialised at /workspace/repo/dpc.log
|
||||||
2026-07-19T15:03:09 DEBUG model=molodetz-pro fps=30
|
2026-07-19T17:38:24 DEBUG model=molodetz-pro fps=30
|
||||||
2026-07-19T15:03:09 INFO read task from file: /workspace/prompts/research-2.txt
|
2026-07-19T17:38:24 INFO read task from file: /workspace/prompts/execution-1.txt
|
||||||
2026-07-19T15:03:09 INFO settings merged: model=<default> allow=0 deny=0 ask=0
|
2026-07-19T17:38:24 INFO settings merged: model=<default> allow=0 deny=0 ask=0
|
||||||
2026-07-19T15:40:16 INFO logging initialised at /workspace/repo/dpc.log
|
2026-07-19T18:33:02 INFO logging initialised at /workspace/repo/dpc.log
|
||||||
2026-07-19T15:40:16 DEBUG model=molodetz-pro fps=30
|
2026-07-19T18:33:02 DEBUG model=molodetz-pro fps=30
|
||||||
2026-07-19T15:40:16 INFO read task from file: /workspace/prompts/research-3.txt
|
2026-07-19T18:33:02 INFO read task from file: /workspace/prompts/execution-2.txt
|
||||||
2026-07-19T15:40:16 INFO settings merged: model=<default> allow=0 deny=0 ask=0
|
2026-07-19T18:33:02 INFO settings merged: model=<default> allow=0 deny=0 ask=0
|
||||||
2026-07-19T17:35:33 INFO logging initialised at /workspace/repo/dpc.log
|
2026-07-19T19:00:28 INFO logging initialised at /workspace/repo/dpc.log
|
||||||
2026-07-19T17:35:33 DEBUG model=molodetz-pro fps=30
|
2026-07-19T19:00:28 DEBUG model=molodetz-pro fps=30
|
||||||
2026-07-19T17:35:33 INFO read task from file: /workspace/prompts/research-4.txt
|
2026-07-19T19:00:28 INFO read task from file: /workspace/prompts/execution-3.txt
|
||||||
2026-07-19T17:35:33 INFO settings merged: model=<default> allow=0 deny=0 ask=0
|
2026-07-19T19:00:28 INFO settings merged: model=<default> allow=0 deny=0 ask=0
|
||||||
2026-07-19T18:11:47 INFO logging initialised at /workspace/repo/dpc.log
|
|
||||||
2026-07-19T18:11:47 DEBUG model=molodetz-pro fps=30
|
|
||||||
2026-07-19T18:11:47 INFO read task from file: /workspace/prompts/execution-1.txt
|
|
||||||
2026-07-19T18:11:47 INFO settings merged: model=<default> allow=0 deny=0 ask=0
|
|
||||||
2026-07-19T18:31:31 INFO logging initialised at /workspace/repo/dpc.log
|
|
||||||
2026-07-19T18:31:31 DEBUG model=molodetz-pro fps=30
|
|
||||||
2026-07-19T18:31:31 INFO read task from file: /workspace/prompts/execution-2.txt
|
|
||||||
2026-07-19T18:31:31 INFO settings merged: model=<default> allow=0 deny=0 ask=0
|
|
||||||
2026-07-19T18:58:51 INFO logging initialised at /workspace/repo/dpc.log
|
|
||||||
2026-07-19T18:58:51 DEBUG model=molodetz-pro fps=30
|
|
||||||
2026-07-19T18:58:51 INFO read task from file: /workspace/prompts/execution-3.txt
|
|
||||||
2026-07-19T18:58:51 INFO settings merged: model=<default> allow=0 deny=0 ask=0
|
|
||||||
|
|||||||
@@ -345,22 +345,3 @@ def test_comment_links_multiple_attachments(app_server):
|
|||||||
assert u1 in r.text and u2 in r.text, (
|
assert u1 in r.text and u2 in r.text, (
|
||||||
"both attachments must be linked and displayed on the comment"
|
"both attachments must be linked and displayed on the comment"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_create_comment_too_long_rejected(app_server):
|
|
||||||
from devplacepy.config import COMMENT_MAX_LENGTH
|
|
||||||
s, _ = _session_comments()
|
|
||||||
post_uid = _create_post_comments(s, f"cmt-long-{int(time.time() * 1000)}")
|
|
||||||
r = s.post(
|
|
||||||
f"{BASE_URL}/comments/create",
|
|
||||||
data={
|
|
||||||
"content": "x" * (COMMENT_MAX_LENGTH + 1),
|
|
||||||
"target_type": "post",
|
|
||||||
"post_uid": post_uid,
|
|
||||||
"target_uid": post_uid,
|
|
||||||
},
|
|
||||||
allow_redirects=False,
|
|
||||||
)
|
|
||||||
assert r.status_code in (400, 422), r.text[:300]
|
|
||||||
refresh_snapshot()
|
|
||||||
assert get_table("comments").find_one(target_uid=post_uid) is None
|
|
||||||
|
|||||||
@@ -129,19 +129,3 @@ def test_edit_comment_too_short_rejected(app_server):
|
|||||||
assert r.status_code in (400, 422)
|
assert r.status_code in (400, 422)
|
||||||
refresh_snapshot()
|
refresh_snapshot()
|
||||||
assert get_table("comments").find_one(uid=comment["uid"])["content"] == "Long enough body"
|
assert get_table("comments").find_one(uid=comment["uid"])["content"] == "Long enough body"
|
||||||
|
|
||||||
|
|
||||||
def test_edit_comment_too_long_rejected(app_server):
|
|
||||||
from devplacepy.config import COMMENT_MAX_LENGTH
|
|
||||||
s, _ = _session_edit()
|
|
||||||
post_uid = _create_post_edit(s, f"edit-long-{int(time.time() * 1000)}")
|
|
||||||
comment = _create_comment_edit(s, post_uid, "Normal body length")
|
|
||||||
r = s.post(
|
|
||||||
f"{BASE_URL}/comments/edit/{comment['uid']}",
|
|
||||||
headers=JSON_edit,
|
|
||||||
data={"content": "x" * (COMMENT_MAX_LENGTH + 1)},
|
|
||||||
allow_redirects=False,
|
|
||||||
)
|
|
||||||
assert r.status_code in (400, 422), r.text[:300]
|
|
||||||
refresh_snapshot()
|
|
||||||
assert get_table("comments").find_one(uid=comment["uid"])["content"] == "Normal body length"
|
|
||||||
|
|||||||
@@ -0,0 +1,129 @@
|
|||||||
|
# retoor <retoor@molodetz.nl>
|
||||||
|
|
||||||
|
import time
|
||||||
|
import pytest
|
||||||
|
import requests
|
||||||
|
from tests.conftest import BASE_URL
|
||||||
|
from devplacepy.database import (
|
||||||
|
create_deepsearch_session,
|
||||||
|
get_table,
|
||||||
|
refresh_snapshot,
|
||||||
|
set_setting,
|
||||||
|
)
|
||||||
|
from devplacepy.services.jobs import queue
|
||||||
|
|
||||||
|
JSON = {"Accept": "application/json"}
|
||||||
|
_counter = [0]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="module", autouse=True)
|
||||||
|
def _settings(app_server):
|
||||||
|
set_setting("rate_limit_per_minute", "1000000")
|
||||||
|
set_setting("registration_open", "1")
|
||||||
|
yield
|
||||||
|
|
||||||
|
|
||||||
|
def _user():
|
||||||
|
_counter[0] += 1
|
||||||
|
name = f"dsl{int(time.time() * 1000)}{_counter[0]}"
|
||||||
|
requests.post(
|
||||||
|
f"{BASE_URL}/auth/signup",
|
||||||
|
data={
|
||||||
|
"username": name,
|
||||||
|
"email": f"{name}@t.dev",
|
||||||
|
"password": "secret123",
|
||||||
|
"confirm_password": "secret123",
|
||||||
|
},
|
||||||
|
allow_redirects=True,
|
||||||
|
)
|
||||||
|
refresh_snapshot()
|
||||||
|
user = get_table("users").find_one(username=name)
|
||||||
|
return user["uid"], user["api_key"]
|
||||||
|
|
||||||
|
|
||||||
|
def _seed_session(owner_id, query="test query"):
|
||||||
|
uid = queue.enqueue(
|
||||||
|
"deepsearch",
|
||||||
|
{"query": query, "depth": 1, "max_pages": 5},
|
||||||
|
"user",
|
||||||
|
owner_id,
|
||||||
|
"DeepSearch",
|
||||||
|
)
|
||||||
|
create_deepsearch_session(
|
||||||
|
uid, "user", owner_id, query, 1, 5, f"ds_{uid.replace('-', '')}"
|
||||||
|
)
|
||||||
|
refresh_snapshot()
|
||||||
|
return uid
|
||||||
|
|
||||||
|
|
||||||
|
def _clear():
|
||||||
|
refresh_snapshot()
|
||||||
|
jobs = get_table("jobs")
|
||||||
|
for row in list(jobs.find(kind="deepsearch")):
|
||||||
|
jobs.delete(uid=row["uid"])
|
||||||
|
sessions = get_table("deepsearch_sessions")
|
||||||
|
for row in list(sessions.find()):
|
||||||
|
sessions.delete(uid=row["uid"])
|
||||||
|
|
||||||
|
|
||||||
|
def test_list_returns_own_sessions(app_server):
|
||||||
|
try:
|
||||||
|
owner_uid, api_key = _user()
|
||||||
|
uid = _seed_session(owner_uid, query="first research")
|
||||||
|
headers = {"X-API-KEY": api_key, **JSON}
|
||||||
|
r = requests.get(f"{BASE_URL}/tools/deepsearch/list", headers=headers)
|
||||||
|
assert r.status_code == 200, r.text
|
||||||
|
body = r.json()
|
||||||
|
assert "sessions" in body
|
||||||
|
assert len(body["sessions"]) == 1
|
||||||
|
assert body["sessions"][0]["uid"] == uid
|
||||||
|
assert body["sessions"][0]["query"] == "first research"
|
||||||
|
assert body["sessions"][0]["status"] == "pending"
|
||||||
|
assert "created_at" in body["sessions"][0]
|
||||||
|
finally:
|
||||||
|
_clear()
|
||||||
|
|
||||||
|
|
||||||
|
def test_list_multiple_sessions_sorted_newest_first(app_server):
|
||||||
|
try:
|
||||||
|
owner_uid, api_key = _user()
|
||||||
|
uids = []
|
||||||
|
for i in range(3):
|
||||||
|
uids.append(_seed_session(owner_uid, query=f"research {i}"))
|
||||||
|
headers = {"X-API-KEY": api_key, **JSON}
|
||||||
|
r = requests.get(f"{BASE_URL}/tools/deepsearch/list", headers=headers)
|
||||||
|
assert r.status_code == 200, r.text
|
||||||
|
body = r.json()
|
||||||
|
assert len(body["sessions"]) == 3
|
||||||
|
uids_in_response = [s["uid"] for s in body["sessions"]]
|
||||||
|
assert sorted(uids_in_response, reverse=True) == uids_in_response
|
||||||
|
actual_queries = [s["query"] for s in body["sessions"]]
|
||||||
|
assert len(set(actual_queries)) == 3
|
||||||
|
finally:
|
||||||
|
_clear()
|
||||||
|
|
||||||
|
|
||||||
|
def test_list_does_not_include_other_users_sessions(app_server):
|
||||||
|
try:
|
||||||
|
owner_uid, api_key = _user()
|
||||||
|
_seed_session(owner_uid, query="my research")
|
||||||
|
other_uid, _ = _user()
|
||||||
|
_seed_session(other_uid, query="other research")
|
||||||
|
headers = {"X-API-KEY": api_key, **JSON}
|
||||||
|
r = requests.get(f"{BASE_URL}/tools/deepsearch/list", headers=headers)
|
||||||
|
assert r.status_code == 200, r.text
|
||||||
|
body = r.json()
|
||||||
|
assert len(body["sessions"]) == 1
|
||||||
|
assert body["sessions"][0]["query"] == "my research"
|
||||||
|
finally:
|
||||||
|
_clear()
|
||||||
|
|
||||||
|
|
||||||
|
def test_list_empty_for_no_sessions(app_server):
|
||||||
|
owner_uid, api_key = _user()
|
||||||
|
headers = {"X-API-KEY": api_key, **JSON}
|
||||||
|
r = requests.get(f"{BASE_URL}/tools/deepsearch/list", headers=headers)
|
||||||
|
assert r.status_code == 200, r.text
|
||||||
|
body = r.json()
|
||||||
|
assert "sessions" in body
|
||||||
|
assert len(body["sessions"]) == 0
|
||||||
Reference in New Issue
Block a user