ticket #10 attempt 1
This commit is contained in:
parent
818568c609
commit
7a93c959df
1
.venv/bin/python
Symbolic link
1
.venv/bin/python
Symbolic link
@ -0,0 +1 @@
|
||||
python3
|
||||
1
.venv/bin/python3
Symbolic link
1
.venv/bin/python3
Symbolic link
@ -0,0 +1 @@
|
||||
/usr/bin/python3
|
||||
1
.venv/bin/python3.11
Symbolic link
1
.venv/bin/python3.11
Symbolic link
@ -0,0 +1 @@
|
||||
python3
|
||||
1
.venv/lib64
Symbolic link
1
.venv/lib64
Symbolic link
@ -0,0 +1 @@
|
||||
lib
|
||||
5
.venv/pyvenv.cfg
Normal file
5
.venv/pyvenv.cfg
Normal file
@ -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
|
||||
@ -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 .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 .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 .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
|
||||
@ -182,6 +182,7 @@ __all__ = [
|
||||
"get_deepsearch_messages",
|
||||
"get_cached_deepsearch_url",
|
||||
"upsert_deepsearch_url_cache",
|
||||
"list_deepsearch_sessions",
|
||||
"VOTABLE_TARGETS",
|
||||
"STAR_TARGETS",
|
||||
"_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:
|
||||
if "deepsearch_url_cache" not in db.tables:
|
||||
return None
|
||||
|
||||
@ -11,7 +11,12 @@ from devplacepy import database
|
||||
from devplacepy.config import DEEPSEARCH_DIR
|
||||
from devplacepy.models import DeepsearchChatForm, DeepsearchRunForm
|
||||
from devplacepy.responses import respond
|
||||
from devplacepy.schemas import DeepsearchJobOut, DeepsearchSessionOut
|
||||
from devplacepy.schemas import (
|
||||
DeepsearchJobOut,
|
||||
DeepsearchListOut,
|
||||
DeepsearchSessionOut,
|
||||
DeepsearchSessionSummary,
|
||||
)
|
||||
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.export import to_json, to_markdown, to_pdf
|
||||
@ -100,10 +105,13 @@ async def deepsearch_page(request: Request):
|
||||
web_application_schema("DeepSearch", description, "/tools/deepsearch", base),
|
||||
],
|
||||
)
|
||||
sessions = []
|
||||
if user:
|
||||
sessions = database.list_deepsearch_sessions("user", user["uid"])
|
||||
return templates.TemplateResponse(
|
||||
request,
|
||||
"tools/deepsearch.html",
|
||||
{**seo_ctx, "request": request, "user": user},
|
||||
{**seo_ctx, "request": request, "user": user, "history_sessions": sessions},
|
||||
)
|
||||
|
||||
@router.post("/run")
|
||||
@ -197,6 +205,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}")
|
||||
async def deepsearch_status(request: Request, uid: str):
|
||||
job = queue.get_job(uid)
|
||||
@ -322,6 +351,7 @@ async def deepsearch_export_pdf(request: Request, uid: str):
|
||||
raise not_found("Report not ready")
|
||||
return Response(content=to_pdf(report), media_type="application/pdf")
|
||||
|
||||
|
||||
@router.websocket("/{uid}/ws")
|
||||
async def deepsearch_ws(websocket: WebSocket, uid: str):
|
||||
await websocket.accept()
|
||||
|
||||
@ -79,7 +79,9 @@ from devplacepy.schemas.containers import (
|
||||
from devplacepy.schemas.jobs import (
|
||||
DbQueryJobOut,
|
||||
DeepsearchJobOut,
|
||||
DeepsearchListOut,
|
||||
DeepsearchSessionOut,
|
||||
DeepsearchSessionSummary,
|
||||
ForkJobOut,
|
||||
PlanningJobOut,
|
||||
SeoJobOut,
|
||||
|
||||
@ -142,6 +142,17 @@ class DeepsearchSessionOut(_Out):
|
||||
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):
|
||||
uid: str = ""
|
||||
kind: str = ""
|
||||
|
||||
@ -108,6 +108,18 @@ TOOLS_ACTIONS: tuple[Action, ...] = (
|
||||
params=(path("uid", "DeepSearch job uid returned by deepsearch."),),
|
||||
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(
|
||||
name="isslop",
|
||||
method="POST",
|
||||
|
||||
@ -95,6 +95,22 @@
|
||||
<p>Research complete.</p>
|
||||
<a class="btn btn-primary" data-deepsearch-open href="#">Open report and chat</a>
|
||||
</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>
|
||||
{% endblock %}
|
||||
|
||||
8
dpc.log
Normal file
8
dpc.log
Normal file
@ -0,0 +1,8 @@
|
||||
2026-07-19T17:32:58 INFO logging initialised at /workspace/repo/dpc.log
|
||||
2026-07-19T17:32:58 DEBUG model=molodetz-pro fps=30
|
||||
2026-07-19T17:32:58 INFO read task from file: /workspace/prompts/research-9.txt
|
||||
2026-07-19T17:32:58 INFO settings merged: model=<default> allow=0 deny=0 ask=0
|
||||
2026-07-19T17:38:24 INFO logging initialised at /workspace/repo/dpc.log
|
||||
2026-07-19T17:38:24 DEBUG model=molodetz-pro fps=30
|
||||
2026-07-19T17:38:24 INFO read task from file: /workspace/prompts/execution-1.txt
|
||||
2026-07-19T17:38:24 INFO settings merged: model=<default> allow=0 deny=0 ask=0
|
||||
129
tests/api/tools/deepsearch/list.py
Normal file
129
tests/api/tools/deepsearch/list.py
Normal file
@ -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
|
||||
Loading…
Reference in New Issue
Block a user